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    /** 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