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