com.android.talkback.eventprocessor.ProcessorVolumeStream.java Source code

Java tutorial

Introduction

Here is the source code for com.android.talkback.eventprocessor.ProcessorVolumeStream.java

Source

/*
 * 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);
        }

    }
}