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