1 /* 2 * Copyright (C) 2012 Google Inc. 3 * 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 17 package com.android.server.policy; 18 19 import android.accessibilityservice.AccessibilityServiceInfo; 20 import android.app.ActivityManager; 21 import android.app.AlertDialog; 22 import android.content.ComponentName; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.database.ContentObserver; 27 import android.media.AudioAttributes; 28 import android.media.Ringtone; 29 import android.media.RingtoneManager; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.os.UserHandle; 33 import android.os.Vibrator; 34 import android.provider.Settings; 35 import android.text.TextUtils; 36 import android.util.Slog; 37 import android.view.Window; 38 import android.view.WindowManager; 39 import android.view.accessibility.AccessibilityManager; 40 41 import android.widget.Toast; 42 import com.android.internal.R; 43 44 import java.util.List; 45 46 import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; 47 48 /** 49 * Class to help manage the accessibility shortcut 50 */ 51 public class AccessibilityShortcutController { 52 private static final String TAG = "AccessibilityShortcutController"; 53 private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() 54 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 55 .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY) 56 .build(); 57 58 59 private final Context mContext; 60 private AlertDialog mAlertDialog; 61 private boolean mIsShortcutEnabled; 62 private boolean mEnabledOnLockScreen; 63 private int mUserId; 64 65 // Visible for testing 66 public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider(); 67 68 public static String getTargetServiceComponentNameString( 69 Context context, int userId) { 70 final String currentShortcutServiceId = Settings.Secure.getStringForUser( 71 context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, 72 userId); 73 if (currentShortcutServiceId != null) { 74 return currentShortcutServiceId; 75 } 76 return context.getString(R.string.config_defaultAccessibilityService); 77 } 78 79 public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) { 80 mContext = context; 81 82 // Keep track of state of shortcut settings 83 final ContentObserver co = new ContentObserver(handler) { 84 @Override 85 public void onChange(boolean selfChange, Uri uri, int userId) { 86 if (userId == mUserId) { 87 onSettingsChanged(); 88 } 89 } 90 }; 91 mContext.getContentResolver().registerContentObserver( 92 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE), 93 false, co, UserHandle.USER_ALL); 94 mContext.getContentResolver().registerContentObserver( 95 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED), 96 false, co, UserHandle.USER_ALL); 97 mContext.getContentResolver().registerContentObserver( 98 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN), 99 false, co, UserHandle.USER_ALL); 100 setCurrentUser(mUserId); 101 } 102 103 public void setCurrentUser(int currentUserId) { 104 mUserId = currentUserId; 105 onSettingsChanged(); 106 } 107 108 /** 109 * Check if the shortcut is available. 110 * 111 * @param onLockScreen Whether or not the phone is currently locked. 112 * 113 * @return {@code true} if the shortcut is available 114 */ 115 public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) { 116 return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen); 117 } 118 119 public void onSettingsChanged() { 120 final boolean haveValidService = 121 !TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId)); 122 final ContentResolver cr = mContext.getContentResolver(); 123 final boolean enabled = Settings.Secure.getIntForUser( 124 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1; 125 mEnabledOnLockScreen = Settings.Secure.getIntForUser( 126 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN, 0, mUserId) == 1; 127 mIsShortcutEnabled = enabled && haveValidService; 128 } 129 130 /** 131 * Called when the accessibility shortcut is activated 132 */ 133 public void performAccessibilityShortcut() { 134 Slog.d(TAG, "Accessibility shortcut activated"); 135 final ContentResolver cr = mContext.getContentResolver(); 136 final int userId = ActivityManager.getCurrentUser(); 137 final int dialogAlreadyShown = Settings.Secure.getIntForUser( 138 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId); 139 140 // Play a notification tone 141 final Ringtone tone = 142 RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI); 143 if (tone != null) { 144 tone.setAudioAttributes(new AudioAttributes.Builder() 145 .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT) 146 .build()); 147 tone.play(); 148 } 149 150 // Play a notification vibration 151 Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); 152 if ((vibrator != null) && vibrator.hasVibrator()) { 153 // Don't check if haptics are disabled, as we need to alert the user that their 154 // way of interacting with the phone may change if they activate the shortcut 155 long[] vibePattern = PhoneWindowManager.getLongIntArray(mContext.getResources(), 156 R.array.config_safeModeDisabledVibePattern); 157 vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES); 158 } 159 160 161 if (dialogAlreadyShown == 0) { 162 // The first time, we show a warning rather than toggle the service to give the user a 163 // chance to turn off this feature before stuff gets enabled. 164 mAlertDialog = createShortcutWarningDialog(userId); 165 if (mAlertDialog == null) { 166 return; 167 } 168 Window w = mAlertDialog.getWindow(); 169 WindowManager.LayoutParams attr = w.getAttributes(); 170 attr.type = TYPE_KEYGUARD_DIALOG; 171 w.setAttributes(attr); 172 mAlertDialog.show(); 173 Settings.Secure.putIntForUser( 174 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1, userId); 175 } else { 176 if (mAlertDialog != null) { 177 mAlertDialog.dismiss(); 178 mAlertDialog = null; 179 } 180 181 // Show a toast alerting the user to what's happening 182 final AccessibilityServiceInfo serviceInfo = getInfoForTargetService(); 183 if (serviceInfo == null) { 184 Slog.e(TAG, "Accessibility shortcut set to invalid service"); 185 return; 186 } 187 String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo) 188 ? R.string.accessibility_shortcut_disabling_service 189 : R.string.accessibility_shortcut_enabling_service); 190 String toastMessage = String.format(toastMessageFormatString, 191 serviceInfo.getResolveInfo() 192 .loadLabel(mContext.getPackageManager()).toString()); 193 Toast warningToast = mFrameworkObjectProvider.makeToastFromText( 194 mContext, toastMessage, Toast.LENGTH_LONG); 195 warningToast.getWindowParams().privateFlags |= 196 WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; 197 warningToast.show(); 198 199 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext) 200 .performAccessibilityShortcut(); 201 } 202 } 203 204 private AlertDialog createShortcutWarningDialog(int userId) { 205 final AccessibilityServiceInfo serviceInfo = getInfoForTargetService(); 206 207 if (serviceInfo == null) { 208 return null; 209 } 210 211 final String warningMessage = String.format( 212 mContext.getString(R.string.accessibility_shortcut_toogle_warning), 213 serviceInfo.getResolveInfo().loadLabel(mContext.getPackageManager()).toString()); 214 final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(mContext) 215 .setTitle(R.string.accessibility_shortcut_warning_dialog_title) 216 .setMessage(warningMessage) 217 .setCancelable(false) 218 .setPositiveButton(R.string.leave_accessibility_shortcut_on, null) 219 .setNegativeButton(R.string.disable_accessibility_shortcut, 220 (DialogInterface d, int which) -> { 221 Settings.Secure.putStringForUser(mContext.getContentResolver(), 222 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "", 223 userId); 224 }) 225 .setOnCancelListener((DialogInterface d) -> { 226 // If canceled, treat as if the dialog has never been shown 227 Settings.Secure.putIntForUser(mContext.getContentResolver(), 228 Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId); 229 }) 230 .create(); 231 return alertDialog; 232 } 233 234 private AccessibilityServiceInfo getInfoForTargetService() { 235 final String currentShortcutServiceString = getTargetServiceComponentNameString( 236 mContext, UserHandle.USER_CURRENT); 237 if (currentShortcutServiceString == null) { 238 return null; 239 } 240 AccessibilityManager accessibilityManager = 241 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext); 242 return accessibilityManager.getInstalledServiceInfoWithComponentName( 243 ComponentName.unflattenFromString(currentShortcutServiceString)); 244 } 245 246 private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) { 247 AccessibilityManager accessibilityManager = 248 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext); 249 return accessibilityManager.getEnabledAccessibilityServiceList( 250 AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo); 251 } 252 253 // Class to allow mocking of static framework calls 254 public static class FrameworkObjectProvider { 255 public AccessibilityManager getAccessibilityManagerInstance(Context context) { 256 return AccessibilityManager.getInstance(context); 257 } 258 259 public AlertDialog.Builder getAlertDialogBuilder(Context context) { 260 return new AlertDialog.Builder(context); 261 } 262 263 public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) { 264 return Toast.makeText(context, charSequence, duration); 265 } 266 } 267 } 268