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