com.android.talkback.menurules.RuleSeekBar.java Source code

Java tutorial

Introduction

Here is the source code for com.android.talkback.menurules.RuleSeekBar.java

Source

/*
 * Copyright (C) 2016 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.menurules;

import com.google.android.marvin.talkback.TalkBackService;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.os.BuildCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.RangeInfoCompat;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;

import com.android.talkback.R;
import com.android.talkback.contextmenu.ContextMenuItem;
import com.android.talkback.contextmenu.ContextMenuItemBuilder;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.PerformActionUtils;

import java.util.LinkedList;
import java.util.List;

/**
 * Provides a LCM item to manually enter a percentage value for seek controls.
 * This functionality is only available on Android N and later.
 */
public class RuleSeekBar implements NodeMenuRule {

    @Override
    public boolean accept(TalkBackService service, AccessibilityNodeInfoCompat node) {
        if (!BuildCompat.isAtLeastN()) {
            return false;
        }

        return AccessibilityNodeInfoUtils.supportsAction(node,
                AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.getId());
    }

    @Override
    public List<ContextMenuItem> getMenuItemsForNode(TalkBackService service,
            ContextMenuItemBuilder menuItemBuilder, AccessibilityNodeInfoCompat node) {
        List<ContextMenuItem> items = new LinkedList<>();

        if (node != null) {
            final ContextMenuItem setLevel = menuItemBuilder.createMenuItem(service, Menu.NONE,
                    R.id.seekbar_breakout_set_level, Menu.NONE, service.getString(R.string.title_seek_bar_edit));
            setLevel.setOnMenuItemClickListener(new SeekBarDialogManager(service, node));
            setLevel.setSkipRefocusEvents(true);
            items.add(setLevel);
        }

        return items;
    }

    @Override
    public CharSequence getUserFriendlyMenuName(Context context) {
        return context.getString(R.string.title_seek_bar_controls);
    }

    @Override
    public boolean canCollapseMenu() {
        return true;
    }

    private static int realToPercent(float real, float min, float max) {
        return (int) (100.0f * (real - min) / (max - min));
    }

    private static float percentToReal(int percent, float min, float max) {
        return min + (percent / 100.0f) * (max - min);
    }

    // Separate package-private method so we can test the logic.
    static void setProgress(AccessibilityNodeInfoCompat node, int progress) {
        RangeInfoCompat rangeInfo = node.getRangeInfo();
        if (rangeInfo != null && progress >= 0 && progress <= 100) {
            Bundle args = new Bundle();
            args.putFloat(AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE,
                    percentToReal(progress, rangeInfo.getMin(), rangeInfo.getMax()));
            PerformActionUtils.performAction(node, AccessibilityAction.ACTION_SET_PROGRESS.getId(), args);
        }
    }

    // Deals with opening the dialog from the menu item and controlling the dialog lifecycle.
    private static class SeekBarDialogManager implements MenuItem.OnMenuItemClickListener, OnDismissListener {
        private static final int INVALID_VALUE = -1;

        private final TalkBackService mService;
        private AccessibilityNodeInfoCompat mSeekBar; // Note: not final so we can null it out.
        private int mOldValue = INVALID_VALUE;
        private int mValue = INVALID_VALUE;

        private View mRootView;
        private AlertDialog mDialog;

        public SeekBarDialogManager(TalkBackService service, AccessibilityNodeInfoCompat seekBar) {
            mService = service;
            mSeekBar = AccessibilityNodeInfoCompat.obtain(seekBar);
        }

        @Override
        public boolean onMenuItemClick(MenuItem item) {
            // Verify that node is OK and get the current seek control level first.
            final RangeInfoCompat rangeInfo = mSeekBar.getRangeInfo();
            if (rangeInfo == null) {
                return false;
            }

            mOldValue = realToPercent(rangeInfo.getCurrent(), rangeInfo.getMin(), rangeInfo.getMax());
            mService.saveFocusedNode();

            LayoutInflater inflater = LayoutInflater.from(mService);
            mRootView = inflater.inflate(R.layout.seekbar_level_dialog, null);

            final AlertDialog.Builder builder = new AlertDialog.Builder(mService).setView(mRootView)
                    .setTitle(mService.getString(R.string.title_seek_bar_edit))
                    .setPositiveButton(android.R.string.ok, null).setNegativeButton(android.R.string.cancel, null)
                    .setCancelable(true).setOnDismissListener(this);

            mDialog = builder.create();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
                mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY);
            } else {
                mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
            }
            mDialog.show();
            mService.getRingerModeAndScreenMonitor().registerDialog(mDialog);

            // We'd like to keep focus off of the text field until the user activates it.
            final Button okButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
            okButton.setFocusableInTouchMode(true);
            okButton.requestFocus();

            // Fill in the text field and restore normal input focus behavior when it gets focus.
            final EditText percentage = (EditText) mRootView.findViewById(R.id.seek_bar_level);
            percentage.setText(Integer.toString(mOldValue));
            percentage.setOnFocusChangeListener(new OnFocusChangeListener() {
                @Override
                public void onFocusChange(View v, boolean hasFocus) {
                    if (hasFocus) {
                        okButton.setFocusableInTouchMode(false);
                    }
                }
            });
            percentage.setOnEditorActionListener(new OnEditorActionListener() {
                @Override
                public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                    if (actionId == EditorInfo.IME_ACTION_DONE) {
                        submitDialog();
                        return true;
                    }
                    return false;
                }
            });

            // Use our own custom listener to prevent the dialog from closing if there's an error.
            okButton.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    submitDialog();
                }
            });

            return true;
        }

        @Override
        public void onDismiss(DialogInterface dialog) {
            if (mSeekBar == null) {
                return;
            }

            // This will only set the value if the user clicked "OK" because only "OK" will
            // change the mValue field to not be INVALID_VALUE.
            if (mValue != INVALID_VALUE && mValue != mOldValue) {
                setProgress(mSeekBar, mValue);
            }

            mService.resetFocusedNode();
            mService.getRingerModeAndScreenMonitor().unregisterDialog(dialog);
            mSeekBar.recycle();
            mSeekBar = null;
        }

        private void submitDialog() {
            if (mRootView == null || mDialog == null) {
                return;
            }

            final EditText percentage = (EditText) mRootView.findViewById(R.id.seek_bar_level);
            try {
                int percentValue = Integer.parseInt(percentage.getText().toString());
                if (percentValue < 0 || percentValue > 100) {
                    throw new IndexOutOfBoundsException();
                }

                // Need to delay setting value until the dialog is dismissed.
                mValue = percentValue;
                mDialog.dismiss();
            } catch (NumberFormatException | IndexOutOfBoundsException ex) {
                // Set the error text popup.
                CharSequence instructions = mService.getString(R.string.value_seek_bar_dialog_instructions);
                percentage.setError(instructions);
            }
        }
    }

}