/*
* 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.talkback.contextmenu;
import com.android.talkback.FeedbackItem;
import com.android.talkback.R;
import android.os.Build;
import android.os.Handler;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.WindowManager;
import com.android.talkback.SpeechController;
import com.google.android.marvin.talkback.TalkBackService;
import com.android.talkback.controller.FeedbackController;
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 TalkBackService 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;
private MenuTransformer mMenuTransformer;
private MenuActionInterceptor mMenuActionInterceptor;
public RadialMenuManager(boolean isTouchScreen,
TalkBackService context) {
mIsTouchScreen = isTouchScreen;
mService = context;
mSpeechController = context.getSpeechController();
mFeedbackController = context.getFeedbackController();
mClient = new TalkBackRadialMenuClient(mService);
}
/**
* 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) {
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;
}
if (mMenuTransformer != null) {
mMenuTransformer.transformMenu(overlay.getMenu(), menuId);
}
overlay.showWithDot();
return true;
}
@Override
public boolean isMenuShowing() {
return (mIsRadialMenuShowing > 0);
}
@Override
public void onGesture(int gestureId) {
dismissAll();
}
@Override
public void setMenuTransformer(MenuTransformer transformer) {
mMenuTransformer = transformer;
}
@Override
public void setMenuActionInterceptor(MenuActionInterceptor actionInterceptor) {
mMenuActionInterceptor = actionInterceptor;
}
@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);
boolean handled = mMenuActionInterceptor != null &&
mMenuActionInterceptor.onInterceptMenuClick(menuItem);
if (!handled) {
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: 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++;
}
@Override
public void onHide(SimpleOverlay overlay) {
mHandler.removeCallbacks(mRadialMenuHint);
if (mHintSpeechPending) {
mSpeechController.interrupt();
}
mIsRadialMenuShowing--;
}
};
/**
* 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);
}
}