content: /*
   * 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.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;
  
  /**
   * 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 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 maximum duration we will hold a wakelock to determine stationary status. */
      private static final long WAKELOCK_TIMEOUT_MILLIS = 30000;
  
      /**
       * 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 final Handler mHandler;
      private final Object mLock = new Object();
      private Sensor mAccelSensor;
      private SensorManager mSensorManager;
      private PowerManager.WakeLock mWakeLock;
  
      /** Threshold angle in degrees beyond which the device is considered moving. */
      private final float mThresholdAngle;
  
      /** The minimum number of samples required to detect AnyMotion. */
      private int mNumSufficientSamples;
  
      /** True if an orientation measurement is in progress. */
      private boolean mMeasurementInProgress;
  
      /** True if sendMessageDelayed() for the mMeasurementTimeout callback has been scheduled */
      private boolean mMeasurementTimeoutIsActive;
  
      /** True if sendMessageDelayed() for the mWakelockTimeout callback has been scheduled */
      private boolean mWakelockTimeoutIsActive;
  
      /** True if sendMessageDelayed() for the mSensorRestart callback has been scheduled */
      private boolean mSensorRestartIsActive;
  
      /** 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(PowerManager pm, Handler handler, SensorManager sm,
              DeviceIdleCallback callback, float thresholdAngle) {
          if (DEBUG) Slog.d(TAG, "AnyMotionDetector instantiated.");
          synchronized (mLock) {
              mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
              mWakeLock.setReferenceCounted(false);
              mHandler = handler;
              mSensorManager = sm;
              mAccelSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
              mMeasurementInProgress = false;
              mMeasurementTimeoutIsActive = false;
              mWakelockTimeoutIsActive = false;
              mSensorRestartIsActive = false;
              mState = STATE_INACTIVE;
              mCallback = callback;
              mThresholdAngle = thresholdAngle;
              mRunningStats = new RunningSignalStats();
              mNumSufficientSamples = (int) Math.ceil(
                      ((double)ORIENTATION_MEASUREMENT_DURATION_MILLIS / SAMPLING_INTERVAL_MILLIS));
              if (DEBUG) Slog.d(TAG, "mNumSufficientSamples = " + mNumSufficientSamples);
          }
      }
  
      /**
       * If we do not have an accelerometer, we are not going to collect much data.
       */
      public boolean hasSensor() {
          return mAccelSensor != null;
      }
  
      /*
       * Acquire accel data until we determine AnyMotion status.
       */
      public void checkForAnyMotion() {
          if (DEBUG) {
              Slog.d(TAG, "checkForAnyMotion(). mState = " + mState);
          }
          if (mState != STATE_ACTIVE) {
              synchronized (mLock) {
                  mState = STATE_ACTIVE;
                  if (DEBUG) {
                      Slog.d(TAG, "Moved from STATE_INACTIVE to STATE_ACTIVE.");
                  }
                  mCurrentGravityVector = null;
                  mPreviousGravityVector = null;
                  mWakeLock.acquire();
                  Message wakelockTimeoutMsg = Message.obtain(mHandler, mWakelockTimeout);
                  mHandler.sendMessageDelayed(wakelockTimeoutMsg, WAKELOCK_TIMEOUT_MILLIS);
                  mWakelockTimeoutIsActive = true;
                  startOrientationMeasurementLocked();
              }
          }
      }
  
      public void stop() {
          synchronized (mLock) {
              if (mState == STATE_ACTIVE) {
                  mState = STATE_INACTIVE;
                  if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE.");
              }
              mHandler.removeCallbacks(mMeasurementTimeout);
              mHandler.removeCallbacks(mSensorRestart);
              mMeasurementTimeoutIsActive = false;
              mSensorRestartIsActive = false;
              if (mMeasurementInProgress) {
                  mMeasurementInProgress = false;
                  mSensorManager.unregisterListener(mListener);
              }
              mCurrentGravityVector = null;
              mPreviousGravityVector = null;
              if (mWakeLock.isHeld()) {
                  mHandler.removeCallbacks(mWakelockTimeout);
                  mWakelockTimeoutIsActive = false;
                  mWakeLock.release();
              }
          }
      }
  
      private void startOrientationMeasurementLocked() {
          if (DEBUG) Slog.d(TAG, "startOrientationMeasurementLocked: mMeasurementInProgress=" +
              mMeasurementInProgress + ", (mAccelSensor != null)=" + (mAccelSensor != null));
          if (!mMeasurementInProgress && mAccelSensor != null) {
              if (mSensorManager.registerListener(mListener, mAccelSensor,
                      SAMPLING_INTERVAL_MILLIS * 1000)) {
                  mMeasurementInProgress = true;
                  mRunningStats.reset();
              }
              Message measurementTimeoutMsg = Message.obtain(mHandler, mMeasurementTimeout);
              mHandler.sendMessageDelayed(measurementTimeoutMsg, ACCELEROMETER_DATA_TIMEOUT_MILLIS);
              mMeasurementTimeoutIsActive = true;
          }
      }
  
      private int stopOrientationMeasurementLocked() {
          if (DEBUG) Slog.d(TAG, "stopOrientationMeasurement. mMeasurementInProgress=" +
                  mMeasurementInProgress);
          int status = RESULT_UNKNOWN;
          if (mMeasurementInProgress) {
              mHandler.removeCallbacks(mMeasurementTimeout);
              mMeasurementTimeoutIsActive = false;
              mSensorManager.unregisterListener(mListener);
              mMeasurementInProgress = false;
              mPreviousGravityVector = mCurrentGravityVector;
              mCurrentGravityVector = mRunningStats.getRunningAverage();
              if (mRunningStats.getSampleCount() == 0) {
                  Slog.w(TAG, "No accelerometer data acquired for orientation measurement.");
              }
              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 (mWakeLock.isHeld()) {
                      mHandler.removeCallbacks(mWakelockTimeout);
                      mWakelockTimeoutIsActive = false;
                      mWakeLock.release();
                  }
                  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);
                  mHandler.sendMessageDelayed(msg, ORIENTATION_MEASUREMENT_INTERVAL_MILLIS);
                  mSensorRestartIsActive = true;
              }
          }
          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
                  + " energy = " + mRunningStats.getEnergy());
          if ((angle < mThresholdAngle) && (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) {
                  mHandler.removeCallbacks(mWakelockTimeout);
                  mWakelockTimeoutIsActive = false;
                  mCallback.onAnyMotionResult(status);
              }
          }
  
          @Override
          public void onAccuracyChanged(Sensor sensor, int accuracy) {
          }
      };
  
      private final Runnable mSensorRestart = new Runnable() {
          @Override
          public void run() {
              synchronized (mLock) {
                  if (mSensorRestartIsActive == true) {
                      mSensorRestartIsActive = false;
                      startOrientationMeasurementLocked();
                  }
              }
          }
      };
  
      private final Runnable mMeasurementTimeout = new Runnable() {
          @Override
          public void run() {
              int status = RESULT_UNKNOWN;
              synchronized (mLock) {
                  if (mMeasurementTimeoutIsActive == true) {
                      mMeasurementTimeoutIsActive = false;
                      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) {
                          mHandler.removeCallbacks(mWakelockTimeout);
                          mWakelockTimeoutIsActive = false;
                          mCallback.onAnyMotionResult(status);
                      }
                  }
              }
          }
      };
  
      private final Runnable mWakelockTimeout = new Runnable() {
          @Override
          public void run() {
              synchronized (mLock) {
                  if (mWakelockTimeoutIsActive == true) {
                      mWakelockTimeoutIsActive = false;
                      stop();
                  }
              }
          }
      };
  
      /**
       * A timestamped three dimensional vector and some vector operations.
       */
      public static final 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;
          }
  
          public float norm() {
              return (float) Math.sqrt(dotProduct(this));
          }
  
          public 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) {
              Vector3 crossVector = cross(other);
              float degrees = Math.abs((float)Math.toDegrees(
                      Math.atan2(crossVector.norm(), dotProduct(other))));
              Slog.d(TAG, "angleBetween: this = " + this.toString() +
                  ", other = " + other.toString() + ", degrees = " + degrees);
              return degrees;
          }
  
          public Vector3 cross(Vector3 v) {
              return new Vector3(
                  v.timeMillisSinceBoot,
                  y * v.z - z * v.y,
                  z * v.x - x * v.z,
                  x * v.y - y * v.x);
          }
  
          @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:
       * <ul>
       *   <li>running average.
       *   <li>running sum-of-squared-errors as the energy of the signal derivative.
       * <ul>
       */
      private static class RunningSignalStats {
          Vector3 previousVector;
          Vector3 currentVector;
          Vector3 runningSum;
          float energy;
          int sampleCount;
  
          public RunningSignalStats() {
              reset();
          }
  
          public void reset() {
              previousVector = null;
              currentVector = null;
              runningSum = new Vector3(0, 0, 0, 0);
              energy = 0;
              sampleCount = 0;
          }
  
          /**
           * Apply a 3D vector v as the next element in the running SSE.
           */
          public void accumulate(Vector3 v) {
              if (v == null) {
                  if (DEBUG) Slog.i(TAG, "Cannot accumulate a null vector.");
                  return;
              }
              sampleCount++;
              runningSum = runningSum.plus(v);
              previousVector = currentVector;
              currentVector = v;
              if (previousVector != null) {
                  Vector3 dv = currentVector.minus(previousVector);
                  float incrementalEnergy = dv.x * dv.x + dv.y * dv.y + dv.z * dv.z;
                  energy += incrementalEnergy;
                  if (DEBUG) Slog.i(TAG, "Accumulated vector " + currentVector.toString() +
                          ", runningSum = " + runningSum.toString() +
                          ", incrementalEnergy = " + incrementalEnergy +
                          ", energy = " + energy);
              }
          }
  
          public Vector3 getRunningAverage() {
              if (sampleCount > 0) {
                return runningSum.times((float)(1.0f / sampleCount));
              }
              return null;
          }
  
          public float getEnergy() {
              return energy;
          }
  
          public int getSampleCount() {
              return sampleCount;
          }
  
          @Override
          public String toString() {
              String msg = "";
              String currentVectorString = (currentVector == null) ?
                  "null" : currentVector.toString();
              String previousVectorString = (previousVector == null) ?
                  "null" : previousVector.toString();
              msg += "previousVector = " + previousVectorString;
              msg += ", currentVector = " + currentVectorString;
              msg += ", sampleCount = " + sampleCount;
              msg += ", energy = " + energy;
              return msg;
          }
      }
  }
  