1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server; 18 19 import android.hardware.Sensor; 20 import android.hardware.SensorEvent; 21 import android.hardware.SensorEventListener; 22 import android.hardware.SensorManager; 23 import android.os.Handler; 24 import android.os.Message; 25 import android.os.PowerManager; 26 import android.os.SystemClock; 27 import android.util.Slog; 28 29 import java.lang.Float; 30 31 /** 32 * Determines if the device has been set upon a stationary object. 33 */ 34 public class AnyMotionDetector { 35 interface DeviceIdleCallback { onAnyMotionResult(int result)36 public void onAnyMotionResult(int result); 37 } 38 39 private static final String TAG = "AnyMotionDetector"; 40 41 private static final boolean DEBUG = false; 42 43 /** Stationary status is unknown due to insufficient orientation measurements. */ 44 public static final int RESULT_UNKNOWN = -1; 45 46 /** Device is stationary, e.g. still on a table. */ 47 public static final int RESULT_STATIONARY = 0; 48 49 /** Device has been moved. */ 50 public static final int RESULT_MOVED = 1; 51 52 /** Orientation measurements are being performed or are planned. */ 53 private static final int STATE_INACTIVE = 0; 54 55 /** No orientation measurements are being performed or are planned. */ 56 private static final int STATE_ACTIVE = 1; 57 58 /** Current measurement state. */ 59 private int mState; 60 61 /** Threshold energy above which the device is considered moving. */ 62 private final float THRESHOLD_ENERGY = 5f; 63 64 /** The duration of the accelerometer orientation measurement. */ 65 private static final long ORIENTATION_MEASUREMENT_DURATION_MILLIS = 2500; 66 67 /** The maximum duration we will collect accelerometer data. */ 68 private static final long ACCELEROMETER_DATA_TIMEOUT_MILLIS = 3000; 69 70 /** The interval between accelerometer orientation measurements. */ 71 private static final long ORIENTATION_MEASUREMENT_INTERVAL_MILLIS = 5000; 72 73 /** The maximum duration we will hold a wakelock to determine stationary status. */ 74 private static final long WAKELOCK_TIMEOUT_MILLIS = 30000; 75 76 /** 77 * The duration in milliseconds after which an orientation measurement is considered 78 * too stale to be used. 79 */ 80 private static final int STALE_MEASUREMENT_TIMEOUT_MILLIS = 2 * 60 * 1000; 81 82 /** The accelerometer sampling interval. */ 83 private static final int SAMPLING_INTERVAL_MILLIS = 40; 84 85 private final Handler mHandler; 86 private final Object mLock = new Object(); 87 private Sensor mAccelSensor; 88 private SensorManager mSensorManager; 89 private PowerManager.WakeLock mWakeLock; 90 91 /** Threshold angle in degrees beyond which the device is considered moving. */ 92 private final float mThresholdAngle; 93 94 /** The minimum number of samples required to detect AnyMotion. */ 95 private int mNumSufficientSamples; 96 97 /** True if an orientation measurement is in progress. */ 98 private boolean mMeasurementInProgress; 99 100 /** True if sendMessageDelayed() for the mMeasurementTimeout callback has been scheduled */ 101 private boolean mMeasurementTimeoutIsActive; 102 103 /** True if sendMessageDelayed() for the mWakelockTimeout callback has been scheduled */ 104 private boolean mWakelockTimeoutIsActive; 105 106 /** True if sendMessageDelayed() for the mSensorRestart callback has been scheduled */ 107 private boolean mSensorRestartIsActive; 108 109 /** The most recent gravity vector. */ 110 private Vector3 mCurrentGravityVector = null; 111 112 /** The second most recent gravity vector. */ 113 private Vector3 mPreviousGravityVector = null; 114 115 /** Running sum of squared errors. */ 116 private RunningSignalStats mRunningStats; 117 118 private DeviceIdleCallback mCallback = null; 119 AnyMotionDetector(PowerManager pm, Handler handler, SensorManager sm, DeviceIdleCallback callback, float thresholdAngle)120 public AnyMotionDetector(PowerManager pm, Handler handler, SensorManager sm, 121 DeviceIdleCallback callback, float thresholdAngle) { 122 if (DEBUG) Slog.d(TAG, "AnyMotionDetector instantiated."); 123 synchronized (mLock) { 124 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 125 mWakeLock.setReferenceCounted(false); 126 mHandler = handler; 127 mSensorManager = sm; 128 mAccelSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); 129 mMeasurementInProgress = false; 130 mMeasurementTimeoutIsActive = false; 131 mWakelockTimeoutIsActive = false; 132 mSensorRestartIsActive = false; 133 mState = STATE_INACTIVE; 134 mCallback = callback; 135 mThresholdAngle = thresholdAngle; 136 mRunningStats = new RunningSignalStats(); 137 mNumSufficientSamples = (int) Math.ceil( 138 ((double)ORIENTATION_MEASUREMENT_DURATION_MILLIS / SAMPLING_INTERVAL_MILLIS)); 139 if (DEBUG) Slog.d(TAG, "mNumSufficientSamples = " + mNumSufficientSamples); 140 } 141 } 142 143 /* 144 * Acquire accel data until we determine AnyMotion status. 145 */ checkForAnyMotion()146 public void checkForAnyMotion() { 147 if (DEBUG) { 148 Slog.d(TAG, "checkForAnyMotion(). mState = " + mState); 149 } 150 if (mState != STATE_ACTIVE) { 151 synchronized (mLock) { 152 mState = STATE_ACTIVE; 153 if (DEBUG) { 154 Slog.d(TAG, "Moved from STATE_INACTIVE to STATE_ACTIVE."); 155 } 156 mCurrentGravityVector = null; 157 mPreviousGravityVector = null; 158 mWakeLock.acquire(); 159 Message wakelockTimeoutMsg = Message.obtain(mHandler, mWakelockTimeout); 160 mHandler.sendMessageDelayed(wakelockTimeoutMsg, WAKELOCK_TIMEOUT_MILLIS); 161 mWakelockTimeoutIsActive = true; 162 startOrientationMeasurementLocked(); 163 } 164 } 165 } 166 stop()167 public void stop() { 168 synchronized (mLock) { 169 if (mState == STATE_ACTIVE) { 170 mState = STATE_INACTIVE; 171 if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE."); 172 } 173 mHandler.removeCallbacks(mMeasurementTimeout); 174 mHandler.removeCallbacks(mSensorRestart); 175 mMeasurementTimeoutIsActive = false; 176 mSensorRestartIsActive = false; 177 if (mMeasurementInProgress) { 178 mMeasurementInProgress = false; 179 mSensorManager.unregisterListener(mListener); 180 } 181 mCurrentGravityVector = null; 182 mPreviousGravityVector = null; 183 if (mWakeLock.isHeld()) { 184 mHandler.removeCallbacks(mWakelockTimeout); 185 mWakelockTimeoutIsActive = false; 186 mWakeLock.release(); 187 } 188 } 189 } 190 startOrientationMeasurementLocked()191 private void startOrientationMeasurementLocked() { 192 if (DEBUG) Slog.d(TAG, "startOrientationMeasurementLocked: mMeasurementInProgress=" + 193 mMeasurementInProgress + ", (mAccelSensor != null)=" + (mAccelSensor != null)); 194 if (!mMeasurementInProgress && mAccelSensor != null) { 195 if (mSensorManager.registerListener(mListener, mAccelSensor, 196 SAMPLING_INTERVAL_MILLIS * 1000)) { 197 mMeasurementInProgress = true; 198 mRunningStats.reset(); 199 } 200 Message measurementTimeoutMsg = Message.obtain(mHandler, mMeasurementTimeout); 201 mHandler.sendMessageDelayed(measurementTimeoutMsg, ACCELEROMETER_DATA_TIMEOUT_MILLIS); 202 mMeasurementTimeoutIsActive = true; 203 } 204 } 205 stopOrientationMeasurementLocked()206 private int stopOrientationMeasurementLocked() { 207 if (DEBUG) Slog.d(TAG, "stopOrientationMeasurement. mMeasurementInProgress=" + 208 mMeasurementInProgress); 209 int status = RESULT_UNKNOWN; 210 if (mMeasurementInProgress) { 211 mHandler.removeCallbacks(mMeasurementTimeout); 212 mMeasurementTimeoutIsActive = false; 213 mSensorManager.unregisterListener(mListener); 214 mMeasurementInProgress = false; 215 mPreviousGravityVector = mCurrentGravityVector; 216 mCurrentGravityVector = mRunningStats.getRunningAverage(); 217 if (mRunningStats.getSampleCount() == 0) { 218 Slog.w(TAG, "No accelerometer data acquired for orientation measurement."); 219 } 220 if (DEBUG) { 221 Slog.d(TAG, "mRunningStats = " + mRunningStats.toString()); 222 String currentGravityVectorString = (mCurrentGravityVector == null) ? 223 "null" : mCurrentGravityVector.toString(); 224 String previousGravityVectorString = (mPreviousGravityVector == null) ? 225 "null" : mPreviousGravityVector.toString(); 226 Slog.d(TAG, "mCurrentGravityVector = " + currentGravityVectorString); 227 Slog.d(TAG, "mPreviousGravityVector = " + previousGravityVectorString); 228 } 229 mRunningStats.reset(); 230 status = getStationaryStatus(); 231 if (DEBUG) Slog.d(TAG, "getStationaryStatus() returned " + status); 232 if (status != RESULT_UNKNOWN) { 233 if (mWakeLock.isHeld()) { 234 mHandler.removeCallbacks(mWakelockTimeout); 235 mWakelockTimeoutIsActive = false; 236 mWakeLock.release(); 237 } 238 if (DEBUG) { 239 Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE. status = " + status); 240 } 241 mState = STATE_INACTIVE; 242 } else { 243 /* 244 * Unknown due to insufficient measurements. Schedule another orientation 245 * measurement. 246 */ 247 if (DEBUG) Slog.d(TAG, "stopOrientationMeasurementLocked(): another measurement" + 248 " scheduled in " + ORIENTATION_MEASUREMENT_INTERVAL_MILLIS + 249 " milliseconds."); 250 Message msg = Message.obtain(mHandler, mSensorRestart); 251 mHandler.sendMessageDelayed(msg, ORIENTATION_MEASUREMENT_INTERVAL_MILLIS); 252 mSensorRestartIsActive = true; 253 } 254 } 255 return status; 256 } 257 258 /* 259 * Updates mStatus to the current AnyMotion status. 260 */ getStationaryStatus()261 public int getStationaryStatus() { 262 if ((mPreviousGravityVector == null) || (mCurrentGravityVector == null)) { 263 return RESULT_UNKNOWN; 264 } 265 Vector3 previousGravityVectorNormalized = mPreviousGravityVector.normalized(); 266 Vector3 currentGravityVectorNormalized = mCurrentGravityVector.normalized(); 267 float angle = previousGravityVectorNormalized.angleBetween(currentGravityVectorNormalized); 268 if (DEBUG) Slog.d(TAG, "getStationaryStatus: angle = " + angle 269 + " energy = " + mRunningStats.getEnergy()); 270 if ((angle < mThresholdAngle) && (mRunningStats.getEnergy() < THRESHOLD_ENERGY)) { 271 return RESULT_STATIONARY; 272 } else if (Float.isNaN(angle)) { 273 /** 274 * Floating point rounding errors have caused the angle calcuation's dot product to 275 * exceed 1.0. In such case, we report RESULT_MOVED to prevent devices from rapidly 276 * retrying this measurement. 277 */ 278 return RESULT_MOVED; 279 } 280 long diffTime = mCurrentGravityVector.timeMillisSinceBoot - 281 mPreviousGravityVector.timeMillisSinceBoot; 282 if (diffTime > STALE_MEASUREMENT_TIMEOUT_MILLIS) { 283 if (DEBUG) Slog.d(TAG, "getStationaryStatus: mPreviousGravityVector is too stale at " + 284 diffTime + " ms ago. Returning RESULT_UNKNOWN."); 285 return RESULT_UNKNOWN; 286 } 287 return RESULT_MOVED; 288 } 289 290 private final SensorEventListener mListener = new SensorEventListener() { 291 @Override 292 public void onSensorChanged(SensorEvent event) { 293 int status = RESULT_UNKNOWN; 294 synchronized (mLock) { 295 Vector3 accelDatum = new Vector3(SystemClock.elapsedRealtime(), event.values[0], 296 event.values[1], event.values[2]); 297 mRunningStats.accumulate(accelDatum); 298 299 // If we have enough samples, stop accelerometer data acquisition. 300 if (mRunningStats.getSampleCount() >= mNumSufficientSamples) { 301 status = stopOrientationMeasurementLocked(); 302 } 303 } 304 if (status != RESULT_UNKNOWN) { 305 mHandler.removeCallbacks(mWakelockTimeout); 306 mWakelockTimeoutIsActive = false; 307 mCallback.onAnyMotionResult(status); 308 } 309 } 310 311 @Override 312 public void onAccuracyChanged(Sensor sensor, int accuracy) { 313 } 314 }; 315 316 private final Runnable mSensorRestart = new Runnable() { 317 @Override 318 public void run() { 319 synchronized (mLock) { 320 if (mSensorRestartIsActive == true) { 321 mSensorRestartIsActive = false; 322 startOrientationMeasurementLocked(); 323 } 324 } 325 } 326 }; 327 328 private final Runnable mMeasurementTimeout = new Runnable() { 329 @Override 330 public void run() { 331 int status = RESULT_UNKNOWN; 332 synchronized (mLock) { 333 if (mMeasurementTimeoutIsActive == true) { 334 mMeasurementTimeoutIsActive = false; 335 if (DEBUG) Slog.i(TAG, "mMeasurementTimeout. Failed to collect sufficient accel " + 336 "data within " + ACCELEROMETER_DATA_TIMEOUT_MILLIS + " ms. Stopping " + 337 "orientation measurement."); 338 status = stopOrientationMeasurementLocked(); 339 if (status != RESULT_UNKNOWN) { 340 mHandler.removeCallbacks(mWakelockTimeout); 341 mWakelockTimeoutIsActive = false; 342 mCallback.onAnyMotionResult(status); 343 } 344 } 345 } 346 } 347 }; 348 349 private final Runnable mWakelockTimeout = new Runnable() { 350 @Override 351 public void run() { 352 synchronized (mLock) { 353 if (mWakelockTimeoutIsActive == true) { 354 mWakelockTimeoutIsActive = false; 355 stop(); 356 } 357 } 358 } 359 }; 360 361 /** 362 * A timestamped three dimensional vector and some vector operations. 363 */ 364 public static final class Vector3 { 365 public long timeMillisSinceBoot; 366 public float x; 367 public float y; 368 public float z; 369 Vector3(long timeMillisSinceBoot, float x, float y, float z)370 public Vector3(long timeMillisSinceBoot, float x, float y, float z) { 371 this.timeMillisSinceBoot = timeMillisSinceBoot; 372 this.x = x; 373 this.y = y; 374 this.z = z; 375 } 376 norm()377 public float norm() { 378 return (float) Math.sqrt(dotProduct(this)); 379 } 380 normalized()381 public Vector3 normalized() { 382 float mag = norm(); 383 return new Vector3(timeMillisSinceBoot, x / mag, y / mag, z / mag); 384 } 385 386 /** 387 * Returns the angle between this 3D vector and another given 3D vector. 388 * Assumes both have already been normalized. 389 * 390 * @param other The other Vector3 vector. 391 * @return angle between this vector and the other given one. 392 */ angleBetween(Vector3 other)393 public float angleBetween(Vector3 other) { 394 Vector3 crossVector = cross(other); 395 float degrees = Math.abs((float)Math.toDegrees( 396 Math.atan2(crossVector.norm(), dotProduct(other)))); 397 Slog.d(TAG, "angleBetween: this = " + this.toString() + 398 ", other = " + other.toString() + ", degrees = " + degrees); 399 return degrees; 400 } 401 cross(Vector3 v)402 public Vector3 cross(Vector3 v) { 403 return new Vector3( 404 v.timeMillisSinceBoot, 405 y * v.z - z * v.y, 406 z * v.x - x * v.z, 407 x * v.y - y * v.x); 408 } 409 410 @Override toString()411 public String toString() { 412 String msg = ""; 413 msg += "timeMillisSinceBoot=" + timeMillisSinceBoot; 414 msg += " | x=" + x; 415 msg += ", y=" + y; 416 msg += ", z=" + z; 417 return msg; 418 } 419 dotProduct(Vector3 v)420 public float dotProduct(Vector3 v) { 421 return x * v.x + y * v.y + z * v.z; 422 } 423 times(float val)424 public Vector3 times(float val) { 425 return new Vector3(timeMillisSinceBoot, x * val, y * val, z * val); 426 } 427 plus(Vector3 v)428 public Vector3 plus(Vector3 v) { 429 return new Vector3(v.timeMillisSinceBoot, x + v.x, y + v.y, z + v.z); 430 } 431 minus(Vector3 v)432 public Vector3 minus(Vector3 v) { 433 return new Vector3(v.timeMillisSinceBoot, x - v.x, y - v.y, z - v.z); 434 } 435 } 436 437 /** 438 * Maintains running statistics on the signal revelant to AnyMotion detection, including: 439 * <ul> 440 * <li>running average. 441 * <li>running sum-of-squared-errors as the energy of the signal derivative. 442 * <ul> 443 */ 444 private static class RunningSignalStats { 445 Vector3 previousVector; 446 Vector3 currentVector; 447 Vector3 runningSum; 448 float energy; 449 int sampleCount; 450 RunningSignalStats()451 public RunningSignalStats() { 452 reset(); 453 } 454 reset()455 public void reset() { 456 previousVector = null; 457 currentVector = null; 458 runningSum = new Vector3(0, 0, 0, 0); 459 energy = 0; 460 sampleCount = 0; 461 } 462 463 /** 464 * Apply a 3D vector v as the next element in the running SSE. 465 */ accumulate(Vector3 v)466 public void accumulate(Vector3 v) { 467 if (v == null) { 468 if (DEBUG) Slog.i(TAG, "Cannot accumulate a null vector."); 469 return; 470 } 471 sampleCount++; 472 runningSum = runningSum.plus(v); 473 previousVector = currentVector; 474 currentVector = v; 475 if (previousVector != null) { 476 Vector3 dv = currentVector.minus(previousVector); 477 float incrementalEnergy = dv.x * dv.x + dv.y * dv.y + dv.z * dv.z; 478 energy += incrementalEnergy; 479 if (DEBUG) Slog.i(TAG, "Accumulated vector " + currentVector.toString() + 480 ", runningSum = " + runningSum.toString() + 481 ", incrementalEnergy = " + incrementalEnergy + 482 ", energy = " + energy); 483 } 484 } 485 getRunningAverage()486 public Vector3 getRunningAverage() { 487 if (sampleCount > 0) { 488 return runningSum.times((float)(1.0f / sampleCount)); 489 } 490 return null; 491 } 492 getEnergy()493 public float getEnergy() { 494 return energy; 495 } 496 getSampleCount()497 public int getSampleCount() { 498 return sampleCount; 499 } 500 501 @Override toString()502 public String toString() { 503 String msg = ""; 504 String currentVectorString = (currentVector == null) ? 505 "null" : currentVector.toString(); 506 String previousVectorString = (previousVector == null) ? 507 "null" : previousVector.toString(); 508 msg += "previousVector = " + previousVectorString; 509 msg += ", currentVector = " + currentVectorString; 510 msg += ", sampleCount = " + sampleCount; 511 msg += ", energy = " + energy; 512 return msg; 513 } 514 } 515 } 516