com.android.screenspeak.contextmenu.RadialMenuManager.java Source code

Java tutorial

Introduction

Here is the source code for com.android.screenspeak.contextmenu.RadialMenuManager.java

Source

/*
 * Copyright (C) 2011 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.screenspeak.contextmenu;

import com.android.screenspeak.FeedbackItem;
import com.android.screenspeak.R;

import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.support.v4.content.LocalBroadcastManager;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.WindowManager;
import com.android.screenspeak.SpeechController;
import com.google.android.marvin.screenspeak.ScreenSpeakService;
import com.android.screenspeak.controller.FeedbackController;
import com.android.screenspeak.tutorial.AccessibilityTutorialActivity;
import com.android.screenspeak.tutorial.ContextMenuMonitor;
import com.android.utils.widget.SimpleOverlay;

public class RadialMenuManager implements MenuManager {
    /** Delay in milliseconds before speaking the radial menu usage hint. */
    /*package*/ static final int DELAY_RADIAL_MENU_HINT = 2000;

    /** The scales used to represent menus of various sizes. */
    private static final int[] SCALES = { R.raw.radial_menu_1, R.raw.radial_menu_2, R.raw.radial_menu_3,
            R.raw.radial_menu_4, R.raw.radial_menu_5, R.raw.radial_menu_6, R.raw.radial_menu_7,
            R.raw.radial_menu_8 };

    /** Cached radial menus. */
    private final SparseArray<RadialMenuOverlay> mCachedRadialMenus = new SparseArray<>();

    private final ScreenSpeakService mService;
    private final SpeechController mSpeechController;
    private final FeedbackController mFeedbackController;

    /** Client that responds to menu item selection and click. */
    private RadialMenuClient mClient;

    /** How many radial menus are showing. */
    private int mIsRadialMenuShowing;

    /** Whether we have queued hint speech and it has not completed yet. */
    private boolean mHintSpeechPending;

    private final boolean mIsTouchScreen;

    public RadialMenuManager(boolean isTouchScreen, ScreenSpeakService context, SpeechController speechController,
            FeedbackController feedbackController) {
        if (speechController == null)
            throw new IllegalStateException();
        if (feedbackController == null)
            throw new IllegalStateException();

        mIsTouchScreen = isTouchScreen;
        mService = context;
        mSpeechController = speechController;
        mFeedbackController = feedbackController;
    }

    /**
     * Sets the radial menu client, which is responsible for populating menus,
     * responding to click actions, and can optionally handle feedback from
     * selection.
     *
     * @param client The client to set.
     */
    public void setClient(RadialMenuClient client) {
        mClient = client;
    }

    /**
     * Shows the specified menu resource as a radial menu.
     *
     * @param menuId The identifier of the menu to display.
     * @return {@code true} if the menu could be shown.
     */
    @Override
    public boolean showMenu(int menuId) {
        // Some ScreenSpeak tutorial modules don't allow context menus.
        if (AccessibilityTutorialActivity.isTutorialActive()
                && !AccessibilityTutorialActivity.shouldAllowContextMenus()) {
            return false;
        }

        if (!mIsTouchScreen)
            return false;

        RadialMenuOverlay overlay = mCachedRadialMenus.get(menuId);

        if (overlay == null) {
            overlay = new RadialMenuOverlay(mService, menuId, false);
            overlay.setListener(mOverlayListener);

            final WindowManager.LayoutParams params = overlay.getParams();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
                params.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
            } else {
                params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
            }
            params.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            overlay.setParams(params);

            final RadialMenu menu = overlay.getMenu();
            menu.setDefaultSelectionListener(mOnSelection);
            menu.setDefaultListener(mOnClick);

            final RadialMenuView view = overlay.getView();
            view.setSubMenuMode(RadialMenuView.SubMenuMode.LIFT_TO_ACTIVATE);

            if (mClient != null) {
                mClient.onCreateRadialMenu(menuId, menu);
            }

            mCachedRadialMenus.put(menuId, overlay);
        }

        if ((mClient != null) && !mClient.onPrepareRadialMenu(menuId, overlay.getMenu())) {
            mFeedbackController.playAuditory(R.raw.complete);
            return false;
        }

        overlay.showWithDot();
        return true;
    }

    @Override
    public boolean isMenuShowing() {
        return (mIsRadialMenuShowing > 0);
    }

    @Override
    public void onGesture(int gestureId) {
        dismissAll();
    }

    @Override
    public void dismissAll() {
        for (int i = 0; i < mCachedRadialMenus.size(); ++i) {
            final RadialMenuOverlay menu = mCachedRadialMenus.valueAt(i);

            if (menu.isVisible()) {
                menu.dismiss();
            }
        }
    }

    public void clearCache() {
        mCachedRadialMenus.clear();
    }

    /**
     * Plays an F# harmonic minor scale with a number of notes equal to the number of items in the
     * specified menu, up to 8 notes.
     *
     * @param menu to play scale for
     */
    private void playScaleForMenu(Menu menu) {
        final int size = menu.size();
        if (size <= 0) {
            return;
        }

        mFeedbackController.playAuditory(SCALES[Math.min(size - 1, 7)]);
    }

    /**
     * Handles selecting menu items.
     */
    private final RadialMenuItem.OnMenuItemSelectionListener mOnSelection = new RadialMenuItem.OnMenuItemSelectionListener() {
        @Override
        public boolean onMenuItemSelection(RadialMenuItem menuItem) {
            mHandler.removeCallbacks(mRadialMenuHint);

            mFeedbackController.playHaptic(R.array.view_actionable_pattern);
            mFeedbackController.playAuditory(R.raw.focus_actionable);

            final boolean handled = (mClient != null) && mClient.onMenuItemHovered();

            if (!handled) {
                final CharSequence text;
                if (menuItem == null) {
                    text = mService.getString(android.R.string.cancel);
                } else if (menuItem.hasSubMenu()) {
                    text = mService.getString(R.string.template_menu, menuItem.getTitle());
                } else {
                    text = menuItem.getTitle();
                }

                mSpeechController.speak(text, SpeechController.QUEUE_MODE_INTERRUPT,
                        FeedbackItem.FLAG_NO_HISTORY | FeedbackItem.FLAG_DURING_RECO, null);
            }

            return true;
        }
    };

    /**
     * Handles clicking on menu items.
     */
    private final OnMenuItemClickListener mOnClick = new OnMenuItemClickListener() {
        @Override
        public boolean onMenuItemClick(MenuItem menuItem) {
            mHandler.removeCallbacks(mRadialMenuHint);

            mFeedbackController.playHaptic(R.array.view_clicked_pattern);
            mFeedbackController.playAuditory(R.raw.tick);

            final boolean handled = (mClient != null) && mClient.onMenuItemClicked(menuItem);

            if (!handled && (menuItem == null)) {
                mService.interruptAllFeedback();
            }

            if ((menuItem != null) && menuItem.hasSubMenu()) {
                playScaleForMenu(menuItem.getSubMenu());
            }

            return true;
        }
    };

    /**
     * Handles feedback from showing and hiding radial menus.
     */
    private final SimpleOverlay.SimpleOverlayListener mOverlayListener = new SimpleOverlay.SimpleOverlayListener() {
        @Override
        public void onShow(SimpleOverlay overlay) {
            final RadialMenu menu = ((RadialMenuOverlay) overlay).getMenu();

            mHandler.postDelayed(mRadialMenuHint, DELAY_RADIAL_MENU_HINT);

            // TODO(CB): Find an alternative or just speak the number of items.
            // Play a note in a C major scale for each item in the menu.
            playScaleForMenu(menu);

            mIsRadialMenuShowing++;

            // Broadcast a notification that the menu was shown.
            Intent intent = new Intent(ContextMenuMonitor.ACTION_CONTEXT_MENU_SHOWN);
            intent.putExtra(ContextMenuMonitor.EXTRA_MENU_ID, overlay.getId());
            LocalBroadcastManager.getInstance(mService).sendBroadcast(intent);
        }

        @Override
        public void onHide(SimpleOverlay overlay) {
            mHandler.removeCallbacks(mRadialMenuHint);

            if (mHintSpeechPending) {
                mSpeechController.interrupt();
            }

            mIsRadialMenuShowing--;

            // Broadcast a notification that the menu was hidden.
            Intent intent = new Intent(ContextMenuMonitor.ACTION_CONTEXT_MENU_HIDDEN);
            intent.putExtra(ContextMenuMonitor.EXTRA_MENU_ID, overlay.getId());
            LocalBroadcastManager.getInstance(mService).sendBroadcast(intent);
        }
    };

    /**
     * Runnable that speaks a usage hint for the radial menu.
     */
    private final Runnable mRadialMenuHint = new Runnable() {
        @Override
        public void run() {
            final String hintText = mService.getString(R.string.hint_radial_menu);

            mHintSpeechPending = true;
            mSpeechController.speak(hintText, null, null, SpeechController.QUEUE_MODE_QUEUE,
                    FeedbackItem.FLAG_NO_HISTORY | FeedbackItem.FLAG_DURING_RECO,
                    SpeechController.UTTERANCE_GROUP_DEFAULT, null, null, mHintSpeechCompleted);
        }
    };

    /**
     * Runnable that confirms the hint speech has completed.
     */
    private final SpeechController.UtteranceCompleteRunnable mHintSpeechCompleted = new SpeechController.UtteranceCompleteRunnable() {
        @Override
        public void run(int status) {
            mHintSpeechPending = false;
        }
    };

    private final Handler mHandler = new Handler();

    public interface RadialMenuClient {
        public void onCreateRadialMenu(int menuId, RadialMenu menu);

        public boolean onPrepareRadialMenu(int menuId, RadialMenu menu);

        public boolean onMenuItemHovered();

        public boolean onMenuItemClicked(MenuItem menuItem);
    }
}