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