1 /* 2 * Copyright (C) 2022 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.settings.connecteddevice.stylus; 18 19 import android.app.Dialog; 20 import android.app.role.RoleManager; 21 import android.bluetooth.BluetoothDevice; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.UserInfo; 27 import android.hardware.input.InputSettings; 28 import android.os.Process; 29 import android.os.UserHandle; 30 import android.os.UserManager; 31 import android.provider.Settings; 32 import android.provider.Settings.Secure; 33 import android.text.TextUtils; 34 import android.util.Log; 35 import android.view.InputDevice; 36 import android.view.KeyEvent; 37 import android.view.inputmethod.InputMethodInfo; 38 import android.view.inputmethod.InputMethodManager; 39 40 import androidx.annotation.Nullable; 41 import androidx.annotation.VisibleForTesting; 42 import androidx.preference.Preference; 43 import androidx.preference.PreferenceCategory; 44 import androidx.preference.PreferenceScreen; 45 import androidx.preference.SwitchPreferenceCompat; 46 import androidx.preference.TwoStatePreference; 47 48 import com.android.settings.R; 49 import com.android.settings.dashboard.profileselector.ProfileSelectDialog; 50 import com.android.settings.dashboard.profileselector.UserAdapter; 51 import com.android.settingslib.PrimarySwitchPreference; 52 import com.android.settingslib.bluetooth.BluetoothUtils; 53 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 54 import com.android.settingslib.core.AbstractPreferenceController; 55 import com.android.settingslib.core.lifecycle.Lifecycle; 56 import com.android.settingslib.core.lifecycle.LifecycleObserver; 57 import com.android.settingslib.core.lifecycle.events.OnResume; 58 59 import java.util.ArrayList; 60 import java.util.List; 61 62 /** 63 * This class adds stylus preferences. 64 */ 65 public class StylusDevicesController extends AbstractPreferenceController implements 66 Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener, 67 LifecycleObserver, OnResume { 68 69 @VisibleForTesting 70 static final String KEY_STYLUS = "device_stylus"; 71 @VisibleForTesting 72 static final String KEY_HANDWRITING = "handwriting_switch"; 73 @VisibleForTesting 74 static final String KEY_IGNORE_BUTTON = "ignore_button"; 75 @VisibleForTesting 76 static final String KEY_DEFAULT_NOTES = "default_notes"; 77 @VisibleForTesting 78 static final String KEY_SHOW_STYLUS_POINTER_ICON = "show_stylus_pointer_icon"; 79 80 private static final String TAG = "StylusDevicesController"; 81 82 @Nullable 83 private final InputDevice mInputDevice; 84 85 @Nullable 86 private final CachedBluetoothDevice mCachedBluetoothDevice; 87 88 @VisibleForTesting 89 PreferenceCategory mPreferencesContainer; 90 91 @VisibleForTesting 92 Dialog mDialog; 93 StylusDevicesController(Context context, InputDevice inputDevice, CachedBluetoothDevice cachedBluetoothDevice, Lifecycle lifecycle)94 public StylusDevicesController(Context context, InputDevice inputDevice, 95 CachedBluetoothDevice cachedBluetoothDevice, Lifecycle lifecycle) { 96 super(context); 97 mInputDevice = inputDevice; 98 mCachedBluetoothDevice = cachedBluetoothDevice; 99 lifecycle.addObserver(this); 100 } 101 102 @Override isAvailable()103 public boolean isAvailable() { 104 return isDeviceStylus(mInputDevice, mCachedBluetoothDevice); 105 } 106 107 @Nullable createOrUpdateDefaultNotesPreference(@ullable Preference preference)108 private Preference createOrUpdateDefaultNotesPreference(@Nullable Preference preference) { 109 RoleManager rm = mContext.getSystemService(RoleManager.class); 110 if (rm == null || !rm.isRoleAvailable(RoleManager.ROLE_NOTES)) { 111 return null; 112 } 113 114 // Check if the connected stylus supports the tail button. A connected device is when input 115 // device is available (mInputDevice != null). For a cached device (mInputDevice == null) 116 // there isn't way to check if the device supports the button so assume it does. 117 if (mInputDevice != null) { 118 boolean doesStylusSupportTailButton = 119 mInputDevice.hasKeys(KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL)[0]; 120 if (!doesStylusSupportTailButton) { 121 return null; 122 } 123 } 124 125 Preference pref = preference == null ? new Preference(mContext) : preference; 126 pref.setKey(KEY_DEFAULT_NOTES); 127 pref.setTitle(mContext.getString(R.string.stylus_default_notes_app)); 128 pref.setIcon(R.drawable.ic_article); 129 pref.setOnPreferenceClickListener(this); 130 pref.setEnabled(true); 131 132 UserHandle user = getDefaultNoteTaskProfile(); 133 List<String> roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, user); 134 if (roleHolders.isEmpty()) { 135 pref.setSummary(R.string.default_app_none); 136 return pref; 137 } 138 139 String packageName = roleHolders.get(0); 140 PackageManager pm = mContext.getPackageManager(); 141 String appName = packageName; 142 try { 143 ApplicationInfo ai = pm.getApplicationInfo(packageName, 144 PackageManager.ApplicationInfoFlags.of(0)); 145 appName = ai == null ? "" : pm.getApplicationLabel(ai).toString(); 146 } catch (PackageManager.NameNotFoundException e) { 147 Log.e(TAG, "Notes role package not found."); 148 } 149 150 if (mContext.getSystemService(UserManager.class).isManagedProfile(user.getIdentifier())) { 151 pref.setSummary( 152 mContext.getString(R.string.stylus_default_notes_summary_work, appName)); 153 } else { 154 pref.setSummary(appName); 155 } 156 return pref; 157 } 158 createOrUpdateHandwritingPreference( PrimarySwitchPreference preference)159 private PrimarySwitchPreference createOrUpdateHandwritingPreference( 160 PrimarySwitchPreference preference) { 161 PrimarySwitchPreference pref = preference == null ? new PrimarySwitchPreference(mContext) 162 : preference; 163 pref.setKey(KEY_HANDWRITING); 164 pref.setTitle(mContext.getString(R.string.stylus_textfield_handwriting)); 165 pref.setIcon(R.drawable.ic_text_fields_alt); 166 // Using a two-target preference, clicking will send an intent and change will toggle. 167 pref.setOnPreferenceChangeListener(this); 168 pref.setOnPreferenceClickListener(this); 169 pref.setChecked(Settings.Secure.getInt(mContext.getContentResolver(), 170 Settings.Secure.STYLUS_HANDWRITING_ENABLED, 171 Secure.STYLUS_HANDWRITING_DEFAULT_VALUE) == 1); 172 pref.setVisible(currentInputMethodSupportsHandwriting()); 173 return pref; 174 } 175 createButtonPressPreference()176 private TwoStatePreference createButtonPressPreference() { 177 TwoStatePreference pref = new SwitchPreferenceCompat(mContext); 178 pref.setKey(KEY_IGNORE_BUTTON); 179 pref.setTitle(mContext.getString(R.string.stylus_ignore_button)); 180 pref.setIcon(R.drawable.ic_block); 181 pref.setOnPreferenceClickListener(this); 182 pref.setChecked(Settings.Secure.getInt(mContext.getContentResolver(), 183 Settings.Secure.STYLUS_BUTTONS_ENABLED, 1) == 0); 184 return pref; 185 } 186 187 @Nullable createShowStylusPointerIconPreference( SwitchPreferenceCompat preference)188 private SwitchPreferenceCompat createShowStylusPointerIconPreference( 189 SwitchPreferenceCompat preference) { 190 if (!mContext.getResources() 191 .getBoolean(com.android.internal.R.bool.config_enableStylusPointerIcon)) { 192 // If the config is not enabled, no need to show the preference to user 193 return null; 194 } 195 SwitchPreferenceCompat pref = preference == null ? new SwitchPreferenceCompat(mContext) 196 : preference; 197 pref.setKey(KEY_SHOW_STYLUS_POINTER_ICON); 198 pref.setTitle(mContext.getString(R.string.show_stylus_pointer_icon)); 199 pref.setIcon(R.drawable.ic_stylus); 200 pref.setOnPreferenceClickListener(this); 201 pref.setChecked(Settings.Secure.getInt(mContext.getContentResolver(), 202 Settings.Secure.STYLUS_POINTER_ICON_ENABLED, 203 InputSettings.DEFAULT_STYLUS_POINTER_ICON_ENABLED) == 1); 204 return pref; 205 } 206 207 @Override onPreferenceClick(Preference preference)208 public boolean onPreferenceClick(Preference preference) { 209 String key = preference.getKey(); 210 switch (key) { 211 case KEY_DEFAULT_NOTES: 212 PackageManager pm = mContext.getPackageManager(); 213 String packageName = pm.getPermissionControllerPackageName(); 214 Intent intent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP).setPackage( 215 packageName).putExtra(Intent.EXTRA_ROLE_NAME, RoleManager.ROLE_NOTES); 216 217 List<UserHandle> users = getUserProfiles(); 218 if (users.size() <= 1) { 219 mContext.startActivity(intent); 220 } else { 221 createAndShowProfileSelectDialog(intent, users); 222 } 223 break; 224 case KEY_HANDWRITING: 225 InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 226 InputMethodInfo inputMethod = imm.getCurrentInputMethodInfo(); 227 if (inputMethod == null) break; 228 Intent handwritingIntent = 229 inputMethod.createStylusHandwritingSettingsActivityIntent(); 230 if (handwritingIntent != null) { 231 mContext.startActivity(handwritingIntent); 232 } 233 break; 234 case KEY_IGNORE_BUTTON: 235 Settings.Secure.putInt(mContext.getContentResolver(), 236 Secure.STYLUS_BUTTONS_ENABLED, 237 ((TwoStatePreference) preference).isChecked() ? 0 : 1); 238 break; 239 case KEY_SHOW_STYLUS_POINTER_ICON: 240 Settings.Secure.putInt(mContext.getContentResolver(), 241 Secure.STYLUS_POINTER_ICON_ENABLED, 242 ((SwitchPreferenceCompat) preference).isChecked() ? 1 : 0); 243 break; 244 } 245 return true; 246 } 247 248 @Override onPreferenceChange(Preference preference, Object newValue)249 public boolean onPreferenceChange(Preference preference, Object newValue) { 250 String key = preference.getKey(); 251 switch (key) { 252 case KEY_HANDWRITING: 253 Settings.Secure.putInt(mContext.getContentResolver(), 254 Settings.Secure.STYLUS_HANDWRITING_ENABLED, 255 (boolean) newValue ? 1 : 0); 256 break; 257 } 258 return true; 259 } 260 261 @Override displayPreference(PreferenceScreen screen)262 public final void displayPreference(PreferenceScreen screen) { 263 mPreferencesContainer = (PreferenceCategory) screen.findPreference(getPreferenceKey()); 264 super.displayPreference(screen); 265 266 refresh(); 267 } 268 269 @Override getPreferenceKey()270 public String getPreferenceKey() { 271 return KEY_STYLUS; 272 } 273 274 @Override onResume()275 public void onResume() { 276 refresh(); 277 } 278 refresh()279 private void refresh() { 280 if (!isAvailable()) return; 281 282 Preference currNotesPref = mPreferencesContainer.findPreference(KEY_DEFAULT_NOTES); 283 Preference notesPref = createOrUpdateDefaultNotesPreference(currNotesPref); 284 if (currNotesPref == null && notesPref != null) { 285 mPreferencesContainer.addPreference(notesPref); 286 } 287 288 PrimarySwitchPreference currHandwritingPref = mPreferencesContainer.findPreference( 289 KEY_HANDWRITING); 290 Preference handwritingPref = createOrUpdateHandwritingPreference(currHandwritingPref); 291 if (currHandwritingPref == null) { 292 mPreferencesContainer.addPreference(handwritingPref); 293 } 294 295 Preference buttonPref = mPreferencesContainer.findPreference(KEY_IGNORE_BUTTON); 296 if (buttonPref == null) { 297 mPreferencesContainer.addPreference(createButtonPressPreference()); 298 } 299 SwitchPreferenceCompat currShowStylusPointerIconPref = mPreferencesContainer 300 .findPreference(KEY_SHOW_STYLUS_POINTER_ICON); 301 Preference showStylusPointerIconPref = 302 createShowStylusPointerIconPreference(currShowStylusPointerIconPref); 303 if (currShowStylusPointerIconPref == null && showStylusPointerIconPref != null) { 304 mPreferencesContainer.addPreference(showStylusPointerIconPref); 305 } 306 } 307 currentInputMethodSupportsHandwriting()308 private boolean currentInputMethodSupportsHandwriting() { 309 InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 310 InputMethodInfo inputMethod = imm.getCurrentInputMethodInfo(); 311 return inputMethod != null && inputMethod.supportsStylusHandwriting(); 312 } 313 getUserProfiles()314 private List<UserHandle> getUserProfiles() { 315 UserManager um = mContext.getSystemService(UserManager.class); 316 final UserHandle currentUser = Process.myUserHandle(); 317 final List<UserHandle> userProfiles = new ArrayList<>(); 318 userProfiles.add(currentUser); 319 320 final List<UserInfo> userInfos = um.getProfiles(currentUser.getIdentifier()); 321 for (UserInfo userInfo : userInfos) { 322 if (userInfo.isManagedProfile() 323 || (android.os.Flags.allowPrivateProfile() 324 && android.multiuser.Flags.enablePrivateSpaceFeatures() 325 && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace() 326 && userInfo.isPrivateProfile())) { 327 userProfiles.add(userInfo.getUserHandle()); 328 } 329 } 330 return userProfiles; 331 } 332 getDefaultNoteTaskProfile()333 private UserHandle getDefaultNoteTaskProfile() { 334 final int userId = Secure.getInt( 335 mContext.getContentResolver(), 336 Secure.DEFAULT_NOTE_TASK_PROFILE, 337 UserHandle.myUserId()); 338 return UserHandle.of(userId); 339 } 340 341 @VisibleForTesting createProfileDialogClickCallback( Intent intent, List<UserHandle> users)342 UserAdapter.OnClickListener createProfileDialogClickCallback( 343 Intent intent, List<UserHandle> users) { 344 // TODO(b/281659827): improve UX flow for when activity is cancelled 345 return (int position) -> { 346 intent.putExtra(Intent.EXTRA_USER, users.get(position)); 347 348 Secure.putInt(mContext.getContentResolver(), 349 Secure.DEFAULT_NOTE_TASK_PROFILE, 350 users.get(position).getIdentifier()); 351 mContext.startActivity(intent); 352 353 mDialog.dismiss(); 354 }; 355 } 356 createAndShowProfileSelectDialog(Intent intent, List<UserHandle> users)357 private void createAndShowProfileSelectDialog(Intent intent, List<UserHandle> users) { 358 mDialog = ProfileSelectDialog.createDialog( 359 mContext, 360 users, 361 createProfileDialogClickCallback(intent, users)); 362 mDialog.show(); 363 } 364 365 /** 366 * Identifies whether a device is a stylus using the associated {@link InputDevice} or 367 * {@link CachedBluetoothDevice}. 368 * 369 * InputDevices are only available when the device is USI or Bluetooth-connected, whereas 370 * CachedBluetoothDevices are available for Bluetooth devices when connected or paired, 371 * so to handle all cases, both are needed. 372 * 373 * @param inputDevice The associated input device of the stylus 374 * @param cachedBluetoothDevice The associated bluetooth device of the stylus 375 */ isDeviceStylus(@ullable InputDevice inputDevice, @Nullable CachedBluetoothDevice cachedBluetoothDevice)376 public static boolean isDeviceStylus(@Nullable InputDevice inputDevice, 377 @Nullable CachedBluetoothDevice cachedBluetoothDevice) { 378 if (inputDevice != null && inputDevice.supportsSource(InputDevice.SOURCE_STYLUS)) { 379 return true; 380 } 381 382 if (cachedBluetoothDevice != null) { 383 BluetoothDevice bluetoothDevice = cachedBluetoothDevice.getDevice(); 384 String deviceType = BluetoothUtils.getStringMetaData(bluetoothDevice, 385 BluetoothDevice.METADATA_DEVICE_TYPE); 386 return TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS); 387 } 388 389 return false; 390 } 391 } 392