blob: be66d0c238cc6f20cd0e437178afadc84f5b307c [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(
Phil Weaverce687c52017-03-15 08:51:52 -0700147 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN),
148 false, co, UserHandle.USER_ALL);
Phil Weaverc5865d62018-03-02 16:00:43 -0800149 mContext.getContentResolver().registerContentObserver(
150 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN),
151 false, co, UserHandle.USER_ALL);
Phil Weaverce687c52017-03-15 08:51:52 -0700152 setCurrentUser(mUserId);
Phil Weaver106fe732016-11-22 18:18:39 -0800153 }
154
Phil Weaverce687c52017-03-15 08:51:52 -0700155 public void setCurrentUser(int currentUserId) {
156 mUserId = currentUserId;
157 onSettingsChanged();
158 }
159
160 /**
161 * Check if the shortcut is available.
162 *
Rhed Jao6dad25d2019-10-22 22:15:05 +0800163 * @param phoneLocked Whether or not the phone is currently locked.
Phil Weaverce687c52017-03-15 08:51:52 -0700164 *
165 * @return {@code true} if the shortcut is available
166 */
167 public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) {
168 return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen);
Phil Weaver106fe732016-11-22 18:18:39 -0800169 }
170
171 public void onSettingsChanged() {
Rhed Jao6dad25d2019-10-22 22:15:05 +0800172 final boolean hasShortcutTarget = hasShortcutTarget();
Phil Weaverce687c52017-03-15 08:51:52 -0700173 final ContentResolver cr = mContext.getContentResolver();
Phil Weaverc5865d62018-03-02 16:00:43 -0800174 // Enable the shortcut from the lockscreen by default if the dialog has been shown
175 final int dialogAlreadyShown = Settings.Secure.getIntForUser(
menghanlie83358b2020-02-11 16:47:18 +0800176 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStaus.NOT_SHOWN,
177 mUserId);
Phil Weaverce687c52017-03-15 08:51:52 -0700178 mEnabledOnLockScreen = Settings.Secure.getIntForUser(
Phil Weaverc5865d62018-03-02 16:00:43 -0800179 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
180 dialogAlreadyShown, mUserId) == 1;
menghanli390f8f92020-03-11 20:52:47 +0800181 mIsShortcutEnabled = hasShortcutTarget;
Phil Weaver106fe732016-11-22 18:18:39 -0800182 }
183
184 /**
185 * Called when the accessibility shortcut is activated
186 */
187 public void performAccessibilityShortcut() {
188 Slog.d(TAG, "Accessibility shortcut activated");
189 final ContentResolver cr = mContext.getContentResolver();
190 final int userId = ActivityManager.getCurrentUser();
191 final int dialogAlreadyShown = Settings.Secure.getIntForUser(
menghanlie83358b2020-02-11 16:47:18 +0800192 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStaus.NOT_SHOWN,
193 userId);
Phil Weaver32ea3722017-03-13 11:32:01 -0700194 // Play a notification vibration
195 Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
196 if ((vibrator != null) && vibrator.hasVibrator()) {
197 // Don't check if haptics are disabled, as we need to alert the user that their
198 // way of interacting with the phone may change if they activate the shortcut
Phil Weaverb9f06122017-11-30 10:48:17 -0800199 long[] vibePattern = convertToLongArray(
200 mContext.getResources().getIntArray(R.array.config_longPressVibePattern));
Phil Weaver32ea3722017-03-13 11:32:01 -0700201 vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
202 }
203
Phil Weaver106fe732016-11-22 18:18:39 -0800204 if (dialogAlreadyShown == 0) {
205 // The first time, we show a warning rather than toggle the service to give the user a
206 // chance to turn off this feature before stuff gets enabled.
207 mAlertDialog = createShortcutWarningDialog(userId);
208 if (mAlertDialog == null) {
209 return;
210 }
Rhed Jao6182af72018-08-22 18:48:59 +0800211 if (!performTtsPrompt(mAlertDialog)) {
212 playNotificationTone();
213 }
Phil Weaver106fe732016-11-22 18:18:39 -0800214 Window w = mAlertDialog.getWindow();
215 WindowManager.LayoutParams attr = w.getAttributes();
216 attr.type = TYPE_KEYGUARD_DIALOG;
217 w.setAttributes(attr);
218 mAlertDialog.show();
219 Settings.Secure.putIntForUser(
menghanlie83358b2020-02-11 16:47:18 +0800220 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStaus.SHOWN,
221 userId);
Phil Weaver106fe732016-11-22 18:18:39 -0800222 } else {
Rhed Jao6182af72018-08-22 18:48:59 +0800223 playNotificationTone();
Phil Weaver106fe732016-11-22 18:18:39 -0800224 if (mAlertDialog != null) {
225 mAlertDialog.dismiss();
226 mAlertDialog = null;
227 }
Rhed Jao6dad25d2019-10-22 22:15:05 +0800228 showToast();
Phil Weaver106fe732016-11-22 18:18:39 -0800229 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
230 .performAccessibilityShortcut();
231 }
232 }
233
Rhed Jao6dad25d2019-10-22 22:15:05 +0800234 /**
Rhed Jao8fcba032020-04-15 23:06:08 +0800235 * Show toast to alert the user that the accessibility shortcut turned on or off an
236 * accessibility service.
Rhed Jao6dad25d2019-10-22 22:15:05 +0800237 */
238 private void showToast() {
239 final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
240 if (serviceInfo == null) {
241 return;
242 }
243 final String serviceName = getShortcutFeatureDescription(/* no summary */ false);
244 if (serviceName == null) {
245 return;
246 }
247 final boolean requestA11yButton = (serviceInfo.flags
248 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
Rhed Jao8fcba032020-04-15 23:06:08 +0800249 final boolean isServiceEnabled = isServiceEnabled(serviceInfo);
250 if (serviceInfo.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion
251 > Build.VERSION_CODES.Q && requestA11yButton && isServiceEnabled) {
252 // An accessibility button callback is sent to the target accessibility service.
253 // No need to show up a toast in this case.
Rhed Jao6dad25d2019-10-22 22:15:05 +0800254 return;
255 }
256 // For accessibility services, show a toast explaining what we're doing.
Rhed Jao8fcba032020-04-15 23:06:08 +0800257 String toastMessageFormatString = mContext.getString(isServiceEnabled
Rhed Jao6dad25d2019-10-22 22:15:05 +0800258 ? R.string.accessibility_shortcut_disabling_service
259 : R.string.accessibility_shortcut_enabling_service);
260 String toastMessage = String.format(toastMessageFormatString, serviceName);
261 Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
262 mContext, toastMessage, Toast.LENGTH_LONG);
Rhed Jao6dad25d2019-10-22 22:15:05 +0800263 warningToast.show();
264 }
265
Phil Weaver106fe732016-11-22 18:18:39 -0800266 private AlertDialog createShortcutWarningDialog(int userId) {
menghanlie83358b2020-02-11 16:47:18 +0800267 final String warningMessage = mContext.getString(
268 R.string.accessibility_shortcut_toogle_warning);
Phil Weaver8c58d452017-07-28 12:58:15 -0700269 final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(
270 // Use SystemUI context so we pick up any theme set in a vendor overlay
Phil Weaverb9f06122017-11-30 10:48:17 -0800271 mFrameworkObjectProvider.getSystemUiContext())
Phil Weaver106fe732016-11-22 18:18:39 -0800272 .setTitle(R.string.accessibility_shortcut_warning_dialog_title)
273 .setMessage(warningMessage)
274 .setCancelable(false)
275 .setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
276 .setNegativeButton(R.string.disable_accessibility_shortcut,
277 (DialogInterface d, int which) -> {
278 Settings.Secure.putStringForUser(mContext.getContentResolver(),
279 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
280 userId);
menghanlie83358b2020-02-11 16:47:18 +0800281
282 // If canceled, treat as if the dialog has never been shown
283 Settings.Secure.putIntForUser(mContext.getContentResolver(),
284 Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
285 DialogStaus.NOT_SHOWN, userId);
Phil Weaver106fe732016-11-22 18:18:39 -0800286 })
287 .setOnCancelListener((DialogInterface d) -> {
288 // If canceled, treat as if the dialog has never been shown
289 Settings.Secure.putIntForUser(mContext.getContentResolver(),
menghanlie83358b2020-02-11 16:47:18 +0800290 Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
291 DialogStaus.NOT_SHOWN, userId);
Phil Weaver106fe732016-11-22 18:18:39 -0800292 })
293 .create();
294 return alertDialog;
295 }
296
297 private AccessibilityServiceInfo getInfoForTargetService() {
Rhed Jao6dad25d2019-10-22 22:15:05 +0800298 final ComponentName targetComponentName = getShortcutTargetComponentName();
299 if (targetComponentName == null) {
Phil Weaver106fe732016-11-22 18:18:39 -0800300 return null;
301 }
302 AccessibilityManager accessibilityManager =
303 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
304 return accessibilityManager.getInstalledServiceInfoWithComponentName(
Rhed Jao6dad25d2019-10-22 22:15:05 +0800305 targetComponentName);
Phil Weaver106fe732016-11-22 18:18:39 -0800306 }
307
Phil Weaverb9f06122017-11-30 10:48:17 -0800308 private String getShortcutFeatureDescription(boolean includeSummary) {
Rhed Jao6dad25d2019-10-22 22:15:05 +0800309 final ComponentName targetComponentName = getShortcutTargetComponentName();
310 if (targetComponentName == null) {
Phil Weaverb9f06122017-11-30 10:48:17 -0800311 return null;
312 }
Phil Weaverb9f06122017-11-30 10:48:17 -0800313 final ToggleableFrameworkFeatureInfo frameworkFeatureInfo =
314 getFrameworkShortcutFeaturesMap().get(targetComponentName);
315 if (frameworkFeatureInfo != null) {
316 return frameworkFeatureInfo.getLabel(mContext);
317 }
318 final AccessibilityServiceInfo serviceInfo = mFrameworkObjectProvider
319 .getAccessibilityManagerInstance(mContext).getInstalledServiceInfoWithComponentName(
320 targetComponentName);
321 if (serviceInfo == null) {
322 return null;
323 }
324 final PackageManager pm = mContext.getPackageManager();
325 String label = serviceInfo.getResolveInfo().loadLabel(pm).toString();
Phil Weaver0f9aa4c2018-02-26 13:17:28 -0800326 CharSequence summary = serviceInfo.loadSummary(pm);
Phil Weaverb9f06122017-11-30 10:48:17 -0800327 if (!includeSummary || TextUtils.isEmpty(summary)) {
328 return label;
329 }
330 return String.format("%s\n%s", label, summary);
331 }
332
Phil Weaver106fe732016-11-22 18:18:39 -0800333 private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
334 AccessibilityManager accessibilityManager =
335 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
336 return accessibilityManager.getEnabledAccessibilityServiceList(
337 AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
338 }
339
Zhen Sun62e54c22017-08-17 15:56:20 -0700340 private boolean hasFeatureLeanback() {
341 return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
342 }
343
Rhed Jao6182af72018-08-22 18:48:59 +0800344 private void playNotificationTone() {
345 // Use USAGE_ASSISTANCE_ACCESSIBILITY for TVs to ensure that TVs play the ringtone as they
346 // have less ways of providing feedback like vibration.
347 final int audioAttributesUsage = hasFeatureLeanback()
348 ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
349 : AudioAttributes.USAGE_NOTIFICATION_EVENT;
350
351 // Play a notification tone
352 final Ringtone tone = mFrameworkObjectProvider.getRingtone(mContext,
353 Settings.System.DEFAULT_NOTIFICATION_URI);
354 if (tone != null) {
355 tone.setAudioAttributes(new AudioAttributes.Builder()
356 .setUsage(audioAttributesUsage)
357 .build());
358 tone.play();
359 }
360 }
361
362 private boolean performTtsPrompt(AlertDialog alertDialog) {
Rhed Jao5d9618b2018-12-24 18:59:13 +0800363 final String serviceName = getShortcutFeatureDescription(false /* no summary */);
Rhed Jao6182af72018-08-22 18:48:59 +0800364 final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
Rhed Jao5d9618b2018-12-24 18:59:13 +0800365 if (TextUtils.isEmpty(serviceName) || serviceInfo == null) {
Rhed Jao6182af72018-08-22 18:48:59 +0800366 return false;
367 }
368 if ((serviceInfo.flags & AccessibilityServiceInfo
369 .FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK) == 0) {
370 return false;
371 }
Rhed Jao5d9618b2018-12-24 18:59:13 +0800372 final TtsPrompt tts = new TtsPrompt(serviceName);
Rhed Jao6182af72018-08-22 18:48:59 +0800373 alertDialog.setOnDismissListener(dialog -> tts.dismiss());
374 return true;
375 }
376
377 /**
Rhed Jao6dad25d2019-10-22 22:15:05 +0800378 * Returns {@code true} if any shortcut targets were assigned to accessibility shortcut key.
379 */
380 private boolean hasShortcutTarget() {
381 // AccessibilityShortcutController is initialized earlier than AccessibilityManagerService.
382 // AccessibilityManager#getAccessibilityShortcutTargets may not return correct shortcut
383 // targets during boot. Needs to read settings directly here.
384 String shortcutTargets = Settings.Secure.getStringForUser(mContext.getContentResolver(),
385 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, mUserId);
Rhed Jaof3fbcf52020-01-15 21:44:22 +0800386 // A11y warning dialog updates settings to empty string, when user disables a11y shortcut.
387 // Only fallback to default a11y service, when setting is never updated.
388 if (shortcutTargets == null) {
Rhed Jao6dad25d2019-10-22 22:15:05 +0800389 shortcutTargets = mContext.getString(R.string.config_defaultAccessibilityService);
390 }
391 return !TextUtils.isEmpty(shortcutTargets);
392 }
393
394 /**
395 * Gets the component name of the shortcut target.
396 *
397 * @return The component name, or null if it's assigned by multiple targets.
398 */
399 private ComponentName getShortcutTargetComponentName() {
400 final List<String> shortcutTargets = mFrameworkObjectProvider
401 .getAccessibilityManagerInstance(mContext)
402 .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY);
403 if (shortcutTargets.size() != 1) {
404 return null;
405 }
406 return ComponentName.unflattenFromString(shortcutTargets.get(0));
407 }
408
409 /**
Rhed Jao6182af72018-08-22 18:48:59 +0800410 * Class to wrap TextToSpeech for shortcut dialog spoken feedback.
411 */
412 private class TtsPrompt implements TextToSpeech.OnInitListener {
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800413 private static final int RETRY_MILLIS = 1000;
414
Rhed Jao6182af72018-08-22 18:48:59 +0800415 private final CharSequence mText;
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800416
417 private int mRetryCount = 3;
Rhed Jao6182af72018-08-22 18:48:59 +0800418 private boolean mDismiss;
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800419 private boolean mLanguageReady = false;
Rhed Jao6182af72018-08-22 18:48:59 +0800420 private TextToSpeech mTts;
421
Rhed Jao5d9618b2018-12-24 18:59:13 +0800422 TtsPrompt(String serviceName) {
423 mText = mContext.getString(R.string.accessibility_shortcut_spoken_feedback,
424 serviceName);
Rhed Jao6182af72018-08-22 18:48:59 +0800425 mTts = mFrameworkObjectProvider.getTextToSpeech(mContext, this);
426 }
427
428 /**
429 * Releases the resources used by the TextToSpeech, when dialog dismiss.
430 */
431 public void dismiss() {
432 mDismiss = true;
433 mHandler.sendMessage(PooledLambda.obtainMessage(TextToSpeech::shutdown, mTts));
434 }
435
436 @Override
437 public void onInit(int status) {
438 if (status != TextToSpeech.SUCCESS) {
439 Slog.d(TAG, "Tts init fail, status=" + Integer.toString(status));
440 playNotificationTone();
441 return;
442 }
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800443 mHandler.sendMessage(PooledLambda.obtainMessage(
444 TtsPrompt::waitForTtsReady, this));
Rhed Jao6182af72018-08-22 18:48:59 +0800445 }
446
447 private void play() {
448 if (mDismiss) {
449 return;
450 }
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800451 final int status = mTts.speak(mText, TextToSpeech.QUEUE_FLUSH, null, null);
Rhed Jao6182af72018-08-22 18:48:59 +0800452 if (status != TextToSpeech.SUCCESS) {
453 Slog.d(TAG, "Tts play fail");
454 playNotificationTone();
455 }
456 }
457
458 /**
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800459 * Waiting for tts is ready to speak. Trying again if tts language pack is not available
460 * or tts voice data is not installed yet.
Rhed Jao6182af72018-08-22 18:48:59 +0800461 */
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800462 private void waitForTtsReady() {
463 if (mDismiss) {
464 return;
Rhed Jao6182af72018-08-22 18:48:59 +0800465 }
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800466 if (!mLanguageReady) {
467 final int status = mTts.setLanguage(Locale.getDefault());
468 // True if language is available and TTS#loadVoice has called once
469 // that trigger TTS service to start initialization.
470 mLanguageReady = status != TextToSpeech.LANG_MISSING_DATA
471 && status != TextToSpeech.LANG_NOT_SUPPORTED;
Rhed Jao6182af72018-08-22 18:48:59 +0800472 }
Dieter Hsu0dbbb422020-02-12 14:52:59 +0800473 if (mLanguageReady) {
474 final Voice voice = mTts.getVoice();
475 final boolean voiceDataInstalled = voice != null
476 && voice.getFeatures() != null
477 && !voice.getFeatures().contains(
478 TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED);
479 if (voiceDataInstalled) {
480 mHandler.sendMessage(PooledLambda.obtainMessage(
481 TtsPrompt::play, this));
482 return;
483 }
484 }
485
486 if (mRetryCount == 0) {
487 Slog.d(TAG, "Tts not ready to speak.");
488 playNotificationTone();
489 return;
490 }
491 // Retry if TTS service not ready yet.
492 mRetryCount -= 1;
493 mHandler.sendMessageDelayed(PooledLambda.obtainMessage(
494 TtsPrompt::waitForTtsReady, this), RETRY_MILLIS);
Rhed Jao6182af72018-08-22 18:48:59 +0800495 }
496 }
497
Phil Weaverb9f06122017-11-30 10:48:17 -0800498 /**
499 * Immutable class to hold info about framework features that can be controlled by shortcut
500 */
501 public static class ToggleableFrameworkFeatureInfo {
502 private final String mSettingKey;
503 private final String mSettingOnValue;
504 private final String mSettingOffValue;
505 private final int mLabelStringResourceId;
506 // These go to the settings wrapper
507 private int mIconDrawableId;
508
509 ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue,
510 String settingOffValue, int labelStringResourceId) {
511 mSettingKey = settingKey;
512 mSettingOnValue = settingOnValue;
513 mSettingOffValue = settingOffValue;
514 mLabelStringResourceId = labelStringResourceId;
515 }
516
517 /**
518 * @return The settings key to toggle between two values
519 */
520 public String getSettingKey() {
521 return mSettingKey;
522 }
523
524 /**
525 * @return The value to write to settings to turn the feature on
526 */
527 public String getSettingOnValue() {
528 return mSettingOnValue;
529 }
530
531 /**
532 * @return The value to write to settings to turn the feature off
533 */
534 public String getSettingOffValue() {
535 return mSettingOffValue;
536 }
537
538 public String getLabel(Context context) {
539 return context.getString(mLabelStringResourceId);
540 }
541 }
542
Phil Weaver106fe732016-11-22 18:18:39 -0800543 // Class to allow mocking of static framework calls
544 public static class FrameworkObjectProvider {
545 public AccessibilityManager getAccessibilityManagerInstance(Context context) {
546 return AccessibilityManager.getInstance(context);
547 }
548
549 public AlertDialog.Builder getAlertDialogBuilder(Context context) {
550 return new AlertDialog.Builder(context);
551 }
552
553 public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
554 return Toast.makeText(context, charSequence, duration);
555 }
Phil Weaverb9f06122017-11-30 10:48:17 -0800556
557 public Context getSystemUiContext() {
558 return ActivityThread.currentActivityThread().getSystemUiContext();
559 }
Rhed Jao6182af72018-08-22 18:48:59 +0800560
561 /**
562 * @param ctx A context for TextToSpeech
563 * @param listener TextToSpeech initialization callback
564 * @return TextToSpeech instance
565 */
566 public TextToSpeech getTextToSpeech(Context ctx, TextToSpeech.OnInitListener listener) {
567 return new TextToSpeech(ctx, listener);
568 }
569
570 /**
571 * @param ctx context for ringtone
572 * @param uri ringtone uri
573 * @return Ringtone instance
574 */
575 public Ringtone getRingtone(Context ctx, Uri uri) {
576 return RingtoneManager.getRingtone(ctx, uri);
577 }
Phil Weaver106fe732016-11-22 18:18:39 -0800578 }
579}