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