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