/*
* Copyright (C) 2013 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.talkback.eventprocessor;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.support.annotation.NonNull;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import com.android.talkback.CursorGranularity;
import com.android.talkback.R;
import com.android.talkback.SpeechController;
import com.android.utils.Role;
import com.android.utils.WeakReferenceHandler;
import com.google.android.marvin.talkback.TalkBackService;
import com.android.talkback.controller.CursorController;
import com.android.talkback.controller.DimScreenController;
import com.android.talkback.controller.FeedbackController;
import com.android.talkback.volumebutton.VolumeButtonPatternDetector;
import com.android.utils.AccessibilityEventListener;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.PerformActionUtils;
import com.android.utils.SharedPreferencesUtils;
import java.util.List;
/**
* Locks the volume control stream during a touch interaction event.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class ProcessorVolumeStream implements AccessibilityEventListener,
TalkBackService.KeyEventListener, VolumeButtonPatternDetector.OnPatternMatchListener {
/** Minimum API version required for this class to function. */
public static final int MIN_API_LEVEL = Build.VERSION_CODES.JELLY_BEAN_MR2;
private static final boolean API_LEVEL_SUPPORTS_WINDOW_NAVIGATION =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
/** Default flags for volume adjustment while touching the screen. */
private static final int DEFAULT_FLAGS_TOUCHING_SCREEN = (AudioManager.FLAG_SHOW_UI
| AudioManager.FLAG_VIBRATE);
/** Default flags for volume adjustment while not touching the screen. */
private static final int DEFAULT_FLAGS_NOT_TOUCHING_SCREEN = (AudioManager.FLAG_SHOW_UI
| AudioManager.FLAG_VIBRATE | AudioManager.FLAG_PLAY_SOUND);
/** Stream to control when the user is touching the screen. */
private static final int STREAM_TOUCHING_SCREEN = SpeechController.DEFAULT_STREAM;
/** Stream to control when the user is not touching the screen. */
private static final int STREAM_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE;
/** Tag used for identification of the wake lock held by this class */
private static final String WL_TAG = ProcessorVolumeStream.class.getSimpleName();
/** The audio manager, used to adjust speech volume. */
private final AudioManager mAudioManager;
/** WakeLock used to keep the screen active during key events */
private final WakeLock mWakeLock;
/** Handler for completing volume key handling outside of the main key-event handler. */
private final VolumeStreamHandler mHandler = new VolumeStreamHandler(this);
/**
* The cursor controller, used for determining the focused node and
* navigating.
*/
private final CursorController mCursorController;
/**
* Feedback controller for providing feedback on boundaries during volume
* key navigation.
*/
private final FeedbackController mFeedbackController;
/** Whether the user is touching the screen. */
private boolean mTouchingScreen = false;
private SharedPreferences mPrefs;
private TalkBackService mService;
private DimScreenController mDimScreenController;
private VolumeButtonPatternDetector mPatternDetector;
@SuppressWarnings("deprecation")
public ProcessorVolumeStream(FeedbackController feedbackController,
CursorController cursorController,
DimScreenController dimScreenController,
TalkBackService service) {
if (feedbackController == null) throw new IllegalStateException(
"CachedFeedbackController is null");
if (cursorController == null) throw new IllegalStateException("CursorController is null");
if (dimScreenController == null) throw new IllegalStateException(
"DimScreenController is null");
mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE);
mCursorController = cursorController;
mFeedbackController = feedbackController;
final PowerManager pm = (PowerManager) service.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(
PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, WL_TAG);
mPrefs = SharedPreferencesUtils.getSharedPreferences(service);
mService = service;
mDimScreenController = dimScreenController;
mPatternDetector = new VolumeButtonPatternDetector();
mPatternDetector.setOnPatternMatchListener(this);
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
switch (event.getEventType()) {
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START:
mTouchingScreen = true;
break;
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
mTouchingScreen = false;
break;
}
}
@Override
public boolean onKeyEvent(KeyEvent event) {
boolean handled = mPatternDetector.onKeyEvent(event);
if (handled) {
// Quickly acquire and release the wake lock so that
// PowerManager.ON_AFTER_RELEASE takes effect.
mWakeLock.acquire();
mWakeLock.release();
}
return handled;
}
@Override
public boolean processWhenServiceSuspended() {
return true;
}
private void handleBothVolumeKeysLongPressed() {
if (TalkBackService.isServiceActive() && switchTalkBackActiveStateEnabled()) {
mService.requestSuspendTalkBack();
} else {
mService.resumeTalkBack();
}
}
private boolean switchTalkBackActiveStateEnabled() {
return SharedPreferencesUtils.getBooleanPref(mPrefs, mService.getResources(),
R.string.pref_two_volume_long_press_key,
R.bool.pref_resume_volume_buttons_long_click_default);
}
private void navigateSlider(int button, @NonNull AccessibilityNodeInfoCompat node) {
int action;
if (button == VolumeButtonPatternDetector.VOLUME_UP) {
action = AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD;
} else if (button == VolumeButtonPatternDetector.VOLUME_DOWN) {
action = AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD;
} else {
return;
}
PerformActionUtils.performAction(node, action);
}
private void navigateEditText(int button, @NonNull AccessibilityNodeInfoCompat node) {
boolean result = false;
Bundle args = new Bundle();
CursorGranularity currentGranularity = mCursorController.getGranularityAt(node);
if (currentGranularity != CursorGranularity.DEFAULT) {
args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
currentGranularity.value);
} else {
args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER);
}
if (mCursorController.isSelectionModeActive()) {
args.putBoolean(
AccessibilityNodeInfoCompat.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, true);
}
EventState.getInstance().addEvent(
EventState.EVENT_SKIP_FOCUS_PROCESSING_AFTER_GRANULARITY_MOVE);
EventState.getInstance().addEvent(
EventState.EVENT_SKIP_HINT_AFTER_GRANULARITY_MOVE);
if (button == VolumeButtonPatternDetector.VOLUME_UP) {
result = PerformActionUtils.performAction(node,
AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, args);
} else if (button == VolumeButtonPatternDetector.VOLUME_DOWN) {
result = PerformActionUtils.performAction(node,
AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, args);
}
if (!result) {
mFeedbackController.playAuditory(R.raw.complete);
}
}
private boolean attemptNavigation(int button) {
AccessibilityNodeInfoCompat node = mCursorController.getCursorOrInputCursor();
// Clear focus if it is on an IME
if (node != null) {
if (API_LEVEL_SUPPORTS_WINDOW_NAVIGATION) {
for (AccessibilityWindowInfo awi : mService.getWindows()) {
if (awi.getId() == node.getWindowId()) {
if (awi.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD) {
node.recycle();
node = null;
}
break;
}
}
}
}
// If we cleared the focus before it is on an IME, try to get the current node again.
if (node == null) {
node = mCursorController.getCursorOrInputCursor();
}
if (node == null) return false;
try {
if (Role.getRole(node) == Role.ROLE_SEEK_CONTROL) {
navigateSlider(button, node);
return true;
}
// In general, do not allow volume key navigation when the a11y focus is placed but
// it is not on the edit field that the keyboard is currently editing.
//
// Example 1:
// EditText1 has input focus and EditText2 has accessibility focus.
// getCursorOrInputCursor() will return EditText2 based on its priority order.
// EditText2.isFocused() = false, so we should not allow volume keys to control text.
//
// Example 2:
// EditText1 in Window1 has input focus. EditText2 in Window2 has input focus as well.
// If Window1 is input-focused but Window2 has the accessibility focus, don't allow
// the volume keys to control the text.
boolean nodeWindowFocused;
if (API_LEVEL_SUPPORTS_WINDOW_NAVIGATION) {
nodeWindowFocused = node.getWindow() != null && node.getWindow().isFocused();
} else {
nodeWindowFocused = true;
}
if (node.isFocused() && nodeWindowFocused &&
AccessibilityNodeInfoUtils.isEditable(node)) {
navigateEditText(button, node);
return true;
}
return false;
} finally {
AccessibilityNodeInfoUtils.recycleNodes(node);
}
}
private void adjustVolumeFromKeyEvent(int button) {
final int direction = ((button == VolumeButtonPatternDetector.VOLUME_UP) ?
AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER);
if (mTouchingScreen) {
mAudioManager.adjustStreamVolume(
STREAM_TOUCHING_SCREEN, direction, DEFAULT_FLAGS_TOUCHING_SCREEN);
} else {
// Attempt to adjust the suggested stream, but let the system
// override in special situations like during voice calls, when an
// application has locked the volume control stream, or when music
// is playing.
mAudioManager.adjustSuggestedStreamVolume(
direction, STREAM_DEFAULT, DEFAULT_FLAGS_NOT_TOUCHING_SCREEN);
}
}
@Override
public void onPatternMatched(int patternCode, int buttonCombination) {
mHandler.postPatternMatched(patternCode, buttonCombination);
}
public void onPatternMatchedInternal(int patternCode, int buttonCombination) {
switch (patternCode) {
case VolumeButtonPatternDetector.SHORT_PRESS_PATTERN:
handleSingleTap(buttonCombination);
break;
case VolumeButtonPatternDetector.TWO_BUTTONS_LONG_PRESS_PATTERN:
handleBothVolumeKeysLongPressed();
mPatternDetector.clearState();
break;
case VolumeButtonPatternDetector.TWO_BUTTONS_THREE_PRESS_PATTERN:
if (!mService.isInstanceActive()) {
// If the service isn't active, the user won't get any feedback that
// anything happened, so we shouldn't change the dimming setting.
return;
}
boolean globalShortcut = isTripleClickEnabledGlobally();
boolean dimmed = mDimScreenController.isDimmingEnabled();
if (dimmed && (globalShortcut || mDimScreenController.isInstructionDisplayed())) {
mDimScreenController.disableDimming();
} else if (!dimmed && globalShortcut) {
mDimScreenController.showDimScreenDialog();
}
break;
}
}
private void handleSingleTap(int button) {
if (TalkBackService.isServiceActive() && attemptNavigation(button)) {
return;
}
adjustVolumeFromKeyEvent(button);
}
private boolean isTripleClickEnabledGlobally() {
SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(mService);
return SharedPreferencesUtils.getBooleanPref(prefs, mService.getResources(),
R.string.pref_dim_volume_three_clicks_key,
R.bool.pref_dim_volume_three_clicks_default);
}
/**
* Used to run potentially long methods outside of the key handler so that we don't ever
* hit the key handler timeout.
*/
private static final class VolumeStreamHandler
extends WeakReferenceHandler<ProcessorVolumeStream> {
public VolumeStreamHandler(ProcessorVolumeStream parent) {
super(parent);
}
@Override
protected void handleMessage(Message msg, ProcessorVolumeStream parent) {
int patternCode = msg.arg1;
int buttonCombination = msg.arg2;
parent.onPatternMatchedInternal(patternCode, buttonCombination);
}
public void postPatternMatched(int patternCode, int buttonCombination) {
Message msg = obtainMessage(0 /* what */,
patternCode /* arg1 */,
buttonCombination /* arg2 */);
sendMessage(msg);
}
}
}