/* * 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; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.Color; import android.os.Bundle; import android.preference.DialogPreference; import android.preference.Preference; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; import android.view.accessibility.AccessibilityManager; import android.widget.Button; import android.widget.TextView; import com.android.talkback.keyboard.KeyComboModel; import com.google.android.marvin.talkback.TalkBackService; public class KeyboardShortcutDialogPreference extends DialogPreference implements DialogInterface.OnKeyListener, AccessibilityManager.AccessibilityStateChangeListener { private static final int KEY_EVENT_SOURCE_ACTIVITY = 0; private static final int KEY_EVENT_SOURCE_ACCESSIBILITY_SERVICE = 1; private TextView mKeyAssignmentView; private KeyComboManager mKeyComboManager; private TextView mInstructionText; private int mKeyEventSource = KEY_EVENT_SOURCE_ACTIVITY; private AccessibilityManager mAccessibilityManager; private int mTemporaryModifier; private int mTemporaryKeyCode; private View.OnClickListener mClearButtonClickListener = new View.OnClickListener() { @Override public void onClick(View v) { mInstructionText.setTextColor(Color.BLACK); clearTemporaryKeyComboCode(); updateKeyAssignmentText(); } }; private View.OnClickListener mOkButtonClickListener = new View.OnClickListener() { @Override public void onClick(View v) { long temporaryKeyComboCode = getTemporaryKeyComboCodeWithoutTriggerModifier(); if (temporaryKeyComboCode == KeyComboModel.KEY_COMBO_CODE_INVALID || !mKeyComboManager.getKeyComboModel().isEligibleKeyComboCode( temporaryKeyComboCode)) { mInstructionText.setTextColor(Color.RED); TalkBackKeyboardShortcutPreferencesActivity.announceText( mInstructionText.getText().toString(), getContext()); return; } String key = mKeyComboManager.getKeyComboModel().getKeyForKeyComboCode( getTemporaryKeyComboCodeWithoutTriggerModifier()); if (key == null) { saveKeyCode(); notifyChanged(); } else if (!key.equals(getKey())) { showOverrideKeyComboDialog(key); return; } Dialog dialog = getDialog(); if (dialog != null) { dialog.dismiss(); } } }; public KeyboardShortcutDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } public KeyboardShortcutDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public KeyboardShortcutDialogPreference(Context context, AttributeSet attrs) { super(context, attrs); init(); } public KeyboardShortcutDialogPreference(Context context) { super(context); init(); } private void init() { setPersistent(true); setDialogLayoutResource(R.layout.keyboard_shortcut_dialog); if (TalkBackService.getInstance() != null) { mKeyComboManager = TalkBackService.getInstance().getKeyComboManager(); } else { mKeyComboManager = KeyComboManager.create(getContext()); } if (mKeyComboManager == null) { throw new IllegalStateException("KeyboardShortcutDialogPreference should never appear " + "on systems where KeyComboManager is unavailable"); } setTemporaryKeyComboCodeWithoutTriggerModifier( mKeyComboManager.getKeyComboModel().getKeyComboCodeForKey(getKey())); mAccessibilityManager = (AccessibilityManager) getContext().getSystemService( Context.ACCESSIBILITY_SERVICE); mAccessibilityManager.addAccessibilityStateChangeListener(this); updateAvailability(); } public void onTriggerModifierChanged() { setTemporaryKeyComboCodeWithoutTriggerModifier( mKeyComboManager.getKeyComboModel().getKeyComboCodeForKey(getKey())); // Update summary since it will be changed when trigger modifier is changed. setSummary(getSummary()); } /** * Clears current temporary key combo code. */ private void clearTemporaryKeyComboCode() { mTemporaryModifier = KeyComboModel.NO_MODIFIER; mTemporaryKeyCode = KeyEvent.KEYCODE_UNKNOWN; } /** * Sets temporary key combo code with trigger modifier. You can set key combo code which doesn't * contain trigger modifier. */ private void setTemporaryKeyComboCodeWithTriggerModifier(long keyComboCode) { mTemporaryModifier = KeyComboManager.getModifier(keyComboCode); mTemporaryKeyCode = KeyComboManager.getKeyCode(keyComboCode); } /** * Sets temporary key combo code without trigger modifier. */ private void setTemporaryKeyComboCodeWithoutTriggerModifier(long keyComboCode) { mTemporaryModifier = KeyComboManager.getModifier(keyComboCode); mTemporaryKeyCode = KeyComboManager.getKeyCode(keyComboCode); int triggerModifier = mKeyComboManager.getKeyComboModel().getTriggerModifier(); if (keyComboCode != KeyComboModel.KEY_COMBO_CODE_UNASSIGNED && triggerModifier != KeyComboModel.NO_MODIFIER) { mTemporaryModifier = mTemporaryModifier | triggerModifier; } } /** * Gets temporary key combo code with trigger modifier. */ private long getTemporaryKeyComboCodeWithTriggerModifier() { return KeyComboManager.getKeyComboCode(mTemporaryModifier, mTemporaryKeyCode); } /** * Gets temporary key combo code without trigger modifier. If current temporary key combo code * doesn't contain trigger modifier, KEY_COMBO_CODE_INVALID will be returned. */ private long getTemporaryKeyComboCodeWithoutTriggerModifier() { if (getTemporaryKeyComboCodeWithTriggerModifier() == KeyComboModel.KEY_COMBO_CODE_UNASSIGNED) { return KeyComboModel.KEY_COMBO_CODE_UNASSIGNED; } int triggerModifier = mKeyComboManager.getKeyComboModel().getTriggerModifier(); if (triggerModifier != KeyComboModel.NO_MODIFIER && (mTemporaryModifier & triggerModifier) == 0) { return KeyComboModel.KEY_COMBO_CODE_INVALID; } int modifier = mTemporaryModifier & ~triggerModifier; return KeyComboManager.getKeyComboCode(modifier, mTemporaryKeyCode); } @Override public void onAccessibilityStateChanged(boolean enabled) { updateAvailability(); } @Override protected void onPrepareForRemoval() { mAccessibilityManager.removeAccessibilityStateChangeListener(this); super.onPrepareForRemoval(); } private void updateAvailability() { int keyEventSource = getKeyEventSourceForCurrentKeyComboModel(); if (keyEventSource == KEY_EVENT_SOURCE_ACTIVITY) { setEnabled(true); return; } else { setEnabled(TalkBackService.isServiceActive()); } } private int getKeyEventSourceForCurrentKeyComboModel() { int triggerModifier = mKeyComboManager.getKeyComboModel().getTriggerModifier(); if (triggerModifier == KeyComboModel.NO_MODIFIER) { return KEY_EVENT_SOURCE_ACTIVITY; } else { return KEY_EVENT_SOURCE_ACCESSIBILITY_SERVICE; } } private void setKeyEventSource(int keyEventSource) { if (mKeyEventSource == keyEventSource) { return; } mKeyEventSource = keyEventSource; if (keyEventSource == KEY_EVENT_SOURCE_ACCESSIBILITY_SERVICE) { mKeyComboManager.setKeyboardShortcutDialogPreferenceForKeyEvents(this); } else { mKeyComboManager.setKeyboardShortcutDialogPreferenceForKeyEvents(null); } } @Override public void notifyChanged() { super.notifyChanged(); } @Override public CharSequence getSummary() { return mKeyComboManager.getKeyComboStringRepresentation( getTemporaryKeyComboCodeWithTriggerModifier()); } @Override protected void onDialogClosed(boolean positiveResult) { super.onDialogClosed(positiveResult); setTemporaryKeyComboCodeWithoutTriggerModifier( mKeyComboManager.getKeyComboModel().getKeyComboCodeForKey(getKey())); mKeyComboManager.setMatchKeyCombo(true); setKeyEventSource(KEY_EVENT_SOURCE_ACTIVITY); } @Override protected void onBindDialogView(@NonNull View view) { super.onBindDialogView(view); setTemporaryKeyComboCodeWithoutTriggerModifier( mKeyComboManager.getKeyComboModel().getKeyComboCodeForKey(getKey())); mKeyAssignmentView = (TextView) view.findViewById(R.id.assigned_combination); mInstructionText = (TextView) view.findViewById(R.id.instruction); mInstructionText.setText( mKeyComboManager.getKeyComboModel().getDescriptionOfEligibleKeyCombo()); updateKeyAssignmentText(); mKeyComboManager.setMatchKeyCombo(false); } private void updateKeyAssignmentText() { mKeyAssignmentView.setText(getSummary()); } @Override protected void showDialog(Bundle state) { super.showDialog(state); AlertDialog alertDialog = (AlertDialog) getDialog(); if (alertDialog == null) { return; } View clear = alertDialog.findViewById(R.id.clear); clear.setOnClickListener(mClearButtonClickListener); alertDialog.getButton(DialogInterface.BUTTON_POSITIVE) .setOnClickListener(mOkButtonClickListener); alertDialog.setOnKeyListener(this); Button okButton = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE); okButton.setFocusableInTouchMode(true); okButton.requestFocus(); setKeyEventSource(getKeyEventSourceForCurrentKeyComboModel()); } public boolean onKeyEventFromKeyComboManager(KeyEvent event) { if (mKeyEventSource != KEY_EVENT_SOURCE_ACCESSIBILITY_SERVICE) { return false; } return onKeyEventInternal(event); } @Override public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { if (mKeyEventSource != KEY_EVENT_SOURCE_ACTIVITY) { return false; } return onKeyEventInternal(event); } private boolean onKeyEventInternal(KeyEvent event) { if (!processKeyEvent(event)) { return false; } // The plain backspace key clears the shortcut; anything else is treated as a new shortcut. if (event.getKeyCode() == KeyEvent.KEYCODE_DEL && event.hasNoModifiers()) { clearTemporaryKeyComboCode(); } else { setTemporaryKeyComboCodeWithTriggerModifier(KeyComboManager.getKeyComboCode(event)); } updateKeyAssignmentText(); return true; } private boolean processKeyEvent(KeyEvent event) { if (event == null) { return false; } if (event.getRepeatCount() > 1) { return false; } //noinspection SimplifiableIfStatement if (event.getKeyCode() == KeyEvent.KEYCODE_BACK || event.getKeyCode() == KeyEvent.KEYCODE_HOME || event.getKeyCode() == KeyEvent.KEYCODE_CALL || event.getKeyCode() == KeyEvent.KEYCODE_ENDCALL) { return false; } // Enter and Esc are used to accept/dismiss dialogs. However, the default shortcuts // involve Enter and Esc (with modifiers), so we should only trap Enter and Esc without // modifiers. boolean isDialogNavigation = event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE; if (isDialogNavigation && event.hasNoModifiers()) { return false; } return event.getAction() == KeyEvent.ACTION_DOWN; } private void showOverrideKeyComboDialog(final String key) { final Preference currentActionPreference = getPreferenceManager().findPreference(key); if (currentActionPreference == null) { return; } final Preference newActionPreference = getPreferenceManager().findPreference(getKey()); if (newActionPreference == null) { return; } CharSequence currentAction = currentActionPreference.getTitle(); CharSequence newAction = newActionPreference.getTitle(); setKeyEventSource(KEY_EVENT_SOURCE_ACTIVITY); showOverrideKeyComboDialog(currentAction, newAction, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (which != DialogInterface.BUTTON_POSITIVE) { setKeyEventSource(getKeyEventSourceForCurrentKeyComboModel()); return; } saveKeyCode(); mKeyComboManager.getKeyComboModel().clearKeyComboCode(key); notifyListener(key, mKeyComboManager.getKeyComboModel().getKeyComboCodeForKey(key)); Dialog mainDialog = getDialog(); if (mainDialog != null) { mainDialog.dismiss(); } } }); } private void saveKeyCode() { mKeyComboManager.getKeyComboModel().saveKeyComboCode(getKey(), getTemporaryKeyComboCodeWithoutTriggerModifier()); notifyListener(getKey(), getTemporaryKeyComboCodeWithoutTriggerModifier()); } public void setKeyComboCode(long keyComboCodeWithoutModifier) { setTemporaryKeyComboCodeWithoutTriggerModifier(keyComboCodeWithoutModifier); } private void notifyListener(String key, Object newValue) { Preference preference = getPreferenceManager().findPreference(key); if (preference == null || !(preference instanceof KeyboardShortcutDialogPreference)) { return; } OnPreferenceChangeListener listener = preference.getOnPreferenceChangeListener(); if (listener != null) { listener.onPreferenceChange(preference, newValue); } } private void showOverrideKeyComboDialog(CharSequence currentAction, CharSequence newAction, final DialogInterface.OnClickListener clickListener) { String message = getContext().getString(R.string.override_keycombo_message_two_params, currentAction, newAction); AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle(R.string.override_keycombo) .setMessage(message) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); clickListener.onClick(dialog, which); } }) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { saveKeyCode(); clickListener.onClick(dialog, which); } }) .show(); } }