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