From 89ecf82f8ecfe328ef5b7ebf140580c8cad868ba Mon Sep 17 00:00:00 2001 From: Kevin Gabayan Date: Mon, 18 May 2015 12:10:07 -0700 Subject: [PATCH] simple AnyMotionDetector Change-Id: I9d49aab83a47c0667a1a059fcb8c7cfddedeb896 --- .../com/android/server/AnyMotionDetector.java | 438 ++++++++++++++++++ .../android/server/DeviceIdleController.java | 115 ++++- 2 files changed, 544 insertions(+), 9 deletions(-) create mode 100644 services/core/java/com/android/server/AnyMotionDetector.java diff --git a/services/core/java/com/android/server/AnyMotionDetector.java b/services/core/java/com/android/server/AnyMotionDetector.java new file mode 100644 index 0000000000000..6390bcd21022d --- /dev/null +++ b/services/core/java/com/android/server/AnyMotionDetector.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Slog; + +import java.lang.Float; + +/** + * Determines if the device has been set upon a stationary object. + */ +public class AnyMotionDetector { + interface DeviceIdleCallback { + public void onAnyMotionResult(int result); + } + + private static final String TAG = "AnyMotionDetector"; + + private static final boolean DEBUG = false; + + /** Stationary status is unknown due to insufficient orientation measurements. */ + public static final int RESULT_UNKNOWN = -1; + + /** Device is stationary, e.g. still on a table. */ + public static final int RESULT_STATIONARY = 0; + + /** Device has been moved. */ + public static final int RESULT_MOVED = 1; + + /** Orientation measurements are being performed or are planned. */ + private static final int STATE_INACTIVE = 0; + + /** No orientation measurements are being performed or are planned. */ + private static final int STATE_ACTIVE = 1; + + /** Current measurement state. */ + private int mState; + + /** Threshold angle in degrees beyond which the device is considered moving. */ + private final float THRESHOLD_ANGLE = 2f; + + /** Threshold energy above which the device is considered moving. */ + private final float THRESHOLD_ENERGY = 5f; + + /** The duration of the accelerometer orientation measurement. */ + private static final long ORIENTATION_MEASUREMENT_DURATION_MILLIS = 2500; + + /** The maximum duration we will collect accelerometer data. */ + private static final long ACCELEROMETER_DATA_TIMEOUT_MILLIS = 3000; + + /** The interval between accelerometer orientation measurements. */ + private static final long ORIENTATION_MEASUREMENT_INTERVAL_MILLIS = 5000; + + /** + * The duration in milliseconds after which an orientation measurement is considered + * too stale to be used. + */ + private static final int STALE_MEASUREMENT_TIMEOUT_MILLIS = 2 * 60 * 1000; + + /** The accelerometer sampling interval. */ + private static final int SAMPLING_INTERVAL_MILLIS = 40; + + private AlarmManager mAlarmManager; + private final Handler mHandler; + private Intent mAlarmIntent; + private final Object mLock = new Object(); + private Sensor mAccelSensor; + private SensorManager mSensorManager; + private PowerManager.WakeLock mWakeLock; + + /** The time when detection was last performed. */ + private long mDetectionStartTime; + + /** The minimum number of samples required to detect AnyMotion. */ + private int mNumSufficientSamples; + + /** True if an orientation measurement is in progress. */ + private boolean mMeasurementInProgress; + + /** The most recent gravity vector. */ + private Vector3 mCurrentGravityVector = null; + + /** The second most recent gravity vector. */ + private Vector3 mPreviousGravityVector = null; + + /** Running sum of squared errors. */ + private RunningSignalStats mRunningStats; + + private DeviceIdleCallback mCallback = null; + + public AnyMotionDetector(AlarmManager am, PowerManager pm, Handler handler, SensorManager sm, + DeviceIdleCallback callback) { + if (DEBUG) Slog.d(TAG, "AnyMotionDetector instantiated."); + mAlarmManager = am; + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mHandler = handler; + mSensorManager = sm; + mAccelSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mMeasurementInProgress = false; + mState = STATE_INACTIVE; + mCallback = callback; + mRunningStats = new RunningSignalStats(); + mNumSufficientSamples = (int) Math.ceil( + ((double)ORIENTATION_MEASUREMENT_DURATION_MILLIS / SAMPLING_INTERVAL_MILLIS)); + if (DEBUG) Slog.d(TAG, "mNumSufficientSamples = " + mNumSufficientSamples); + } + + /* + * Acquire accel data until we determine AnyMotion status. + */ + public void checkForAnyMotion() { + if (DEBUG) Slog.d(TAG, "checkForAnyMotion(). mState = " + mState); + if (mState != STATE_ACTIVE) { + mState = STATE_ACTIVE; + if (DEBUG) Slog.d(TAG, "Moved from STATE_INACTIVE to STATE_ACTIVE."); + mCurrentGravityVector = null; + mPreviousGravityVector = null; + startOrientationMeasurement(); + } + } + + private void startOrientationMeasurement() { + if (DEBUG) Slog.d(TAG, "startOrientationMeasurement: mMeasurementInProgress=" + + mMeasurementInProgress + ", (mAccelSensor != null)=" + (mAccelSensor != null)); + + if (!mMeasurementInProgress && mAccelSensor != null) { + if (mSensorManager.registerListener(mListener, mAccelSensor, + SAMPLING_INTERVAL_MILLIS * 1000)) { + mWakeLock.acquire(); + mMeasurementInProgress = true; + mDetectionStartTime = SystemClock.elapsedRealtime(); + mRunningStats.reset(); + } + + Message msg = Message.obtain(mHandler, mMeasurementTimeout); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, ACCELEROMETER_DATA_TIMEOUT_MILLIS); + } + } + + private int stopOrientationMeasurementLocked() { + if (DEBUG) Slog.d(TAG, "stopOrientationMeasurement. mMeasurementInProgress=" + + mMeasurementInProgress); + int status = RESULT_UNKNOWN; + if (mMeasurementInProgress) { + mSensorManager.unregisterListener(mListener); + mHandler.removeCallbacks(mMeasurementTimeout); + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + long detectionEndTime = SystemClock.elapsedRealtime(); + mMeasurementInProgress = false; + mPreviousGravityVector = mCurrentGravityVector; + mCurrentGravityVector = mRunningStats.getRunningAverage(); + if (DEBUG) { + Slog.d(TAG, "mRunningStats = " + mRunningStats.toString()); + String currentGravityVectorString = (mCurrentGravityVector == null) ? + "null" : mCurrentGravityVector.toString(); + String previousGravityVectorString = (mPreviousGravityVector == null) ? + "null" : mPreviousGravityVector.toString(); + Slog.d(TAG, "mCurrentGravityVector = " + currentGravityVectorString); + Slog.d(TAG, "mPreviousGravityVector = " + previousGravityVectorString); + } + mRunningStats.reset(); + status = getStationaryStatus(); + if (DEBUG) Slog.d(TAG, "getStationaryStatus() returned " + status); + if (status != RESULT_UNKNOWN) { + if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE. status = " + + status); + mState = STATE_INACTIVE; + } else { + /* + * Unknown due to insufficient measurements. Schedule another orientation + * measurement. + */ + if (DEBUG) Slog.d(TAG, "stopOrientationMeasurementLocked(): another measurement" + + " scheduled in " + ORIENTATION_MEASUREMENT_INTERVAL_MILLIS + + " milliseconds."); + Message msg = Message.obtain(mHandler, mSensorRestart); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, ORIENTATION_MEASUREMENT_INTERVAL_MILLIS); + } + } + return status; + } + + /* + * Updates mStatus to the current AnyMotion status. + */ + public int getStationaryStatus() { + if ((mPreviousGravityVector == null) || (mCurrentGravityVector == null)) { + return RESULT_UNKNOWN; + } + Vector3 previousGravityVectorNormalized = mPreviousGravityVector.normalized(); + Vector3 currentGravityVectorNormalized = mCurrentGravityVector.normalized(); + float angle = previousGravityVectorNormalized.angleBetween(currentGravityVectorNormalized); + if (DEBUG) Slog.d(TAG, "getStationaryStatus: angle = " + angle); + if ((angle < THRESHOLD_ANGLE) && (mRunningStats.getEnergy() < THRESHOLD_ENERGY)) { + return RESULT_STATIONARY; + } else if (Float.isNaN(angle)) { + /** + * Floating point rounding errors have caused the angle calcuation's dot product to + * exceed 1.0. In such case, we report RESULT_MOVED to prevent devices from rapidly + * retrying this measurement. + */ + return RESULT_MOVED; + } + long diffTime = mCurrentGravityVector.timeMillisSinceBoot - + mPreviousGravityVector.timeMillisSinceBoot; + if (diffTime > STALE_MEASUREMENT_TIMEOUT_MILLIS) { + if (DEBUG) Slog.d(TAG, "getStationaryStatus: mPreviousGravityVector is too stale at " + + diffTime + " ms ago. Returning RESULT_UNKNOWN."); + return RESULT_UNKNOWN; + } + return RESULT_MOVED; + } + + private final SensorEventListener mListener = new SensorEventListener() { + @Override + public void onSensorChanged(SensorEvent event) { + int status = RESULT_UNKNOWN; + synchronized (mLock) { + Vector3 accelDatum = new Vector3(SystemClock.elapsedRealtime(), event.values[0], + event.values[1], event.values[2]); + mRunningStats.accumulate(accelDatum); + + // If we have enough samples, stop accelerometer data acquisition. + if (mRunningStats.getSampleCount() >= mNumSufficientSamples) { + status = stopOrientationMeasurementLocked(); + } + } + if (status != RESULT_UNKNOWN) { + mCallback.onAnyMotionResult(status); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + }; + + private final Runnable mSensorRestart = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + startOrientationMeasurement(); + } + } + }; + + private final Runnable mMeasurementTimeout = new Runnable() { + @Override + public void run() { + int status = RESULT_UNKNOWN; + synchronized (mLock) { + if (DEBUG) Slog.i(TAG, "mMeasurementTimeout. Failed to collect sufficient accel " + + "data within " + ACCELEROMETER_DATA_TIMEOUT_MILLIS + " ms. Stopping " + + "orientation measurement."); + status = stopOrientationMeasurementLocked(); + } + if (status != RESULT_UNKNOWN) { + mCallback.onAnyMotionResult(status); + } + } + }; + + /** + * A timestamped three dimensional vector and some vector operations. + */ + private static class Vector3 { + public long timeMillisSinceBoot; + public float x; + public float y; + public float z; + + public Vector3(long timeMillisSinceBoot, float x, float y, float z) { + this.timeMillisSinceBoot = timeMillisSinceBoot; + this.x = x; + this.y = y; + this.z = z; + } + + private float norm() { + return (float) Math.sqrt(dotProduct(this)); + } + + private Vector3 normalized() { + float mag = norm(); + return new Vector3(timeMillisSinceBoot, x / mag, y / mag, z / mag); + } + + /** + * Returns the angle between this 3D vector and another given 3D vector. + * Assumes both have already been normalized. + * + * @param other The other Vector3 vector. + * @return angle between this vector and the other given one. + */ + public float angleBetween(Vector3 other) { + double degrees = Math.toDegrees(Math.acos(this.dotProduct(other))); + float returnValue = (float) degrees; + Slog.d(TAG, "angleBetween: this = " + this.toString() + + ", other = " + other.toString()); + Slog.d(TAG, " degrees = " + degrees + ", returnValue = " + returnValue); + return returnValue; + } + + @Override + public String toString() { + String msg = ""; + msg += "timeMillisSinceBoot=" + timeMillisSinceBoot; + msg += " | x=" + x; + msg += ", y=" + y; + msg += ", z=" + z; + return msg; + } + + public float dotProduct(Vector3 v) { + return x * v.x + y * v.y + z * v.z; + } + + public Vector3 times(float val) { + return new Vector3(timeMillisSinceBoot, x * val, y * val, z * val); + } + + public Vector3 plus(Vector3 v) { + return new Vector3(v.timeMillisSinceBoot, x + v.x, y + v.y, z + v.z); + } + + public Vector3 minus(Vector3 v) { + return new Vector3(v.timeMillisSinceBoot, x - v.x, y - v.y, z - v.z); + } + } + + /** + * Maintains running statistics on the signal revelant to AnyMotion detection, including: + *