blob: 54ea57a6cae4bc457beddf3c63dc05fcf7341137 [file] [log] [blame]
Phil Weaver106fe732016-11-22 18:18:39 -08001/*
Phil Weaverb9f06122017-11-30 10:48:17 -08002 * Copyright 2017 Google Inc.
Phil Weaver106fe732016-11-22 18:18:39 -08003 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
Phil Weaverb9f06122017-11-30 10:48:17 -080017package com.android.internal.accessibility;
Phil Weaver106fe732016-11-22 18:18:39 -080018
Rhed Jao6182af72018-08-22 18:48:59 +080019import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
Rhed Jao6dad25d2019-10-22 22:15:05 +080020import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
Rhed Jao6182af72018-08-22 18:48:59 +080021
22import static com.android.internal.util.ArrayUtils.convertToLongArray;
23
Phil Weaver106fe732016-11-22 18:18:39 -080024import android.accessibilityservice.AccessibilityServiceInfo;
menghanlie83358b2020-02-11 16:47:18 +080025import android.annotation.IntDef;
Phil Weaver106fe732016-11-22 18:18:39 -080026import android.app.ActivityManager;
Phil Weaver8c58d452017-07-28 12:58:15 -070027import android.app.ActivityThread;
Phil Weaver106fe732016-11-22 18:18:39 -080028import android.app.AlertDialog;
29import android.content.ComponentName;
30import android.content.ContentResolver;
31import android.content.Context;
32import android.content.DialogInterface;
Zhen Sun62e54c22017-08-17 15:56:20 -070033import android.content.pm.PackageManager;
Phil Weaver106fe732016-11-22 18:18:39 -080034import android.database.ContentObserver;
35import android.media.AudioAttributes;
36import android.media.Ringtone;
37import android.media.RingtoneManager;
Phil Weaverce687c52017-03-15 08:51:52 -070038import android.net.Uri;
Rhed Jao6dad25d2019-10-22 22:15:05 +080039import android.os.Build;
Phil Weaver106fe732016-11-22 18:18:39 -080040import android.os.Handler;
41import android.os.UserHandle;
Phil Weaver32ea3722017-03-13 11:32:01 -070042import android.os.Vibrator;
Phil Weaver106fe732016-11-22 18:18:39 -080043import android.provider.Settings;
Rhed Jao6182af72018-08-22 18:48:59 +080044import android.speech.tts.TextToSpeech;
45import android.speech.tts.Voice;
Phil Weaver106fe732016-11-22 18:18:39 -080046import android.text.TextUtils;
Phil Weaverb9f06122017-11-30 10:48:17 -080047import android.util.ArrayMap;
Phil Weaver106fe732016-11-22 18:18:39 -080048import android.util.Slog;
49import android.view.Window;
50import android.view.WindowManager;
51import android.view.accessibility.AccessibilityManager;
Phil Weaver106fe732016-11-22 18:18:39 -080052import android.widget.Toast;
Rhed Jao6182af72018-08-22 18:48:59 +080053
Phil Weaver106fe732016-11-22 18:18:39 -080054import com.android.internal.R;
Rhed Jao6182af72018-08-22 18:48:59 +080055import com.android.internal.util.function.pooled.PooledLambda;
Phil Weaver106fe732016-11-22 18:18:39 -080056
menghanlie83358b2020-02-11 16:47:18 +080057import java.lang.annotation.Retention;
58import java.lang.annotation.RetentionPolicy;
Jeff Sharkey8b0cff72020-03-09 15:49:01 -060059import java.util.Collection;
Phil Weaverb9f06122017-11-30 10:48:17 -080060import java.util.Collections;
Rhed Jao6dad25d2019-10-22 22:15:05 +080061import java.util.List;
Rhed Jao6182af72018-08-22 18:48:59 +080062import java.util.Locale;
Phil Weaverb9f06122017-11-30 10:48:17 -080063import java.util.Map;
Phil Weaver106fe732016-11-22 18:18:39 -080064
Phil Weaver106fe732016-11-22 18:18:39 -080065/**
Rhed Jao6dad25d2019-10-22 22:15:05 +080066 * Class to help manage the accessibility shortcut key
Phil Weaver106fe732016-11-22 18:18:39 -080067 */
68public class AccessibilityShortcutController {
69 private static final String TAG = "AccessibilityShortcutController";
Phil Weaverb9f06122017-11-30 10:48:17 -080070
71 // Dummy component names for framework features
72 public static final ComponentName COLOR_INVERSION_COMPONENT_NAME =
73 new ComponentName("com.android.server.accessibility", "ColorInversion");
74 public static final ComponentName DALTONIZER_COMPONENT_NAME =
75 new ComponentName("com.android.server.accessibility", "Daltonizer");
Rhed Jao6dad25d2019-10-22 22:15:05 +080076 public static final String MAGNIFICATION_CONTROLLER_NAME =
77 "com.android.server.accessibility.MagnificationController";
Phil Weaverb9f06122017-11-30 10:48:17 -080078
Phil Weaver32ea3722017-03-13 11:32:01 -070079 private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
80 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
81 .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
82 .build();
Phil Weaverb9f06122017-11-30 10:48:17 -080083 private static Map<ComponentName, ToggleableFrameworkFeatureInfo> sFrameworkShortcutFeaturesMap;
Phil Weaver106fe732016-11-22 18:18:39 -080084
85 private final Context mContext;
Rhed Jao6182af72018-08-22 18:48:59 +080086 private final Handler mHandler;
Phil Weaver106fe732016-11-22 18:18:39 -080087 private AlertDialog mAlertDialog;
88 private boolean mIsShortcutEnabled;
Phil Weaverce687c52017-03-15 08:51:52 -070089 private boolean mEnabledOnLockScreen;
90 private int mUserId;
91
menghanlie83358b2020-02-11 16:47:18 +080092 @Retention(RetentionPolicy.SOURCE)
93 @IntDef({
94 DialogStaus.NOT_SHOWN,
95 DialogStaus.SHOWN,
96 })
97 /** Denotes the user shortcut type. */
98 private @interface DialogStaus {
99 int NOT_SHOWN = 0;
100 int SHOWN = 1;
101 }
102
Phil Weaver106fe732016-11-22 18:18:39 -0800103 // Visible for testing
104 public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
105
Phil Weaverb9f06122017-11-30 10:48:17 -0800106 /**
Phil Weaverb9f06122017-11-30 10:48:17 -0800107 * @return An immutable map from dummy component names to feature info for toggling a framework
108 * feature
109 */
110 public static Map<ComponentName, ToggleableFrameworkFeatureInfo>
111 getFrameworkShortcutFeaturesMap() {
112 if (sFrameworkShortcutFeaturesMap == null) {
113 Map<ComponentName, ToggleableFrameworkFeatureInfo> featuresMap = new ArrayMap<>(2);
114 featuresMap.put(COLOR_INVERSION_COMPONENT_NAME,
115 new ToggleableFrameworkFeatureInfo(
116 Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
117 "1" /* Value to enable */, "0" /* Value to disable */,
118 R.string.color_inversion_feature_name));
119 featuresMap.put(DALTONIZER_COMPONENT_NAME,
120 new ToggleableFrameworkFeatureInfo(
121 Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
122 "1" /* Value to enable */, "0" /* Value to disable */,
123 R.string.color_correction_feature_name));
124 sFrameworkShortcutFeaturesMap = Collections.unmodifiableMap(featuresMap);
125 }
126 return sFrameworkShortcutFeaturesMap;
127 }
128
Phil Weaverce687c52017-03-15 08:51:52 -0700129 public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) {
Phil Weaver106fe732016-11-22 18:18:39 -0800130 mContext = context;
Rhed Jao6182af72018-08-22 18:48:59 +0800131 mHandler = handler;
Phil Weaverf2871802017-04-19 11:40:32 -0700132 mUserId = initialUserId;
Phil Weaver106fe732016-11-22 18:18:39 -0800133
Phil Weaverce687c52017-03-15 08:51:52 -0700134 // Keep track of state of shortcut settings
135 final ContentObserver co = new ContentObserver(handler) {
136 @Override
Jeff Sharkey8b0cff72020-03-09 15:49:01 -0600137 public void onChange(boolean selfChange, Collection<Uri> uris, int flags, int userId) {
Phil Weaverce687c52017-03-15 08:51:52 -0700138 if (userId == mUserId) {
139 onSettingsChanged();
140 }
141 }
142 };
Phil Weaver106fe732016-11-22 18:18:39 -0800143 mContext.getContentResolver().registerContentObserver(
144 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
Phil Weaverce687c52017-03-15 08:51:52 -0700145 false, co, UserHandle.USER_ALL);
146 mContext.getContentResolver().registerContentObserver(
147 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED),
148 false, co, UserHandle.USER_ALL);
149 mContext.getContentResolver().registerContentObserver(
150 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN),
151 false, co, UserHandle.USER_ALL);
Phil Weaverc5865d62018-03-02 16:00:43 -0800152 mContext.getContentResolver().registerContentObserver(
153 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN),
154 false, co, UserHandle.USER_ALL);
Phil Weaverce687c52017-03-15 08:51:52 -0700155 setCurrentUser(mUserId);
Phil Weaver106fe732016-11-22 18:18:39 -0800156 }
157
Phil Weaverce687c52017-03-15 08:51:52 -0700158 public void setCurrentUser(int currentUserId) {
159 mUserId = currentUserId;
160 onSettingsChanged();
161 }
162
163 /**
164 * Check if the shortcut is available.
165 *
Rhed Jao6dad25d2019-10-22 22:15:05 +0800166 * @param phoneLocked Whether or not the phone is currently locked.
Phil Weaverce687c52017-03-15 08:51:52 -0700167 *
168 * @return {@code true} if the shortcut is available
169 */
170 public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) {
171 return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen);
Phil Weaver106fe732016-11-22 18:18:39 -0800172 }
173
174 public void onSettingsChanged() {
Rhed Jao6dad25d2019-10-22 22:15:05 +0800175 final boolean hasShortcutTarget = hasShortcutTarget();
Phil Weaverce687c52017-03-15 08:51:52 -0700176 final ContentResolver cr = mContext.getContentResolver();
177 final boolean enabled = Settings.Secure.getIntForUser(
178 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1;
Phil Weaverc5865d62018-03-02 16:00:43 -0800179 // Enable the shortcut from the lockscreen by default if the dialog has been shown
180 final int dialogAlreadyShown = Settings.Secure.getIntForUser(
menghanlie83358b2020-02-11 16:47:18 +0800181 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStaus.NOT_SHOWN,
182 mUserId);
Phil Weaverce687c52017-03-15 08:51:52 -0700183 mEnabledOnLockScreen = Settings.Secure.getIntForUser(
Phil Weaverc5865d62018-03-02 16:00:43 -0800184 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
185 dialogAlreadyShown, mUserId) == 1;
Rhed Jao6dad25d2019-10-22 22:15:05 +0800186 mIsShortcutEnabled = enabled && hasShortcutTarget;
Phil Weaver106fe732016-11-22 18:18:39 -0800187 }
188
189 /**
190 * Called when the accessibility shortcut is activated
191 */
192 public void performAccessibilityShortcut() {
193 Slog.d(TAG, "Accessibility shortcut activated");
194 final ContentResolver cr = mContext.getContentResolver();
195 final int userId = ActivityManager.getCurrentUser();
196 final int dialogAlreadyShown = Settings.Secure.getIntForUser(
menghanlie83358b2020-02-11 16:47:18 +0800197 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStaus.NOT_SHOWN,
198 userId);
Phil Weaver32ea3722017-03-13 11:32:01 -0700199 // Play a notification vibration
200 Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
201 if ((vibrator != null) && vibrator.hasVibrator()) {
202 // Don't check if haptics are disabled, as we need to alert the user that their
203 // way of interacting with the phone may change if they activate the shortcut
Phil Weaverb9f06122017-11-30 10:48:17 -0800204 long[] vibePattern = convertToLongArray(
205 mContext.getResources().getIntArray(R.array.config_longPressVibePattern));
Phil Weaver32ea3722017-03-13 11:32:01 -0700206 vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
207 }
208
Phil Weaver106fe732016-11-22 18:18:39 -0800209 if (dialogAlreadyShown == 0) {
210 // The first time, we show a warning rather than toggle the service to give the user a
211 // chance to turn off this feature before stuff gets enabled.
212 mAlertDialog = createShortcutWarningDialog(userId);
213 if (mAlertDialog == null) {
214 return;
215 }
Rhed Jao6182af72018-08-22 18:48:59 +0800216 if (!performTtsPrompt(mAlertDialog)) {
217 playNotificationTone();
218 }
Phil Weaver106fe732016-11-22 18:18:39 -0800219 Window w = mAlertDialog.getWindow();
220 WindowManager.LayoutParams attr = w.getAttributes();
221 attr.type = TYPE_KEYGUARD_DIALOG;
222 w.setAttributes(attr);
223 mAlertDialog.show();
224 Settings.Secure.putIntForUser(
menghanlie83358b2020-02-11 16:47:18 +0800225 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStaus.SHOWN,
226 userId);
Phil Weaver106fe732016-11-22 18:18:39 -0800227 } else {
Rhed Jao6182af72018-08-22 18:48:59 +0800228 playNotificationTone();
Phil Weaver106fe732016-11-22 18:18:39 -0800229 if (mAlertDialog != null) {
230 mAlertDialog.dismiss();
231 mAlertDialog = null;
232 }
Rhed Jao6dad25d2019-10-22 22:15:05 +0800233 showToast();
Phil Weaver106fe732016-11-22 18:18:39 -0800234 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
235 .performAccessibilityShortcut();
236 }
237 }
238
Rhed Jao6dad25d2019-10-22 22:15:05 +0800239 /**
240 * Show toast if current assigned shortcut target is an accessibility service and its target
241 * sdk version is less than or equal to Q, or greater than Q and does not request
242 * accessibility button.
243 */
244 private void showToast() {
245 final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
246 if (serviceInfo == null) {
247 return;
248 }
249 final String serviceName = getShortcutFeatureDescription(/* no summary */ false);
250 if (serviceName == null) {
251 return;
252 }
253 final boolean requestA11yButton = (serviceInfo.flags
254 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
255 if (serviceInfo.getResolveInfo().serviceInfo.applicationInfo
256 .targetSdkVersion > Build.VERSION_CODES.Q && requestA11yButton) {
257 return;
258 }
259 // For accessibility services, show a toast explaining what we're doing.
260 String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
261 ? R.string.accessibility_shortcut_disabling_service
262 : R.string.accessibility_shortcut_enabling_service);
263 String toastMessage = String.format(toastMessageFormatString, serviceName);
264 Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
265 mContext, toastMessage, Toast.LENGTH_LONG);
Rhed Jao6dad25d2019-10-22 22:15:05 +0800266 warningToast.show();
267 }
268
Phil Weaver106fe732016-11-22 18:18:39 -0800269 private AlertDialog createShortcutWarningDialog(int userId) {
menghanlie83358b2020-02-11 16:47:18 +0800270 final String warningMessage = mContext.getString(
271 R.string.accessibility_shortcut_toogle_warning);
Phil Weaver8c58d452017-07-28 12:58:15 -0700272 final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(
273 // Use SystemUI context so we pick up any theme set in a vendor overlay
Phil Weaverb9f06122017-11-30 10:48:17 -0800274 mFrameworkObjectProvider.getSystemUiContext())
Phil Weaver106fe732016-11-22 18:18:39 -0800275 .setTitle(R.string.accessibility_shortcut_warning_dialog_title)
276 .setMessage(warningMessage)
277 .setCancelable(false)
278 .setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
279 .setNegativeButton(R.string.disable_accessibility_shortcut,
280 (DialogInterface d, int which) -> {
281 Settings.Secure.putStringForUser(mContext.getContentResolver(),
282 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
283 userId);
menghanlie83358b2020-02-11 16:47:18 +0800284
285 // If canceled, treat as if the dialog has never been shown
286 Settings.Secure.putIntForUser(mContext.getContentResolver(),
287 Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
288 DialogStaus.NOT_SHOWN, userId);
Phil Weaver106fe732016-11-22 18:18:39 -0800289 })
290 .setOnCancelListener((DialogInterface d) -> {
291 // If canceled, treat as if the dialog has never been shown
292 Settings.Secure.putIntForUser(mContext.getContentResolver(),
menghanlie83358b2020-02-11 16:47:18 +0800293 Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
294 DialogStaus.NOT_SHOWN, userId);
Phil Weaver106fe732016-11-22 18:18:39 -0800295 })
296 .create();
297 return alertDialog;
298 }
299
300 private AccessibilityServiceInfo getInfoForTargetService() {
Rhed Jao6dad25d2019-10-22 22:15:05 +0800301 final ComponentName targetComponentName = getShortcutTargetComponentName();
302 if (targetComponentName == null) {
Phil Weaver106fe732016-11-22 18:18:39 -0800303 return null;
304 }
305 AccessibilityManager accessibilityManager =
306 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
307 return accessibilityManager.getInstalledServiceInfoWithComponentName(
Rhed Jao6dad25d2019-10-22 22:15:05 +0800308 targetComponentName);
Phil Weaver106fe732016-11-22 18:18:39 -0800309 }
310
Phil Weaverb9f06122017-11-30 10:48:17 -0800311 private String getShortcutFeatureDescription(boolean includeSummary) {
Rhed Jao6dad25d2019-10-22 22:15:05 +0800312 final ComponentName targetComponentName = getShortcutTargetComponentName();
313 if (targetComponentName == null) {
Phil Weaverb9f06122017-11-30 10:48:17 -0800314 return null;
315 }
Phil Weaverb9f06122017-11-30 10:48:17 -0800316 final ToggleableFrameworkFeatureInfo frameworkFeatureInfo =
317 getFrameworkShortcutFeaturesMap().get(targetComponentName);
318 if (frameworkFeatureInfo != null) {
319 return frameworkFeatureInfo.getLabel(mContext);
320 }
321 final AccessibilityServiceInfo serviceInfo = mFrameworkObjectProvider
322 .getAccessibilityManagerInstance(mContext).getInstalledServiceInfoWithComponentName(
323 targetComponentName);
324 if (serviceInfo == null) {
325 return null;
326 }
327 final PackageManager pm = mContext.getPackageManager();
328 String label = serviceInfo.getResolveInfo().loadLabel(pm).toString();
Phil Weaver0f9aa4c2018-02-26 13:17:28 -0800329 CharSequence summary = serviceInfo.loadSummary(pm);
Phil Weaverb9f06122017-11-30 10:48:17 -0800330 if (!includeSummary || TextUtils.isEmpty(summary)) {
331 return label;
332 }
333 return String.format("%s\n%s", label, summary);
334 }
335
Phil Weaver106fe732016-11-22 18:18:39 -0800336 private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
337 AccessibilityManager accessibilityManager =
338 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
339 return accessibilityManager.getEnabledAccessibilityServiceList(
340 AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
341 }
342
Zhen Sun62e54c22017-08-17 15:56:20 -0700343 private boolean hasFeatureLeanback() {
344 return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
345 }
346
Rhed Jao6182af72018-08-22 18:48:59 +0800347 private void playNotificationTone() {
348 // Use USAGE_ASSISTANCE_ACCESSIBILITY for TVs to ensure that TVs play the ringtone as they
349 // have less ways of providing feedback like vibration.
350 final int audioAttributesUsage = hasFeatureLeanback()
351 ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
352 : AudioAttributes.USAGE_NOTIFICATION_EVENT;
353
354 // Play a notification tone
355 final Ringtone tone = mFrameworkObjectProvider.getRingtone(mContext,
356 Settings.System.DEFAULT_NOTIFICATION_URI);
357 if (tone != null) {
358 tone.setAudioAttributes(new AudioAttributes.Builder()
359 .setUsage(audioAttributesUsage)
360 .build());
361 tone.play();
362 }
363 }
364
365 private boolean performTtsPrompt(AlertDialog alertDialog) {
Rhed Jao5d9618b2018-12-24 18:59:13 +0800366 final String serviceName = getShortcutFeatureDescription(false /* no summary */);
Rhed Jao6182af72018-08-22 18:48:59 +0800367 final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
Rhed Jao5d9618b2018-12-24 18:59:13 +0800368 if (TextUtils.isEmpty(serviceName) || serviceInfo == null) {
Rhed Jao6182af72018-08-22 18:48:59 +0800369 return false;
370 }
371 if ((serviceInfo.flags & AccessibilityServiceInfo
372 .FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK) == 0) {
373 return false;
374 }
Rhed Jao5d9618b2018-12-24 18:59:13 +0800375 final TtsPrompt tts = new TtsPrompt(serviceName);
Rhed Jao6182af72018-08-22 18:48:59 +0800376 alertDialog.setOnDismissListener(dialog -> tts.dismiss());
377 return true;
378 }
379
380 /**
Rhed Jao6dad25d2019-10-22 22:15:05 +0800381 * Returns {@code true} if any shortcut targets were assigned to accessibility shortcut key.
382 */
383 private boolean hasShortcutTarget() {
384 // AccessibilityShortcutController is initialized earlier than AccessibilityManagerService.
385 // AccessibilityManager#getAccessibilityShortcutTargets may not return correct shortcut
386 // targets during boot. Needs to read settings directly here.
387 String shortcutTargets = Settings.Secure.getStringForUser(mContext.getContentResolver(),
388 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, mUserId);
Rhed Jaof3fbcf52020-01-15 21:44:22 +0800389 // A11y warning dialog updates settings to empty string, when user disables a11y shortcut.
390 // Only fallback to default a11y service, when setting is never updated.
391 if (shortcutTargets == null) {
Rhed Jao6dad25d2019-10-22 22:15:05 +0800392 shortcutTargets = mContext.getString(R.string.config_defaultAccessibilityService);
393 }
394 return !TextUtils.isEmpty(shortcutTargets);
395 }
396
397 /**
398 * Gets the component name of the shortcut target.
399 *
400 * @return The component name, or null if it's assigned by multiple targets.
401 */
402 private ComponentName getShortcutTargetComponentName() {
403 final List<String> shortcutTargets = mFrameworkObjectProvider
404 .getAccessibilityManagerInstance(mContext)
405 .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY);
406 if (shortcutTargets.size() != 1) {
407 return null;
408 }
409 return ComponentName.unflattenFromString(shortcutTargets.get(0));
410 }
411
412 /**
Rhed Jao6182af72018-08-22 18:48:59 +0800413 * Class to wrap TextToSpeech for shortcut dialog spoken feedback.
414 */
415 private class TtsPrompt implements TextToSpeech.OnInitListener {
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800416 private static final int RETRY_MILLIS = 1000;
417
Rhed Jao6182af72018-08-22 18:48:59 +0800418 private final CharSequence mText;
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800419
420 private int mRetryCount = 3;
Rhed Jao6182af72018-08-22 18:48:59 +0800421 private boolean mDismiss;
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800422 private boolean mLanguageReady = false;
Rhed Jao6182af72018-08-22 18:48:59 +0800423 private TextToSpeech mTts;
424
Rhed Jao5d9618b2018-12-24 18:59:13 +0800425 TtsPrompt(String serviceName) {
426 mText = mContext.getString(R.string.accessibility_shortcut_spoken_feedback,
427 serviceName);
Rhed Jao6182af72018-08-22 18:48:59 +0800428 mTts = mFrameworkObjectProvider.getTextToSpeech(mContext, this);
429 }
430
431 /**
432 * Releases the resources used by the TextToSpeech, when dialog dismiss.
433 */
434 public void dismiss() {
435 mDismiss = true;
436 mHandler.sendMessage(PooledLambda.obtainMessage(TextToSpeech::shutdown, mTts));
437 }
438
439 @Override
440 public void onInit(int status) {
441 if (status != TextToSpeech.SUCCESS) {
442 Slog.d(TAG, "Tts init fail, status=" + Integer.toString(status));
443 playNotificationTone();
444 return;
445 }
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800446 mHandler.sendMessage(PooledLambda.obtainMessage(
447 TtsPrompt::waitForTtsReady, this));
Rhed Jao6182af72018-08-22 18:48:59 +0800448 }
449
450 private void play() {
451 if (mDismiss) {
452 return;
453 }
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800454 final int status = mTts.speak(mText, TextToSpeech.QUEUE_FLUSH, null, null);
Rhed Jao6182af72018-08-22 18:48:59 +0800455 if (status != TextToSpeech.SUCCESS) {
456 Slog.d(TAG, "Tts play fail");
457 playNotificationTone();
458 }
459 }
460
461 /**
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800462 * Waiting for tts is ready to speak. Trying again if tts language pack is not available
463 * or tts voice data is not installed yet.
Rhed Jao6182af72018-08-22 18:48:59 +0800464 */
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800465 private void waitForTtsReady() {
466 if (mDismiss) {
467 return;
Rhed Jao6182af72018-08-22 18:48:59 +0800468 }
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800469 if (!mLanguageReady) {
470 final int status = mTts.setLanguage(Locale.getDefault());
471 // True if language is available and TTS#loadVoice has called once
472 // that trigger TTS service to start initialization.
473 mLanguageReady = status != TextToSpeech.LANG_MISSING_DATA
474 && status != TextToSpeech.LANG_NOT_SUPPORTED;
Rhed Jao6182af72018-08-22 18:48:59 +0800475 }
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800476 if (mLanguageReady) {
477 final Voice voice = mTts.getVoice();
478 final boolean voiceDataInstalled = voice != null
479 && voice.getFeatures() != null
480 && !voice.getFeatures().contains(
481 TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED);
482 if (voiceDataInstalled) {
483 mHandler.sendMessage(PooledLambda.obtainMessage(
484 TtsPrompt::play, this));
485 return;
486 }
487 }
488
489 if (mRetryCount == 0) {
490 Slog.d(TAG, "Tts not ready to speak.");
491 playNotificationTone();
492 return;
493 }
494 // Retry if TTS service not ready yet.
495 mRetryCount -= 1;
496 mHandler.sendMessageDelayed(PooledLambda.obtainMessage(
497 TtsPrompt::waitForTtsReady, this), RETRY_MILLIS);
Rhed Jao6182af72018-08-22 18:48:59 +0800498 }
499 }
500
Phil Weaverb9f06122017-11-30 10:48:17 -0800501 /**
502 * Immutable class to hold info about framework features that can be controlled by shortcut
503 */
504 public static class ToggleableFrameworkFeatureInfo {
505 private final String mSettingKey;
506 private final String mSettingOnValue;
507 private final String mSettingOffValue;
508 private final int mLabelStringResourceId;
509 // These go to the settings wrapper
510 private int mIconDrawableId;
511
512 ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue,
513 String settingOffValue, int labelStringResourceId) {
514 mSettingKey = settingKey;
515 mSettingOnValue = settingOnValue;
516 mSettingOffValue = settingOffValue;
517 mLabelStringResourceId = labelStringResourceId;
518 }
519
520 /**
521 * @return The settings key to toggle between two values
522 */
523 public String getSettingKey() {
524 return mSettingKey;
525 }
526
527 /**
528 * @return The value to write to settings to turn the feature on
529 */
530 public String getSettingOnValue() {
531 return mSettingOnValue;
532 }
533
534 /**
535 * @return The value to write to settings to turn the feature off
536 */
537 public String getSettingOffValue() {
538 return mSettingOffValue;
539 }
540
541 public String getLabel(Context context) {
542 return context.getString(mLabelStringResourceId);
543 }
544 }
545
Phil Weaver106fe732016-11-22 18:18:39 -0800546 // Class to allow mocking of static framework calls
547 public static class FrameworkObjectProvider {
548 public AccessibilityManager getAccessibilityManagerInstance(Context context) {
549 return AccessibilityManager.getInstance(context);
550 }
551
552 public AlertDialog.Builder getAlertDialogBuilder(Context context) {
553 return new AlertDialog.Builder(context);
554 }
555
556 public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
557 return Toast.makeText(context, charSequence, duration);
558 }
Phil Weaverb9f06122017-11-30 10:48:17 -0800559
560 public Context getSystemUiContext() {
561 return ActivityThread.currentActivityThread().getSystemUiContext();
562 }
Rhed Jao6182af72018-08-22 18:48:59 +0800563
564 /**
565 * @param ctx A context for TextToSpeech
566 * @param listener TextToSpeech initialization callback
567 * @return TextToSpeech instance
568 */
569 public TextToSpeech getTextToSpeech(Context ctx, TextToSpeech.OnInitListener listener) {
570 return new TextToSpeech(ctx, listener);
571 }
572
573 /**
574 * @param ctx context for ringtone
575 * @param uri ringtone uri
576 * @return Ringtone instance
577 */
578 public Ringtone getRingtone(Context ctx, Uri uri) {
579 return RingtoneManager.getRingtone(ctx, uri);
580 }
Phil Weaver106fe732016-11-22 18:18:39 -0800581 }
582}