/*
* 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.talkback;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.Context;
import android.content.SharedPreferences;
import android.hardware.SensorManager;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.view.accessibility.AccessibilityEvent;
import com.android.talkback.controller.FeedbackController;
import com.android.talkback.controller.GestureController;
import com.android.utils.AccessibilityEventListener;
import com.android.utils.SharedPreferencesUtils;
import com.android.utils.picidae.IntegratedTapDetector;
import com.google.android.marvin.talkback.TalkBackService;
/**
* Manages detection of taps on the side of the device. Wraps IntegratedTapDetector.
*/
public class SideTapManager extends BroadcastReceiver
implements IntegratedTapDetector.TapListener, FeedbackController.HapticFeedbackListener,
AccessibilityEventListener {
/* Ignore taps for this long after the screen is touched */
private static final long MIN_TIME_BETWEEN_TOUCH_AND_TAP_NANOS = 500 * 1000 * 1000;
/* Ignore taps for this long after the screen is touched */
private static final long MIN_TIME_BETWEEN_HAPTIC_AND_TAP_NANOS = 500 * 1000 * 1000;
/* Time interval for double taps */
private static final long DOUBLE_TAP_SPACING_NANOS = 500 * 1000 * 1000;
private static final long MILIS_PER_NANO = 1000 * 1000;
private Context mContext;
/* Time of last touch of the screen */
private long mLastTouchTime = 0;
/* Time of last haptic feedback */
private long mLastHapticTime = 0;
/* Class that deals with the hardware and calls us back with taps */
private IntegratedTapDetector mIntegratedTapDetector;
private final GestureController mGestureController;
/**
* @param context TalkBackService whose {@code performCustomGesture}
* will be called when taps are detected
*/
public SideTapManager(TalkBackService context, GestureController gestureController) {
if (gestureController == null) throw new IllegalStateException();
mContext = context;
mIntegratedTapDetector = new IntegratedTapDetector(
(SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE));
mIntegratedTapDetector.addListener(this);
mIntegratedTapDetector
.setPostDelayTimeMillis(MIN_TIME_BETWEEN_TOUCH_AND_TAP_NANOS / MILIS_PER_NANO);
mGestureController = gestureController;
}
/**
* Stops tap detection.
*/
public void onSuspendInfrastructure() {
mIntegratedTapDetector.stop();
}
/**
* Enables tap detection if appropriate based on preferences.
*/
public void onReloadPreferences() {
SharedPreferences settings = SharedPreferencesUtils.getSharedPreferences(mContext);
boolean enableTapDetection = false;
if (!settings.getString(mContext.getString(R.string.pref_shortcut_single_tap_key),
mContext.getString(R.string.pref_shortcut_single_tap_default)).
equals(mContext.getString(R.string.shortcut_value_unassigned))) {
/* Single tap is assigned */
enableTapDetection = true;
}
if (!settings.getString(mContext.getString(R.string.pref_shortcut_double_tap_key),
mContext.getString(R.string.pref_shortcut_double_tap_default)).
equals(mContext.getString(R.string.shortcut_value_unassigned))) {
/* Double tap is assigned */
enableTapDetection = true;
mIntegratedTapDetector.setMaxDoubleTapSpacingNanos(DOUBLE_TAP_SPACING_NANOS);
} else {
mIntegratedTapDetector.setMaxDoubleTapSpacingNanos(0);
}
/* The setting is 'sensitivity', which is the opposite of the detector's 'quality' */
if (settings.getString(mContext.getString(R.string.pref_tap_sensitivity_key),
mContext.getString(R.string.pref_tap_sensitivity_default)).
equals(mContext.getString(R.string.tap_sensitivity_value_lowest))) {
mIntegratedTapDetector.setTapDetectionQuality(
IntegratedTapDetector.TAP_QUALITY_HIGHEST);
}
if (settings.getString(mContext.getString(R.string.pref_tap_sensitivity_key),
mContext.getString(R.string.pref_tap_sensitivity_default)).
equals(mContext.getString(R.string.tap_sensitivity_value_low))) {
mIntegratedTapDetector.setTapDetectionQuality(IntegratedTapDetector.TAP_QUALITY_HIGH);
}
if (settings.getString(mContext.getString(R.string.pref_tap_sensitivity_key),
mContext.getString(R.string.pref_tap_sensitivity_default)).
equals(mContext.getString(R.string.tap_sensitivity_value_medium))) {
mIntegratedTapDetector.setTapDetectionQuality(
IntegratedTapDetector.TAP_QUALITY_MEDIUM);
}
if (settings.getString(mContext.getString(R.string.pref_tap_sensitivity_key),
mContext.getString(R.string.pref_tap_sensitivity_default)).
equals(mContext.getString(R.string.tap_sensitivity_value_high))) {
mIntegratedTapDetector.setTapDetectionQuality(IntegratedTapDetector.TAP_QUALITY_LOW);
}
/* Second tap of double taps can be low quality */
mIntegratedTapDetector.setDoubleTapDetectionQuality(
IntegratedTapDetector.TAP_QUALITY_LOW);
if (enableTapDetection) {
mIntegratedTapDetector.start();
} else {
mIntegratedTapDetector.stop();
}
}
/**
* Called so we can avoid detecting screen touches as side taps.
*/
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEventCompat.TYPE_TOUCH_INTERACTION_START) {
mLastTouchTime = System.nanoTime();
}
}
/* Handle a single tap on the side of the device */
@Override
public void onSingleTap(long timeStamp) {
boolean talkBackActive = TalkBackService.isServiceActive();
boolean tapIsntFromScreenTouch =
(Math.abs(timeStamp - mLastTouchTime) > MIN_TIME_BETWEEN_TOUCH_AND_TAP_NANOS);
boolean tapIsntFromHaptic =
(Math.abs(timeStamp - mLastHapticTime) > MIN_TIME_BETWEEN_HAPTIC_AND_TAP_NANOS);
if (talkBackActive && tapIsntFromScreenTouch && tapIsntFromHaptic) {
SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(mContext);
mGestureController.performAction(prefs.getString(
mContext.getString(R.string.pref_shortcut_single_tap_key),
mContext.getString(R.string.pref_shortcut_single_tap_default)));
}
}
/* Handle a double tap on the side of the device */
@Override
public void onDoubleTap(long timeStamp) {
boolean talkBackActive = TalkBackService.isServiceActive();
boolean tapIsntFromScreenTouch =
(Math.abs(timeStamp - mLastTouchTime) > MIN_TIME_BETWEEN_TOUCH_AND_TAP_NANOS);
boolean tapIsntFromHaptic =
(Math.abs(timeStamp - mLastHapticTime) > MIN_TIME_BETWEEN_HAPTIC_AND_TAP_NANOS);
if (talkBackActive && tapIsntFromScreenTouch && tapIsntFromHaptic) {
SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(mContext);
mGestureController.performAction(prefs.getString(
mContext.getString(R.string.pref_shortcut_double_tap_key),
mContext.getString(R.string.pref_shortcut_double_tap_default)));
}
}
/*
* Haptic feedback can be interpreted as a tap, similarly to a screen tap.
*/
@Override
public void onHapticFeedbackStarting(long currentNanoTime) {
mLastHapticTime = currentNanoTime;
}
@Override
public void onReceive(Context context, Intent intent) {
if (!TalkBackService.isServiceActive()) {
return;
}
String action = intent.getAction();
if (action.equals(Intent.ACTION_SCREEN_ON)) {
onReloadPreferences();
}
if (action.equals(Intent.ACTION_SCREEN_OFF)) {
mIntegratedTapDetector.stop();
}
}
public static IntentFilter getFilter() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
return filter;
}
/**
* Permit tests to replace the tap detector to verify calls to it
* @param itd new IntegratedTapDetector
*/
// Visible for testing
/* package */ void setIntegratedTapDetector(IntegratedTapDetector itd) {
mIntegratedTapDetector = itd;
}
}