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