1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.policy; 18 19 import android.accessibilityservice.AccessibilityServiceInfo; 20 import android.app.AlertDialog; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.ResolveInfo; 26 import android.content.res.Resources; 27 import android.os.Handler; 28 import android.os.Vibrator; 29 import android.provider.Settings; 30 import android.support.test.runner.AndroidJUnit4; 31 32 import android.test.mock.MockContentResolver; 33 import android.text.TextUtils; 34 import android.view.Window; 35 import android.view.WindowManager; 36 import android.view.accessibility.AccessibilityManager; 37 import android.view.accessibility.IAccessibilityManager; 38 import android.widget.Toast; 39 import com.android.internal.R; 40 import com.android.internal.util.test.FakeSettingsProvider; 41 import com.android.server.policy.AccessibilityShortcutController.FrameworkObjectProvider; 42 43 import org.junit.After; 44 import org.junit.Before; 45 import org.junit.Test; 46 import org.junit.runner.RunWith; 47 import org.mockito.ArgumentCaptor; 48 import org.mockito.Mock; 49 import org.mockito.MockitoAnnotations; 50 51 import java.lang.reflect.Field; 52 import java.util.Collections; 53 54 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN; 55 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED; 56 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN; 57 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; 58 import static junit.framework.Assert.assertEquals; 59 import static junit.framework.Assert.assertFalse; 60 import static junit.framework.Assert.assertTrue; 61 import static org.mockito.AdditionalMatchers.aryEq; 62 import static org.mockito.Matchers.anyBoolean; 63 import static org.mockito.Matchers.anyInt; 64 import static org.mockito.Matchers.anyObject; 65 import static org.mockito.Matchers.eq; 66 import static org.mockito.Mockito.mock; 67 import static org.mockito.Mockito.times; 68 import static org.mockito.Mockito.verify; 69 import static org.mockito.Mockito.verifyZeroInteractions; 70 import static org.mockito.Mockito.when; 71 72 @RunWith(AndroidJUnit4.class) 73 public class AccessibilityShortcutControllerTest { 74 private static final String SERVICE_NAME_STRING = "fake.package/fake.service.name"; 75 private static final long VIBRATOR_PATTERN_1 = 100L; 76 private static final long VIBRATOR_PATTERN_2 = 150L; 77 private static final int[] VIBRATOR_PATTERN_INT = {(int) VIBRATOR_PATTERN_1, 78 (int) VIBRATOR_PATTERN_2}; 79 private static final long[] VIBRATOR_PATTERN_LONG = {VIBRATOR_PATTERN_1, VIBRATOR_PATTERN_2}; 80 81 // Convenience values for enabling/disabling to make code more readable 82 private static final int DISABLED = 0; 83 private static final int ENABLED_EXCEPT_LOCK_SCREEN = 1; 84 private static final int ENABLED_INCLUDING_LOCK_SCREEN = 2; 85 private static final int DISABLED_BUT_LOCK_SCREEN_ON = 3; 86 87 private @Mock Context mContext; 88 private @Mock FrameworkObjectProvider mFrameworkObjectProvider; 89 private @Mock IAccessibilityManager mAccessibilityManagerService; 90 private @Mock Handler mHandler; 91 private @Mock AlertDialog.Builder mAlertDialogBuilder; 92 private @Mock AlertDialog mAlertDialog; 93 private @Mock AccessibilityServiceInfo mServiceInfo; 94 private @Mock Resources mResources; 95 private @Mock Toast mToast; 96 private @Mock Vibrator mVibrator; 97 private @Mock ApplicationInfo mApplicationInfo; 98 99 private MockContentResolver mContentResolver; 100 private WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams(); 101 102 @Before 103 public void setUp() throws Exception { 104 MockitoAnnotations.initMocks(this); 105 106 when(mVibrator.hasVibrator()).thenReturn(true); 107 108 when(mContext.getResources()).thenReturn(mResources); 109 when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo); 110 when(mContext.getSystemService(Context.VIBRATOR_SERVICE)).thenReturn(mVibrator); 111 112 mContentResolver = new MockContentResolver(mContext); 113 mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); 114 when(mContext.getContentResolver()).thenReturn(mContentResolver); 115 116 when(mAccessibilityManagerService.getInstalledAccessibilityServiceList(anyInt())) 117 .thenReturn(Collections.singletonList(mServiceInfo)); 118 119 // Use the extra level of indirection in the object to mock framework objects 120 AccessibilityManager accessibilityManager = 121 new AccessibilityManager(mHandler, mAccessibilityManagerService, 0); 122 when(mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)) 123 .thenReturn(accessibilityManager); 124 when(mFrameworkObjectProvider.getAlertDialogBuilder(mContext)) 125 .thenReturn(mAlertDialogBuilder); 126 when(mFrameworkObjectProvider.makeToastFromText(eq(mContext), anyObject(), anyInt())) 127 .thenReturn(mToast); 128 129 when(mResources.getString(anyInt())).thenReturn("Howdy %s"); 130 when(mResources.getIntArray(anyInt())).thenReturn(VIBRATOR_PATTERN_INT); 131 132 ResolveInfo resolveInfo = mock(ResolveInfo.class); 133 when(resolveInfo.loadLabel(anyObject())).thenReturn("Service name"); 134 when(mServiceInfo.getResolveInfo()).thenReturn(resolveInfo); 135 when(mServiceInfo.getComponentName()) 136 .thenReturn(ComponentName.unflattenFromString(SERVICE_NAME_STRING)); 137 138 when(mAlertDialogBuilder.setTitle(anyInt())).thenReturn(mAlertDialogBuilder); 139 when(mAlertDialogBuilder.setCancelable(anyBoolean())).thenReturn(mAlertDialogBuilder); 140 when(mAlertDialogBuilder.setMessage(anyObject())).thenReturn(mAlertDialogBuilder); 141 when(mAlertDialogBuilder.setPositiveButton(anyInt(), anyObject())) 142 .thenReturn(mAlertDialogBuilder); 143 when(mAlertDialogBuilder.setNegativeButton(anyInt(), anyObject())) 144 .thenReturn(mAlertDialogBuilder); 145 when(mAlertDialogBuilder.setOnCancelListener(anyObject())).thenReturn(mAlertDialogBuilder); 146 when(mAlertDialogBuilder.create()).thenReturn(mAlertDialog); 147 148 mLayoutParams.privateFlags = 0; 149 when(mToast.getWindowParams()).thenReturn(mLayoutParams); 150 151 Window window = mock(Window.class); 152 // Initialize the mWindowAttributes field which was not properly initialized during mock 153 // creation. 154 try { 155 Field field = Window.class.getDeclaredField("mWindowAttributes"); 156 field.setAccessible(true); 157 field.set(window, new WindowManager.LayoutParams()); 158 } catch (Exception e) { 159 throw new RuntimeException("Unable to set mWindowAttributes", e); 160 } 161 when(mAlertDialog.getWindow()).thenReturn(window); 162 } 163 164 @After 165 public void tearDown() { 166 } 167 168 @Test 169 public void testShortcutAvailable_enabledButNoServiceWhenCreated_shouldReturnFalse() { 170 configureNoShortcutService(); 171 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 172 assertFalse(getController().isAccessibilityShortcutAvailable(false)); 173 } 174 175 @Test 176 public void testShortcutAvailable_enabledWithValidServiceWhenCreated_shouldReturnTrue() { 177 configureValidShortcutService(); 178 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 179 assertTrue(getController().isAccessibilityShortcutAvailable(false)); 180 } 181 182 @Test 183 public void testShortcutAvailable_disabledWithValidServiceWhenCreated_shouldReturnFalse() { 184 configureValidShortcutService(); 185 configureShortcutEnabled(DISABLED_BUT_LOCK_SCREEN_ON); 186 assertFalse(getController().isAccessibilityShortcutAvailable(false)); 187 } 188 189 @Test 190 public void testShortcutAvailable_onLockScreenButDisabledThere_shouldReturnFalse() { 191 configureValidShortcutService(); 192 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 193 assertFalse(getController().isAccessibilityShortcutAvailable(true)); 194 } 195 196 @Test 197 public void testShortcutAvailable_onLockScreenAndEnabledThere_shouldReturnTrue() { 198 configureValidShortcutService(); 199 configureShortcutEnabled(ENABLED_INCLUDING_LOCK_SCREEN); 200 assertTrue(getController().isAccessibilityShortcutAvailable(true)); 201 } 202 203 @Test 204 public void testShortcutAvailable_whenServiceIdBecomesNull_shouldReturnFalse() { 205 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 206 configureValidShortcutService(); 207 AccessibilityShortcutController accessibilityShortcutController = getController(); 208 Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, ""); 209 accessibilityShortcutController.onSettingsChanged(); 210 assertFalse(accessibilityShortcutController.isAccessibilityShortcutAvailable(false)); 211 } 212 213 @Test 214 public void testShortcutAvailable_whenServiceIdBecomesNonNull_shouldReturnTrue() { 215 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 216 configureNoShortcutService(); 217 AccessibilityShortcutController accessibilityShortcutController = getController(); 218 configureValidShortcutService(); 219 accessibilityShortcutController.onSettingsChanged(); 220 assertTrue(accessibilityShortcutController.isAccessibilityShortcutAvailable(false)); 221 } 222 223 @Test 224 public void testShortcutAvailable_whenShortcutBecomesDisabled_shouldReturnFalse() { 225 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 226 configureValidShortcutService(); 227 AccessibilityShortcutController accessibilityShortcutController = getController(); 228 configureShortcutEnabled(DISABLED); 229 accessibilityShortcutController.onSettingsChanged(); 230 assertFalse(accessibilityShortcutController.isAccessibilityShortcutAvailable(false)); 231 } 232 233 @Test 234 public void testShortcutAvailable_whenShortcutBecomesEnabled_shouldReturnTrue() { 235 configureShortcutEnabled(DISABLED); 236 configureValidShortcutService(); 237 AccessibilityShortcutController accessibilityShortcutController = getController(); 238 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 239 accessibilityShortcutController.onSettingsChanged(); 240 assertTrue(accessibilityShortcutController.isAccessibilityShortcutAvailable(false)); 241 } 242 243 @Test 244 public void testShortcutAvailable_whenLockscreenBecomesDisabled_shouldReturnFalse() { 245 configureShortcutEnabled(ENABLED_INCLUDING_LOCK_SCREEN); 246 configureValidShortcutService(); 247 AccessibilityShortcutController accessibilityShortcutController = getController(); 248 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 249 accessibilityShortcutController.onSettingsChanged(); 250 assertFalse(accessibilityShortcutController.isAccessibilityShortcutAvailable(true)); 251 } 252 253 @Test 254 public void testShortcutAvailable_whenLockscreenBecomesEnabled_shouldReturnTrue() { 255 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 256 configureValidShortcutService(); 257 AccessibilityShortcutController accessibilityShortcutController = getController(); 258 configureShortcutEnabled(ENABLED_INCLUDING_LOCK_SCREEN); 259 accessibilityShortcutController.onSettingsChanged(); 260 assertTrue(accessibilityShortcutController.isAccessibilityShortcutAvailable(true)); 261 } 262 263 @Test 264 public void testOnAccessibilityShortcut_vibrates() { 265 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 266 AccessibilityShortcutController accessibilityShortcutController = getController(); 267 accessibilityShortcutController.performAccessibilityShortcut(); 268 verify(mVibrator).vibrate(aryEq(VIBRATOR_PATTERN_LONG), eq(-1), anyObject()); 269 } 270 271 @Test 272 public void testOnAccessibilityShortcut_firstTime_showsWarningDialog() 273 throws Exception { 274 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 275 configureValidShortcutService(); 276 AccessibilityShortcutController accessibilityShortcutController = getController(); 277 Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0); 278 accessibilityShortcutController.performAccessibilityShortcut(); 279 280 assertEquals(1, Settings.Secure.getInt( 281 mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0)); 282 verify(mResources).getString(R.string.accessibility_shortcut_toogle_warning); 283 verify(mAlertDialog).show(); 284 verify(mAccessibilityManagerService).getInstalledAccessibilityServiceList(anyInt()); 285 verify(mAccessibilityManagerService, times(0)).performAccessibilityShortcut(); 286 } 287 288 @Test 289 public void testOnAccessibilityShortcut_withDialogShowing_callsServer() 290 throws Exception { 291 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 292 configureValidShortcutService(); 293 AccessibilityShortcutController accessibilityShortcutController = getController(); 294 Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0); 295 accessibilityShortcutController.performAccessibilityShortcut(); 296 accessibilityShortcutController.performAccessibilityShortcut(); 297 verify(mToast).show(); 298 assertEquals(WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS, 299 mLayoutParams.privateFlags 300 & WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS); 301 verify(mAccessibilityManagerService, times(1)).performAccessibilityShortcut(); 302 } 303 304 @Test 305 public void testOnAccessibilityShortcut_ifCanceledFirstTime_showsWarningDialog() 306 throws Exception { 307 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 308 configureValidShortcutService(); 309 AccessibilityShortcutController accessibilityShortcutController = getController(); 310 Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0); 311 accessibilityShortcutController.performAccessibilityShortcut(); 312 ArgumentCaptor<AlertDialog.OnCancelListener> cancelListenerCaptor = 313 ArgumentCaptor.forClass(AlertDialog.OnCancelListener.class); 314 verify(mAlertDialogBuilder).setOnCancelListener(cancelListenerCaptor.capture()); 315 // Call the cancel callback 316 cancelListenerCaptor.getValue().onCancel(null); 317 318 accessibilityShortcutController.performAccessibilityShortcut(); 319 verify(mAlertDialog, times(2)).show(); 320 } 321 322 @Test 323 public void testClickingDisableButtonInDialog_shouldClearShortcutId() { 324 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 325 configureValidShortcutService(); 326 Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0); 327 getController().performAccessibilityShortcut(); 328 329 ArgumentCaptor<DialogInterface.OnClickListener> captor = 330 ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); 331 verify(mAlertDialogBuilder).setNegativeButton(eq(R.string.disable_accessibility_shortcut), 332 captor.capture()); 333 // Call the button callback 334 captor.getValue().onClick(null, 0); 335 assertTrue(TextUtils.isEmpty( 336 Settings.Secure.getString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))); 337 } 338 339 @Test 340 public void testClickingLeaveOnButtonInDialog_shouldLeaveShortcutReady() throws Exception { 341 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 342 configureValidShortcutService(); 343 Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0); 344 getController().performAccessibilityShortcut(); 345 346 ArgumentCaptor<DialogInterface.OnClickListener> captor = 347 ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); 348 verify(mAlertDialogBuilder).setPositiveButton(eq(R.string.leave_accessibility_shortcut_on), 349 captor.capture()); 350 // Call the button callback, if one exists 351 if (captor.getValue() != null) { 352 captor.getValue().onClick(null, 0); 353 } 354 assertEquals(SERVICE_NAME_STRING, 355 Settings.Secure.getString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)); 356 assertEquals(1, Settings.Secure.getInt( 357 mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)); 358 } 359 360 @Test 361 public void testOnAccessibilityShortcut_afterDialogShown_shouldCallServer() throws Exception { 362 configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); 363 configureValidShortcutService(); 364 Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1); 365 getController().performAccessibilityShortcut(); 366 367 verifyZeroInteractions(mAlertDialogBuilder, mAlertDialog); 368 verify(mToast).show(); 369 verify(mAccessibilityManagerService).performAccessibilityShortcut(); 370 } 371 372 private void configureNoShortcutService() { 373 Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, ""); 374 } 375 376 private void configureValidShortcutService() { 377 Settings.Secure.putString( 378 mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, SERVICE_NAME_STRING); 379 } 380 381 private void configureShortcutEnabled(int enabledValue) { 382 final boolean enabled; 383 final boolean lockscreen; 384 385 switch (enabledValue) { 386 case DISABLED: 387 enabled = false; 388 lockscreen = false; 389 break; 390 case DISABLED_BUT_LOCK_SCREEN_ON: 391 enabled = false; 392 lockscreen = true; 393 break; 394 case ENABLED_INCLUDING_LOCK_SCREEN: 395 enabled = true; 396 lockscreen = true; 397 break; 398 case ENABLED_EXCEPT_LOCK_SCREEN: 399 enabled = true; 400 lockscreen = false; 401 break; 402 default: 403 throw new IllegalArgumentException(); 404 } 405 406 Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_ENABLED, enabled ? 1 : 0); 407 Settings.Secure.putInt( 408 mContentResolver, ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN, lockscreen ? 1 : 0); 409 } 410 411 private AccessibilityShortcutController getController() { 412 AccessibilityShortcutController accessibilityShortcutController = 413 new AccessibilityShortcutController(mContext, mHandler, 0); 414 accessibilityShortcutController.mFrameworkObjectProvider = mFrameworkObjectProvider; 415 return accessibilityShortcutController; 416 } 417 } 418