/*
* Copyright (C) 2015 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 android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.android.talkback.FeedbackItem;
import com.android.talkback.R;
import com.android.talkback.SpeechController;
import com.android.talkback.eventprocessor.EventState;
import com.google.android.marvin.talkback.TalkBackService;
import com.android.utils.AccessibilityEventListener;
import java.util.ArrayList;
import java.util.List;
public class ListMenuManager implements MenuManager, AccessibilityEventListener {
/** Minimum reset-focus delay to prevent flakiness. Should be hardly perceptible. */
private static final long RESET_FOCUS_DELAY_SHORT = 50;
/** Longer reset-focus delay for actions that explicitly request a delay. */
private static final long RESET_FOCUS_DELAY_LONG = 1000;
private TalkBackService mService;
private SpeechController mSpeechController;
private int mMenuShown;
private ContextMenuItemClickProcessor mMenuClickProcessor;
private DeferredAction mDeferredAction;
private Dialog mCurrentDialog;
private FeedbackItem mLastUtterance;
private MenuTransformer mMenuTransformer;
private MenuActionInterceptor mMenuActionInterceptor;
public ListMenuManager(TalkBackService service) {
mService = service;
mSpeechController = service.getSpeechController();
mMenuClickProcessor = new ContextMenuItemClickProcessor(service);
}
@Override
public boolean showMenu(int menuId) {
mLastUtterance = mSpeechController.getLastUtterance();
dismissAll();
mService.saveFocusedNode();
final ListMenu menu = new ListMenu(mService);
menu.setDefaultListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.hasSubMenu()) {
CharSequence[] items = getItemsFromMenu(item.getSubMenu());
ListMenu menu = (ListMenu) item.getSubMenu();
showDialogMenu(menu.getTitle(), items, menu);
} else if (item.getItemId() == R.id.spell_last_utterance) {
mService.getSpeechController().interrupt();
mService.getSpeechController().spellUtterance(
mLastUtterance.getAggregateText());
} else if (item.getItemId() == R.id.repeat_last_utterance) {
mService.getSpeechController().interrupt();
mService.getSpeechController().repeatUtterance(mLastUtterance);
} else if (item.getItemId() == R.id.copy_last_utterance_to_clipboard) {
mService.getSpeechController().interrupt();
mService.getSpeechController().copyLastUtteranceToClipboard(mLastUtterance);
} else {
mMenuClickProcessor.onMenuItemClicked(item);
}
return true;
}
});
ListMenuPreparer menuPreparer = new ListMenuPreparer(mService);
menuPreparer.prepareMenu(menu, menuId);
if (mMenuTransformer != null) {
mMenuTransformer.transformMenu(menu, menuId);
}
if (menu.size() == 0) {
mSpeechController.speak(mService.getString(R.string.title_local_breakout_no_items),
SpeechController.QUEUE_MODE_FLUSH_ALL, FeedbackItem.FLAG_NO_HISTORY, null);
return false;
}
showDialogMenu(menu.getTitle(), getItemsFromMenu(menu), menu);
return true;
}
private void showDialogMenu(String title, CharSequence[] items, final ContextMenu menu) {
if (items == null || items.length == 0) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(mService);
builder.setTitle(title);
View view = prepareCustomView(items, new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Dialog currentDialog = mCurrentDialog;
final ContextMenuItem menuItem = menu.getItem(position);
if (mMenuActionInterceptor != null) {
if (mMenuActionInterceptor.onInterceptMenuClick(menuItem)) {
// If the click was intercepted, stop processing the
// event.
return;
}
}
if (menuItem.isEnabled()) {
// Defer the action only if we are about to close the menu and there's a saved
// node. In that case, we have to wait for it to regain accessibility focus
// before acting.
if (menuItem.hasSubMenu() || !mService.hasSavedNode()) {
menuItem.onClickPerformed();
} else {
mDeferredAction = getDeferredAction(menuItem);
}
}
if (currentDialog != null && currentDialog.isShowing() && menuItem.isEnabled()) {
if (menuItem.getSkipRefocusEvents()) {
EventState.getInstance().addEvent(EventState
.EVENT_SKIP_FOCUS_PROCESSING_AFTER_CURSOR_CONTROL);
EventState.getInstance().addEvent(EventState
.EVENT_SKIP_WINDOWS_CHANGED_PROCESSING_AFTER_CURSOR_CONTROL);
EventState.getInstance().addEvent(EventState
.EVENT_SKIP_WINDOW_STATE_CHANGED_PROCESSING_AFTER_CURSOR_CONTROL);
EventState.getInstance().addEvent(EventState
.EVENT_SKIP_HINT_AFTER_CURSOR_CONTROL);
}
currentDialog.dismiss();
}
}
});
builder.setView(view);
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (mMenuActionInterceptor != null) {
mMenuActionInterceptor.onCancelButtonClicked();
}
dialog.dismiss();
}
});
AlertDialog alert = builder.create();
alert.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
mMenuShown--;
if (mMenuShown == 0) {
// Sometimes, the node we want to refocus erroneously reports that it is
// already accessibility focused right after windows change; to mitigate this,
// we should wait a very short delay.
long delay = RESET_FOCUS_DELAY_SHORT;
if (mDeferredAction != null) {
mService.addEventListener(ListMenuManager.this);
// Actions that explicitly need a focus delay should get a much longer
// focus delay.
if (needFocusDelay(mDeferredAction.actionId)) {
delay = RESET_FOCUS_DELAY_LONG;
}
}
mService.resetFocusedNode(delay);
mCurrentDialog = null;
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
alert.getWindow().setType(WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY);
} else {
alert.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
}
alert.show();
mCurrentDialog = alert;
mMenuShown++;
}
private View prepareCustomView(CharSequence[] items, AdapterView.OnItemClickListener listener) {
ListView view = new ListView(mService);
view.setBackground(null);
view.setDivider(null);
ArrayAdapter<CharSequence> adapter = new ArrayAdapter<>(mService,
android.R.layout.simple_list_item_1, android.R.id.text1, items);
view.setAdapter(adapter);
view.setOnItemClickListener(listener);
return view;
}
// on pre L_MR1 version focus events could be swallowed on platform after window state change
// so for actions that are rely on accessibility focus we need to delay focus request
private boolean needFocusDelay(int actionId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
return false;
}
switch (actionId) {
case R.id.pause_feedback:
case R.id.talkback_settings:
case R.id.enable_dimming:
case R.id.disable_dimming:
case R.id.tts_settings:
return false;
}
return true;
}
private DeferredAction getDeferredAction(final ContextMenuItem menuItem) {
DeferredAction action = new DeferredAction() {
@Override
public void run() {
menuItem.onClickPerformed();
}
};
action.actionId = menuItem.getItemId();
return action;
}
private CharSequence[] getItemsFromMenu(Menu menu) {
int size = menu.size();
List<CharSequence> items = new ArrayList<>();
for (int i = 0; i < size; i++) {
MenuItem item = menu.getItem(i);
if (item != null && item.isVisible()) {
items.add(item.getTitle());
}
}
return items.toArray(new CharSequence[items.size()]);
}
@Override
public boolean isMenuShowing() {
return false;
}
@Override
public void dismissAll() {
if (mCurrentDialog != null && mCurrentDialog.isShowing()) {
mCurrentDialog.dismiss();
mCurrentDialog = null;
}
}
@Override
public void clearCache() {
// NoOp
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED &&
mDeferredAction != null) {
Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
if (mDeferredAction != null) {
mDeferredAction.run();
mDeferredAction = null;
}
}
});
mService.postRemoveEventListener(this);
}
}
@Override
public void onGesture(int gesture) {}
@Override
public void setMenuTransformer(MenuTransformer transformer) {
mMenuTransformer = transformer;
}
@Override
public void setMenuActionInterceptor(MenuActionInterceptor actionInterceptor) {
mMenuActionInterceptor = actionInterceptor;
}
private static abstract class DeferredAction implements Runnable {
public int actionId;
}
}