1 /* 2 * Copyright (C) 2023 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.systemui.accessibility.accessibilitymenu; 18 19 import android.Manifest; 20 import android.accessibilityservice.AccessibilityButtonController; 21 import android.accessibilityservice.AccessibilityService; 22 import android.app.KeyguardManager; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.SharedPreferences; 28 import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.content.res.Configuration; 32 import android.hardware.display.BrightnessInfo; 33 import android.hardware.display.DisplayManager; 34 import android.media.AudioManager; 35 import android.os.Handler; 36 import android.os.Looper; 37 import android.os.SystemClock; 38 import android.provider.Settings; 39 import android.util.Log; 40 import android.view.Display; 41 import android.view.KeyEvent; 42 import android.view.MotionEvent; 43 import android.view.View; 44 import android.view.accessibility.AccessibilityEvent; 45 46 import androidx.preference.PreferenceManager; 47 48 import com.android.settingslib.display.BrightnessUtils; 49 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut.ShortcutId; 50 import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuOverlayLayout; 51 52 import java.util.List; 53 54 /** @hide */ 55 public class AccessibilityMenuService extends AccessibilityService 56 implements View.OnTouchListener { 57 58 public static final String PACKAGE_NAME = AccessibilityMenuService.class.getPackageName(); 59 public static final String INTENT_TOGGLE_MENU = ".toggle_menu"; 60 public static final String INTENT_HIDE_MENU = ".hide_menu"; 61 public static final String INTENT_GLOBAL_ACTION = ".global_action"; 62 public static final String INTENT_GLOBAL_ACTION_EXTRA = "GLOBAL_ACTION"; 63 64 private static final String TAG = "A11yMenuService"; 65 private static final long BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE = 100L; 66 67 private static final int BRIGHTNESS_UP_INCREMENT_GAMMA = 68 (int) Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f); 69 private static final int BRIGHTNESS_DOWN_INCREMENT_GAMMA = 70 (int) -Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f); 71 72 private long mLastTimeTouchedOutside = 0L; 73 // Timeout used to ignore the A11y button onClick() when ACTION_OUTSIDE is also received on 74 // clicking on the A11y button. 75 public static final long BUTTON_CLICK_TIMEOUT = 200; 76 77 private A11yMenuOverlayLayout mA11yMenuLayout; 78 private SharedPreferences mPrefs; 79 80 private static boolean sInitialized = false; 81 82 private AudioManager mAudioManager; 83 84 // TODO(b/136716947): Support multi-display once a11y framework side is ready. 85 private DisplayManager mDisplayManager; 86 87 private KeyguardManager mKeyguardManager; 88 89 private final DisplayManager.DisplayListener mDisplayListener = 90 new DisplayManager.DisplayListener() { 91 int mRotation; 92 93 @Override 94 public void onDisplayAdded(int displayId) {} 95 96 @Override 97 public void onDisplayRemoved(int displayId) { 98 // TODO(b/136716947): Need to reset A11yMenuOverlayLayout by display id. 99 } 100 101 @Override 102 public void onDisplayChanged(int displayId) { 103 Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); 104 if (mRotation != display.getRotation()) { 105 mRotation = display.getRotation(); 106 mA11yMenuLayout.updateViewLayout(); 107 } 108 } 109 }; 110 111 private final BroadcastReceiver mHideMenuReceiver = new BroadcastReceiver() { 112 @Override 113 public void onReceive(Context context, Intent intent) { 114 mA11yMenuLayout.hideMenu(); 115 } 116 }; 117 118 private final BroadcastReceiver mToggleMenuReceiver = new BroadcastReceiver() { 119 @Override 120 public void onReceive(Context context, Intent intent) { 121 toggleVisibility(); 122 } 123 }; 124 125 /** 126 * Update a11y menu layout when large button setting is changed. 127 */ 128 private final OnSharedPreferenceChangeListener mSharedPreferenceChangeListener = 129 (SharedPreferences prefs, String key) -> { 130 { 131 if (key.equals(getString(R.string.pref_large_buttons))) { 132 mA11yMenuLayout.configureLayout(); 133 } 134 } 135 }; 136 137 // Update layout. 138 private final Handler mHandler = new Handler(Looper.getMainLooper()); 139 private final Runnable mOnConfigChangedRunnable = new Runnable() { 140 @Override 141 public void run() { 142 if (!sInitialized) { 143 return; 144 } 145 // Re-assign theme to service after onConfigurationChanged 146 getTheme().applyStyle(R.style.ServiceTheme, true); 147 // Caches & updates the page index to ViewPager when a11y menu is refreshed. 148 // Otherwise, the menu page would reset on a UI update. 149 int cachedPageIndex = mA11yMenuLayout.getPageIndex(); 150 mA11yMenuLayout.configureLayout(cachedPageIndex); 151 } 152 }; 153 154 @Override onCreate()155 public void onCreate() { 156 super.onCreate(); 157 setTheme(R.style.ServiceTheme); 158 159 getAccessibilityButtonController().registerAccessibilityButtonCallback( 160 new AccessibilityButtonController.AccessibilityButtonCallback() { 161 /** 162 * {@inheritDoc} 163 */ 164 @Override 165 public void onClicked(AccessibilityButtonController controller) { 166 toggleVisibility(); 167 } 168 169 /** 170 * {@inheritDoc} 171 */ 172 @Override 173 public void onAvailabilityChanged(AccessibilityButtonController controller, 174 boolean available) {} 175 } 176 ); 177 } 178 179 @Override onDestroy()180 public void onDestroy() { 181 if (mHandler.hasCallbacks(mOnConfigChangedRunnable)) { 182 mHandler.removeCallbacks(mOnConfigChangedRunnable); 183 } 184 185 super.onDestroy(); 186 } 187 188 @Override onServiceConnected()189 protected void onServiceConnected() { 190 mA11yMenuLayout = new A11yMenuOverlayLayout(this); 191 192 IntentFilter hideMenuFilter = new IntentFilter(); 193 hideMenuFilter.addAction(Intent.ACTION_SCREEN_OFF); 194 hideMenuFilter.addAction(PACKAGE_NAME + INTENT_HIDE_MENU); 195 196 // Including WRITE_SECURE_SETTINGS enforces that we only listen to apps 197 // with the restricted WRITE_SECURE_SETTINGS permission who broadcast this intent. 198 registerReceiver(mHideMenuReceiver, hideMenuFilter, 199 Manifest.permission.WRITE_SECURE_SETTINGS, null, 200 Context.RECEIVER_EXPORTED); 201 registerReceiver(mToggleMenuReceiver, 202 new IntentFilter(PACKAGE_NAME + INTENT_TOGGLE_MENU), 203 Manifest.permission.WRITE_SECURE_SETTINGS, null, 204 Context.RECEIVER_EXPORTED); 205 206 mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 207 mPrefs.registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); 208 209 210 mDisplayManager = getSystemService(DisplayManager.class); 211 mDisplayManager.registerDisplayListener(mDisplayListener, null); 212 mAudioManager = getSystemService(AudioManager.class); 213 mKeyguardManager = getSystemService(KeyguardManager.class); 214 215 sInitialized = true; 216 } 217 218 @Override onAccessibilityEvent(AccessibilityEvent event)219 public void onAccessibilityEvent(AccessibilityEvent event) {} 220 221 /** 222 * This method would notify service when device configuration, such as display size, 223 * localization, orientation or theme, is changed. 224 * 225 * @param newConfig the new device configuration. 226 */ 227 @Override onConfigurationChanged(Configuration newConfig)228 public void onConfigurationChanged(Configuration newConfig) { 229 // Prevent update layout failure 230 // if multiple onConfigurationChanged are called at the same time. 231 if (mHandler.hasCallbacks(mOnConfigChangedRunnable)) { 232 mHandler.removeCallbacks(mOnConfigChangedRunnable); 233 } 234 mHandler.postDelayed( 235 mOnConfigChangedRunnable, BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE); 236 } 237 238 /** 239 * Performs global action and broadcasts an intent indicating the action was performed. 240 * This is unnecessary for any current functionality, but is used for testing. 241 * Refer to {@code performGlobalAction()}. 242 * 243 * @param globalAction Global action to be performed. 244 * @return {@code true} if successful, {@code false} otherwise. 245 */ performGlobalActionInternal(int globalAction)246 private boolean performGlobalActionInternal(int globalAction) { 247 Intent intent = new Intent(PACKAGE_NAME + INTENT_GLOBAL_ACTION); 248 intent.putExtra(INTENT_GLOBAL_ACTION_EXTRA, globalAction); 249 sendBroadcast(intent); 250 Log.i("A11yMenuService", "Broadcasting global action " + globalAction); 251 return performGlobalAction(globalAction); 252 } 253 254 /** 255 * Handles click events of shortcuts. 256 * 257 * @param view the shortcut button being clicked. 258 */ handleClick(View view)259 public void handleClick(View view) { 260 // Shortcuts are repeatable in a11y menu rather than unique, so use tag ID to handle. 261 int viewTag = (int) view.getTag(); 262 263 if (viewTag == ShortcutId.ID_ASSISTANT_VALUE.ordinal()) { 264 // Always restart the voice command activity, so that the UI is reloaded. 265 startActivityIfIntentIsSafe( 266 new Intent(Intent.ACTION_VOICE_COMMAND), 267 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 268 } else if (viewTag == ShortcutId.ID_A11YSETTING_VALUE.ordinal()) { 269 startActivityIfIntentIsSafe( 270 new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS), 271 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 272 } else if (viewTag == ShortcutId.ID_POWER_VALUE.ordinal()) { 273 performGlobalActionInternal(GLOBAL_ACTION_POWER_DIALOG); 274 } else if (viewTag == ShortcutId.ID_RECENT_VALUE.ordinal()) { 275 performGlobalActionInternal(GLOBAL_ACTION_RECENTS); 276 } else if (viewTag == ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()) { 277 performGlobalActionInternal(GLOBAL_ACTION_LOCK_SCREEN); 278 } else if (viewTag == ShortcutId.ID_QUICKSETTING_VALUE.ordinal()) { 279 performGlobalActionInternal(GLOBAL_ACTION_QUICK_SETTINGS); 280 } else if (viewTag == ShortcutId.ID_NOTIFICATION_VALUE.ordinal()) { 281 performGlobalActionInternal(GLOBAL_ACTION_NOTIFICATIONS); 282 } else if (viewTag == ShortcutId.ID_SCREENSHOT_VALUE.ordinal()) { 283 performGlobalActionInternal(GLOBAL_ACTION_TAKE_SCREENSHOT); 284 } else if (viewTag == ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal()) { 285 adjustBrightness(BRIGHTNESS_UP_INCREMENT_GAMMA); 286 return; 287 } else if (viewTag == ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal()) { 288 adjustBrightness(BRIGHTNESS_DOWN_INCREMENT_GAMMA); 289 return; 290 } else if (viewTag == ShortcutId.ID_VOLUME_UP_VALUE.ordinal()) { 291 adjustVolume(AudioManager.ADJUST_RAISE); 292 return; 293 } else if (viewTag == ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal()) { 294 adjustVolume(AudioManager.ADJUST_LOWER); 295 return; 296 } 297 298 mA11yMenuLayout.hideMenu(); 299 } 300 301 /** 302 * Adjusts brightness using the same logic and utils class as the SystemUI brightness slider. 303 * 304 * @see BrightnessUtils 305 * @see com.android.systemui.settings.brightness.BrightnessController 306 * @param increment The increment amount in gamma-space 307 */ adjustBrightness(int increment)308 private void adjustBrightness(int increment) { 309 Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); 310 BrightnessInfo info = display.getBrightnessInfo(); 311 int gamma = BrightnessUtils.convertLinearToGammaFloat( 312 info.brightness, 313 info.brightnessMinimum, 314 info.brightnessMaximum 315 ); 316 gamma = Math.max( 317 BrightnessUtils.GAMMA_SPACE_MIN, 318 Math.min(BrightnessUtils.GAMMA_SPACE_MAX, gamma + increment)); 319 320 float brightness = BrightnessUtils.convertGammaToLinearFloat( 321 gamma, 322 info.brightnessMinimum, 323 info.brightnessMaximum 324 ); 325 mDisplayManager.setBrightness(display.getDisplayId(), brightness); 326 mA11yMenuLayout.showSnackbar( 327 getString(R.string.brightness_percentage_label, 328 (gamma / (BrightnessUtils.GAMMA_SPACE_MAX / 100)))); 329 } 330 adjustVolume(int direction)331 private void adjustVolume(int direction) { 332 mAudioManager.adjustStreamVolume( 333 AudioManager.STREAM_MUSIC, direction, 334 AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); 335 final int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 336 final int volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 337 mA11yMenuLayout.showSnackbar( 338 getString( 339 R.string.music_volume_percentage_label, 340 (int) (100.0 / maxVolume * volume)) 341 ); 342 } 343 startActivityIfIntentIsSafe(Intent intent, int flag)344 private void startActivityIfIntentIsSafe(Intent intent, int flag) { 345 PackageManager packageManager = getPackageManager(); 346 List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 347 PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY)); 348 if (!activities.isEmpty()) { 349 intent.setFlags(flag); 350 startActivity(intent); 351 } 352 } 353 354 @Override onInterrupt()355 public void onInterrupt() { 356 } 357 358 @Override onUnbind(Intent intent)359 public boolean onUnbind(Intent intent) { 360 unregisterReceiver(mHideMenuReceiver); 361 unregisterReceiver(mToggleMenuReceiver); 362 mPrefs.unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener); 363 sInitialized = false; 364 return super.onUnbind(intent); 365 } 366 367 @Override onKeyEvent(KeyEvent event)368 protected boolean onKeyEvent(KeyEvent event) { 369 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 370 mA11yMenuLayout.hideMenu(); 371 } 372 return false; 373 } 374 375 @Override onTouch(View v, MotionEvent event)376 public boolean onTouch(View v, MotionEvent event) { 377 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 378 if (mA11yMenuLayout.hideMenu()) { 379 mLastTimeTouchedOutside = SystemClock.uptimeMillis(); 380 } 381 } 382 return false; 383 } 384 toggleVisibility()385 private void toggleVisibility() { 386 boolean locked = mKeyguardManager != null && mKeyguardManager.isKeyguardLocked(); 387 if (!locked && SystemClock.uptimeMillis() - mLastTimeTouchedOutside 388 > BUTTON_CLICK_TIMEOUT) { 389 mA11yMenuLayout.toggleVisibility(); 390 } 391 } 392 } 393