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

Java tutorial

Introduction

Here is the source code for com.android.screenspeak.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.screenspeak.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.PowerManager;
import android.os.PowerManager.WakeLock;
import android.preference.PreferenceManager;
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 android.widget.EditText;

import android.widget.SeekBar;
import com.android.screenspeak.CursorGranularity;
import com.android.screenspeak.R;
import com.android.screenspeak.SpeechController;
import com.google.android.marvin.screenspeak.ScreenSpeakService;
import com.android.screenspeak.controller.CursorController;
import com.android.screenspeak.controller.DimScreenController;
import com.android.screenspeak.controller.FeedbackController;
import com.android.screenspeak.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, ScreenSpeakService.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;

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

    /**
     * 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 ScreenSpeakService mService;
    private DimScreenController mDimScreenController;

    private VolumeButtonPatternDetector mPatternDetector;

    @SuppressWarnings("deprecation")
    public ProcessorVolumeStream(FeedbackController feedbackController, CursorController cursorController,
            DimScreenController dimScreenController, ScreenSpeakService 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 = PreferenceManager.getDefaultSharedPreferences(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 (ScreenSpeakService.isServiceActive() && switchScreenSpeakActiveStateEnabled()) {
            mService.requestSuspendScreenSpeak();
        } else {
            mService.resumeScreenSpeak();
        }
    }

    private boolean switchScreenSpeakActiveStateEnabled() {
        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) {
        if (button == VolumeButtonPatternDetector.VOLUME_UP) {
            mCursorController.more();
        } else if (button == VolumeButtonPatternDetector.VOLUME_DOWN) {
            mCursorController.less();
        }
    }

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

        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 AccessibilityNodeInfoCompat findInputFocus() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
            List<AccessibilityWindowInfo> awis = mService.getWindows();
            for (AccessibilityWindowInfo awi : awis) {
                if (awi.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD)
                    continue;
                AccessibilityNodeInfo info = awi.getRoot();
                if (info == null)
                    continue;
                AccessibilityNodeInfoCompat root = new AccessibilityNodeInfoCompat(awi.getRoot());
                AccessibilityNodeInfoCompat focus = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT);
                if (focus != null) {
                    if (!root.equals(focus))
                        root.recycle();
                    return focus;
                } else {
                    root.recycle();
                }
            }
            return null;
        } else {
            AccessibilityNodeInfo info = mService.getRootInActiveWindow();
            if (info == null)
                return null;
            AccessibilityNodeInfoCompat root = new AccessibilityNodeInfoCompat(info);
            AccessibilityNodeInfoCompat focus = root.findFocus(AccessibilityNodeInfoCompat.FOCUS_INPUT);
            if (focus != null && !focus.equals(root))
                root.recycle();
            return focus;
        }
    }

    private boolean attemptNavigation(int button) {
        AccessibilityNodeInfoCompat node = mCursorController.getCursor();

        // Clear focus if it is on an IME
        if (node != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
                for (AccessibilityWindowInfo awi : mService.getWindows()) {
                    if (awi.getId() == node.getWindowId()) {
                        if (awi.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD) {
                            node.recycle();
                            node = null;
                        }
                        break;
                    }
                }
            }
        }

        if (node == null) {
            node = findInputFocus();
        }

        if (node == null)
            return false;
        try {
            if (AccessibilityNodeInfoUtils.nodeMatchesClassByType(node, SeekBar.class)) {
                navigateSlider(button);
                return true;
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                if (((AccessibilityNodeInfo) node.getInfo()).isEditable()) {
                    navigateEditText(button, node);
                    return true;
                }
            } else {
                if (AccessibilityNodeInfoUtils.nodeMatchesClassByType(node, EditText.class)
                        || AccessibilityNodeInfoUtils.nodeMatchesClassByName(node,
                                "com.google.android.search.searchplate.SimpleSearchText")) {
                    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) {
        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 (mDimScreenController.isInstructionDisplayed()) {
                mDimScreenController.makeScreenBright();
                resetDimScreenSettings();
            }
            break;
        }
    }

    private void resetDimScreenSettings() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mService);
        SharedPreferencesUtils.putBooleanPref(prefs, mService.getResources(),
                R.string.pref_dim_when_screenspeak_enabled_key, false);
    }

    private void handleSingleTap(int button) {
        if (ScreenSpeakService.isServiceActive() && attemptNavigation(button)) {
            return;
        }

        adjustVolumeFromKeyEvent(button);
    }
}