/* * Copyright (C) 2015 Google Inc. * * 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.utils.picidae; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Queue; /** * Class to gather data from the accelerometer and gyroscope and detect tapping on the side of the * device. Taps (and double-taps, if requested) are reported to registered listeners. * <p> * Raw data can be saved to a LittleEndianDataOutputStream for development. * <p> * This detector integrates output from individual detectors that each detect taps based on a * single sensor. Options exist to consider only one sensor at a time and to qualify taps based on * a tap quality, which can range from 0.0 to 1.0. * <p> * Functions adjust the minimum spacing between two individual taps, as well as the maximum spacing * between the taps of a double tap. These two parameters are orthogonal, as a double tap is * treated as a individual event. * <p> */ public class IntegratedTapDetector implements SensorEventListener, ThreeDSensorTapDetector.TapListener { private static final boolean DEBUG = false; /* * Quantitative tap quality metrics used to match qualitative. * TODO refactor to have individual detectors report their own confidence along some * absolute scale so we don't need to define the public quality guidance values based on * private constants. */ private static final double TAP_QUALITY_POSSIBLE = 0.15; private static final double TAP_QUALITY_DEFINITE = 0.5; private static final float DEFAULT_MAX_ACCEL_SCALE = 30f; private static final float DEFAULT_MAX_GYRO_SCALE = 8f; /** * Guidance value for setTapDetectionQuality. Try not to lose any taps and accept that there * will be quite a few false positives. This value is the default for double tap quality, as * the first tap qualifies the second one. */ public static final double TAP_QUALITY_LOW = TAP_QUALITY_POSSIBLE; /** * Guidance value for setTapDetectionQuality. Balance between false positives and false * negatives. */ public static final double TAP_QUALITY_MEDIUM = 2 * TAP_QUALITY_POSSIBLE; /** * Guidance value for setTapDetectionQuality. Work hard to avoid false positives. * This is the default for tap detection. */ public static final double TAP_QUALITY_HIGH = TAP_QUALITY_DEFINITE; /** * Guidance value for setTapDetectionQuality. Very few false positives, but tends to miss taps * that aren't sampled very well by the accelerometer and gyroscope. */ public static final double TAP_QUALITY_HIGHEST = TAP_QUALITY_DEFINITE + TAP_QUALITY_POSSIBLE; /* Suggested value for min tap spacing */ private static final long DEFAULT_MIN_TAP_SPACING = 100 * 1000 * 1000; /* * The various tap detectors used may report the same tap at different times. We combine these * taps together independent of the value of mMinTapSpacing. */ private static final long MAX_OFFSET_BETWEEN_TAP_DETECTORS = 100 * 1000 * 1000; private static final long MILIS_PER_NANO = 1000 * 1000; /* * Maximum latency of a low-level tap detector * TODO: Each low-level detector should expose its latency, and we * could query it rather than relying on a constant. */ private static final long MAX_TAP_DETECTOR_LATENCY = 100 * 1000 * 1000; /* Handler containing the internal processing thread */ private final Handler mHandler; /* Framework sensor manager that gives us access to the accelerometer and gyroscope */ private final SensorManager mSensorManager; /* Queues to store taps from accelerometer. */ private final Queue<Tap> mAccelTapEventQueue = new LinkedList<>(); /* Queues to store taps from gyroscope. */ private final Queue<Tap> mGyroTapEventQueue = new LinkedList<>(); /* Queues to store taps detected by integrating the lower-level detectors. */ private final Queue<Tap> mIntegratedTapEventQueue = new LinkedList<>(); /* The accelerometer-based tap detector being integrated */ private final ThreeDSensorTapDetector mAccelTapDetector; /* The gyroscope-based tap detector being integrated */ private final ThreeDSensorTapDetector mGyroTapDetector; /* Minimum quality value used for a single tap and/or the first half of a double tap */ private double mMinTapQuality; /* Minimum quality value used for the second half of a double tap */ private double mMinDoubleTapQuality; /* Map to keep track of listeners and which handlers to call them through */ private final Map<TapListener, Handler> mListenerMap = new HashMap<>(); /* Current tap detection strategy */ private TapDetector mCurrentTapDetector = TapDetector.INTEGRATED_TAP_DETECTOR; /* The last time we reported a tap */ private long mLastReportedTapTime; /* Flag that we have reported at least one tap, making mLastReportedTapTime valid */ private boolean mHaveReportedAtLeastOneTap = false; /* Taps that happen too close together are ignored because they are likely noise */ private long mMinTapSpacingNanos = DEFAULT_MIN_TAP_SPACING; /* Taps close enough together are double taps. A value of 0 disables double-tap detection */ private long mMaxDoubleTapSpacingNanos = 0; /* Amount to delay posting of messages, which is helpful for certain services */ private long mPostDelayTime = 0; /** * @param sensorManager system's SensorManager. Obtain with * getSystemService(Context.SENSOR_SERVICE); */ public IntegratedTapDetector(SensorManager sensorManager) { this(sensorManager, null, null); } // Visible for testing public IntegratedTapDetector(SensorManager sensorManager, ThreeDSensorTapDetector accelTapDetector, ThreeDSensorTapDetector gyroTapDetector) { mSensorManager = sensorManager; // TODO: Determine the correct priority of this thread HandlerThread thread = new HandlerThread("AccelGyroAudioTapDetector", -20); thread.start(); mHandler = new Handler(thread.getLooper()); mHaveReportedAtLeastOneTap = false; mMinTapQuality = TAP_QUALITY_HIGH; mMinDoubleTapQuality = TAP_QUALITY_LOW; if (accelTapDetector == null) { Sensor accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); float maxRange = DEFAULT_MAX_ACCEL_SCALE; if (accelerometer != null) { maxRange = accelerometer.getMaximumRange(); } accelTapDetector = new ThreeDSensorTapDetector( this, maxRange, ThreeDSensorTapDetectorType.ACCELEROMETER); } if (gyroTapDetector == null) { Sensor gyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); float maxRange = DEFAULT_MAX_GYRO_SCALE; if (gyroscope != null) { maxRange = gyroscope.getMaximumRange(); } gyroTapDetector = new ThreeDSensorTapDetector( this, maxRange, ThreeDSensorTapDetectorType.GYROSCOPE); } mAccelTapDetector = accelTapDetector; mGyroTapDetector = gyroTapDetector; } /** * Choose desired tap detection method */ public void useTapDetector(TapDetector tapDetector) { mCurrentTapDetector = tapDetector; } /** * Choose the minimum tap spacing in nanos * * @param tapSpacing Maximum time (in nanoseconds) by which taps may be separated */ public void setMinimumTapSpacingNanos(long tapSpacing) { mMinTapSpacingNanos = tapSpacing; } /** * Choose the maximum spacing in nanos for double-taps, which are reported as separate events * from taps. Default is 0, which disables double-tap detection. * * @param maxDTapSpacing taps closer together than this will be combined into a single double * tap. 0 disabled double-tap detection. */ public void setMaxDoubleTapSpacingNanos(long maxDTapSpacing) { mMaxDoubleTapSpacingNanos = maxDTapSpacing; } /** * Set desired tap quality * * @param tq value between 0 and 1. Larger numbers are higher quality taps. */ public void setTapDetectionQuality(double tq) { mMinTapQuality = tq; } /** * Set desired double-tap quality. Double-taps must have at least one tap that is of standard * tap quality, and one over this quality. * * @param tq value between 0 and 1. Larger numbers are higher quality taps. */ public void setDoubleTapDetectionQuality(double tq) { mMinDoubleTapQuality = tq; } /** * For some applications, it's convenient if taps are reported with a slight delay. Processing * gives some delay, so this value is a loose lower bound. * * @param millisToDelayPosts - target time to delay posting of tap callback */ public void setPostDelayTimeMillis(long millisToDelayPosts) { mPostDelayTime = millisToDelayPosts; } /** * Start listening to sensors for taps. */ public void start() { Sensor accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mSensorManager.registerListener( this, accelerometer, SensorManager.SENSOR_DELAY_FASTEST, mHandler); Sensor gyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); mSensorManager .registerListener(this, gyroscope, SensorManager.SENSOR_DELAY_FASTEST, mHandler); } /** * Stop listening to sensors. The detector should be disabled when it's not needed (as when the * activity paying attention to it doesn't have focus or the screen is off) since it takes * considerable CPU resources. */ public void stop() { mSensorManager.unregisterListener(this); } /** * Process an accelerometer event. */ void onAccelerometerChanged(long timestamp, float[] values) { switch(mCurrentTapDetector) { case ACCEL_ONLY_DETECTOR: case INTEGRATED_TAP_DETECTOR: mAccelTapDetector.onSensorChanged(timestamp, values); break; default: } emitTapsFromQueues(timestamp); } /** * Process a gyro event. */ void onGyroscopeChanged(long timestamp, float values[]) { switch(mCurrentTapDetector) { case GYRO_ONLY_DETECTOR: case INTEGRATED_TAP_DETECTOR: mGyroTapDetector.onSensorChanged(timestamp, values); break; default: } emitTapsFromQueues(timestamp); } /** * Process arrival of more audio data */ public void onNewAudioData(long timestamp, byte audioData[]) { } /** * Add a listener to be called back on the thread specified by Handler. */ void addListener(TapListener listener, Handler handler) { synchronized (mListenerMap) { mListenerMap.put(listener, handler); } } /** * Add a listener on the current thread. */ public void addListener(TapListener listener) { addListener(listener, new Handler()); } /** * Remove a listener on the current thread. */ public void removeListener(TapListener listener) { synchronized (mListenerMap) { mListenerMap.remove(listener); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // Nothing to do } @Override public void onSensorChanged(SensorEvent event) { switch (event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: onAccelerometerChanged(System.nanoTime(), event.values); break; case Sensor.TYPE_GYROSCOPE: onGyroscopeChanged(System.nanoTime(), event.values); break; default: } } @Override public void threeDSensorTapDetected( ThreeDSensorTapDetector threeDSensorTapDetector, long timestamp, double tapConfidence) { if (threeDSensorTapDetector == mAccelTapDetector) { mAccelTapEventQueue.add(new Tap(tapConfidence, timestamp)); } if (threeDSensorTapDetector == mGyroTapDetector) { mGyroTapEventQueue.add(new Tap(tapConfidence, timestamp)); } emitTapsFromQueues(timestamp); } // visible for testing public void setLastTapNanoTime(long newTime) { mLastReportedTapTime = newTime; } /* * Emit or discard all pending taps from the integrated queue as single */ // visible for testing public void flushPendingTaps() { emitTapsFromQueues(Long.MAX_VALUE); } /* * Send a double tap to all listeners */ // visible for testing void sendDoubleTapToListeners(long timestampNanos) { final long finalTimestamp = timestampNanos; mLastReportedTapTime = timestampNanos; mHaveReportedAtLeastOneTap = true; // Pass double-tap along to all listeners synchronized (mListenerMap) { for (TapListener listener : mListenerMap.keySet()) { final TapListener finalListener = listener; long delay = mPostDelayTime - (System.nanoTime() - timestampNanos); delay = (delay > 0) ? delay : 0; mListenerMap.get(listener).postDelayed(new Runnable() { @Override public void run() { finalListener.onDoubleTap(finalTimestamp); } }, delay); } } } /* * Send a single tap to all listeners */ // visible for testing void sendSingleTapToListeners(long timestampNanos) { final long finalTimestamp = timestampNanos; mLastReportedTapTime = timestampNanos; mHaveReportedAtLeastOneTap = true; // Pass tap along to all listeners synchronized (mListenerMap) { for (TapListener listener : mListenerMap.keySet()) { final TapListener finalListener = listener; long delay = mPostDelayTime - (System.nanoTime() - timestampNanos) / MILIS_PER_NANO; delay = (delay > 0) ? delay : 0; mListenerMap.get(listener).postDelayed(new Runnable() { @Override public void run() { finalListener.onSingleTap(finalTimestamp); } }, delay); } } } /* * Look at the queues for the individual tap detectors and see if there are taps that are * ready to emit. * * @param timestamp is the current timestamp. */ private void emitTapsFromQueues(long timestamp) { integrateAccelAndGyroQueues(timestamp); processIntegratedQueueAsSingleAndDoubleTaps(timestamp); } /* * Combine taps from mAccelTapEventQueue and mGyroTapEventQueue into a single * mIntegratedTapEventQueue. * * @param timestamp is the current timestamp. */ private void integrateAccelAndGyroQueues(long timestamp) { /* * When neither queue is empty, we have all of the information needed to process the most * recent tap. */ while ((mGyroTapEventQueue.size() > 0) && (mAccelTapEventQueue.size() > 0)) { if (mGyroTapEventQueue.peek().nanos > mAccelTapEventQueue.peek().nanos + MAX_OFFSET_BETWEEN_TAP_DETECTORS) { mIntegratedTapEventQueue.add(mAccelTapEventQueue.remove()); continue; } if (mAccelTapEventQueue.peek().nanos > mGyroTapEventQueue.peek().nanos + MAX_OFFSET_BETWEEN_TAP_DETECTORS) { mIntegratedTapEventQueue.add(mGyroTapEventQueue.remove()); continue; } /* * The two times are close enough that we can combine these taps. We set the quality * of this tap equal to the sum of that from the two detectors. */ Tap accelTap = mAccelTapEventQueue.remove(); Tap gyroTap = mGyroTapEventQueue.remove(); Tap integratedTap = new Tap( accelTap.quality + gyroTap.quality, Math.min(accelTap.nanos, gyroTap.nanos)); mIntegratedTapEventQueue.add(integratedTap); } /* * Emit any taps we can from the non-empty queue. Note that if both queues are empty, the * while() loop never executes. */ Queue<Tap> nonEmptyTapQueue = (mGyroTapEventQueue.size() > 0) ? mGyroTapEventQueue : mAccelTapEventQueue; while (nonEmptyTapQueue.size() > 0) { long latestTimeThatCantBeADoubleTap = timestamp - MAX_OFFSET_BETWEEN_TAP_DETECTORS - MAX_TAP_DETECTOR_LATENCY; if (nonEmptyTapQueue.peek().nanos > latestTimeThatCantBeADoubleTap) { break; } if (DEBUG) { Log.v("Picidae", String.format( "Adding tap at time %d from nonempty to integrated queue at time %d", nonEmptyTapQueue.peek().nanos, timestamp)); } mIntegratedTapEventQueue.add(nonEmptyTapQueue.remove()); } } /* * Work through the integrated queue as far as possible, reporting single and double taps. One * tap may be left in the queue if insufficient time has elapsed to be sure it isn't a double * tap. */ private void processIntegratedQueueAsSingleAndDoubleTaps(long timestamp) { while (mIntegratedTapEventQueue.size() >= 2) { Tap olderTap = mIntegratedTapEventQueue.remove(); if (!tapAllowedAt(olderTap.nanos)) { if (DEBUG) { Log.v("Picidae", String.format("Disallowing tap at time %d with quality %f", olderTap.nanos, olderTap.quality)); } continue; } Tap newerTap = mIntegratedTapEventQueue.peek(); if (newerTap.nanos < olderTap.nanos + mMaxDoubleTapSpacingNanos) { /* * Taps are close enough together. Must have one tap above min single-tap quality, * and the other above min double-tap quality */ boolean qualityGoodEnough1 = (newerTap.quality >= mMinTapQuality) && (olderTap.quality >= mMinDoubleTapQuality); boolean qualityGoodEnough2 = (newerTap.quality >= mMinDoubleTapQuality) && (olderTap.quality >= mMinTapQuality); if (qualityGoodEnough1 || qualityGoodEnough2) { sendDoubleTapToListeners(olderTap.nanos); mIntegratedTapEventQueue.remove(); continue; } } /* Not a double tap. Check single-tap quality */ if (olderTap.quality >= mMinTapQuality) { sendSingleTapToListeners(olderTap.nanos); } else if (DEBUG) { Log.v("Picidae", String.format("Discarding tap at time %d with quality %f", olderTap.nanos, olderTap.quality)); } } if (mIntegratedTapEventQueue.size() == 0) { return; } /* Make sure there's no way a second tap is coming before emitting single tap */ long maxOffsetToConsider = (mMaxDoubleTapSpacingNanos > 0) ? MAX_OFFSET_BETWEEN_TAP_DETECTORS + MAX_TAP_DETECTOR_LATENCY : 0; if (mIntegratedTapEventQueue.peek().nanos <= timestamp - mMaxDoubleTapSpacingNanos - maxOffsetToConsider) { Tap tap = mIntegratedTapEventQueue.remove(); if ((tap.quality >= mMinTapQuality) && tapAllowedAt(tap.nanos)) { sendSingleTapToListeners(tap.nanos); } else if (DEBUG) { Log.v("Picidae", String.format("Discarding tap at time %d with quality %f", tap.nanos, tap.quality)); } } } /* Check that a tap hasn't been sent out too recently */ private boolean tapAllowedAt(long timestamp) { return ((timestamp - mLastReportedTapTime > mMinTapSpacingNanos) || !mHaveReportedAtLeastOneTap); } /** * Listener which is notified whenever a tap is detected. */ public interface TapListener { // Invoked when tap occurs void onSingleTap(long timestampNanos); // Invoked on double-tap void onDoubleTap(long timestampNanos); } /** * List of different sensor tap detectors available for use */ public enum TapDetector { /** Detector that integrates accelerometer and gyroscope tap detectors */ INTEGRATED_TAP_DETECTOR, /** Detector that uses only accelerometer */ ACCEL_ONLY_DETECTOR, /** Detector that uses only gyroscope */ GYRO_ONLY_DETECTOR } /** * List of different sensor types that this detector integrates */ public enum SensorTag { INVALID, ACCELEROMETER, GYROSCOPE, AUDIO } /** * List of different tap detection quality required to report a tap. Higher * qualities have lower false positive rates but are more likely to miss taps. * Some detectors - OTP, for example, are not affected by this value. */ public enum TapDetectionQuality { HIGHEST, HIGH, MEDIUM, LOW } /* Simple pair-like class to hold taps. Not using pair for clarity. */ private static class Tap { public final double quality; public final long nanos; public Tap(double qualityInit, long nanosInit) { quality = qualityInit; nanos = nanosInit; } } }