shortcutFeatureKeys = new ArrayList<>();
shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_QS_TARGETS);
return shortcutFeatureKeys;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
initTopIntroPreference();
initAnimatedImagePreference();
initToggleServiceSwitchPreference();
initGeneralCategory();
initShortcutPreference();
initSettingsPreference();
initAppInfoPreference();
initHtmlTextPreference();
initFooterPreference();
installActionBarToggleSwitch();
updateToggleServiceTitle(mToggleServiceSwitchPreference);
mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> {
mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
};
updatePreferenceOrder();
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public Dialog onCreateDialog(int dialogId) {
switch (dialogId) {
case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
if (isAnySetupWizard()) {
mDialog = AccessibilityShortcutsTutorial
.createAccessibilityTutorialDialogForSetupWizard(
getPrefContext(), getUserPreferredShortcutTypes(),
this::callOnTutorialDialogButtonClicked, mFeatureName);
} else {
mDialog = AccessibilityShortcutsTutorial
.createAccessibilityTutorialDialog(
getPrefContext(), getUserPreferredShortcutTypes(),
this::callOnTutorialDialogButtonClicked, mFeatureName);
}
mDialog.setCanceledOnTouchOutside(false);
return mDialog;
default:
throw new IllegalArgumentException("Unsupported dialogId " + dialogId);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final SettingsActivity settingsActivity = (SettingsActivity) getActivity();
final SettingsMainSwitchBar switchBar = settingsActivity.getSwitchBar();
switchBar.hide();
writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(getContext());
}
@Override
public void onResume() {
super.onResume();
final AccessibilityManager am = getPrefContext().getSystemService(
AccessibilityManager.class);
am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
mSettingsContentObserver.register(getContentResolver());
updateShortcutPreferenceData();
updateShortcutPreference();
}
@Override
public void onPause() {
final AccessibilityManager am = getPrefContext().getSystemService(
AccessibilityManager.class);
am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
mSettingsContentObserver.unregister(getContentResolver());
super.onPause();
}
@Override
public void onDestroyView() {
super.onDestroyView();
removeActionBarToggleSwitch();
}
@Override
public int getDialogMetricsCategory(int dialogId) {
switch (dialogId) {
case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL;
default:
return SettingsEnums.ACTION_UNKNOWN;
}
}
@Override
public int getMetricsCategory() {
return SettingsEnums.ACCESSIBILITY_SERVICE;
}
/**
* Returns the category of the feedback page.
*
* By default, this method returns {@link SettingsEnums#PAGE_UNKNOWN}. This indicates that
* the feedback category is unknown, and the absence of a feedback menu.
*
* @return The feedback category, which is {@link SettingsEnums#PAGE_UNKNOWN} by default.
*/
protected int getFeedbackCategory() {
return SettingsEnums.PAGE_UNKNOWN;
}
@Override
public int getHelpResource() {
return 0;
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
onPreferenceToggled(mPreferenceKey, isChecked);
}
/**
* Returns the shortcut type list which has been checked by user.
*/
abstract int getUserShortcutTypes();
/** Returns the accessibility tile component name. */
abstract ComponentName getTileComponentName();
protected void updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference) {
final CharSequence title =
getString(R.string.accessibility_service_primary_switch_title, mFeatureName);
switchPreference.setTitle(title);
}
protected String getUseServicePreferenceKey() {
return "use_service";
}
protected CharSequence getShortcutTitle() {
return getString(R.string.accessibility_shortcut_title, mFeatureName);
}
@VisibleForTesting
CharSequence getContentDescriptionForAnimatedIllustration() {
return getString(R.string.accessibility_illustration_content_description, mFeatureName);
}
protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
}
protected void onInstallSwitchPreferenceToggleSwitch() {
// Implement this to set a checked listener.
updateSwitchBarToggleSwitch();
mToggleServiceSwitchPreference.addOnSwitchChangeListener(this);
}
protected void onRemoveSwitchPreferenceToggleSwitch() {
// Implement this to reset a checked listener.
}
protected void updateSwitchBarToggleSwitch() {
// Implement this to update the state of switch.
}
public void setTitle(String title) {
getActivity().setTitle(title);
}
protected void onProcessArguments(@Nullable Bundle arguments) {
if (arguments == null) {
return;
}
// Key.
mPreferenceKey = arguments.getString(AccessibilitySettings.EXTRA_PREFERENCE_KEY);
// Title.
if (arguments.containsKey(AccessibilitySettings.EXTRA_RESOLVE_INFO)) {
ResolveInfo info = arguments.getParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO);
getActivity().setTitle(info.loadLabel(getPackageManager()).toString());
} else if (arguments.containsKey(AccessibilitySettings.EXTRA_TITLE)) {
setTitle(arguments.getString(AccessibilitySettings.EXTRA_TITLE));
}
// Summary.
if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) {
mDescription = arguments.getCharSequence(AccessibilitySettings.EXTRA_SUMMARY);
}
// Settings html description.
if (arguments.containsKey(AccessibilitySettings.EXTRA_HTML_DESCRIPTION)) {
mHtmlDescription = arguments.getCharSequence(
AccessibilitySettings.EXTRA_HTML_DESCRIPTION);
}
// Intro.
if (arguments.containsKey(AccessibilitySettings.EXTRA_INTRO)) {
mTopIntroTitle = arguments.getCharSequence(AccessibilitySettings.EXTRA_INTRO);
}
}
private void installActionBarToggleSwitch() {
onInstallSwitchPreferenceToggleSwitch();
}
private void removeActionBarToggleSwitch() {
mToggleServiceSwitchPreference.setOnPreferenceClickListener(null);
onRemoveSwitchPreferenceToggleSwitch();
}
private void updatePreferenceOrder() {
final List lists = getPreferenceOrderList();
final PreferenceScreen preferenceScreen = getPreferenceScreen();
preferenceScreen.setOrderingAsAdded(false);
final int size = lists.size();
for (int i = 0; i < size; i++) {
final Preference preference = preferenceScreen.findPreference(lists.get(i));
if (preference != null) {
preference.setOrder(i);
}
}
}
/** Customizes the order by preference key. */
protected List getPreferenceOrderList() {
final List lists = new ArrayList<>();
lists.add(KEY_TOP_INTRO_PREFERENCE);
lists.add(KEY_ANIMATED_IMAGE);
lists.add(getUseServicePreferenceKey());
lists.add(KEY_GENERAL_CATEGORY);
lists.add(KEY_HTML_DESCRIPTION_PREFERENCE);
return lists;
}
private Drawable getDrawableFromUri(Uri imageUri) {
if (mImageGetterCacheView == null) {
mImageGetterCacheView = new ImageView(getPrefContext());
}
mImageGetterCacheView.setAdjustViewBounds(true);
mImageGetterCacheView.setImageURI(imageUri);
if (mImageGetterCacheView.getDrawable() == null) {
return null;
}
final Drawable drawable =
mImageGetterCacheView.getDrawable().mutate().getConstantState().newDrawable();
mImageGetterCacheView.setImageURI(null);
final int imageWidth = drawable.getIntrinsicWidth();
final int imageHeight = drawable.getIntrinsicHeight();
final int screenHalfHeight = AccessibilityUtil.getScreenHeightPixels(getPrefContext()) / 2;
if ((imageWidth > AccessibilityUtil.getScreenWidthPixels(getPrefContext()))
|| (imageHeight > screenHalfHeight)) {
return null;
}
drawable.setBounds(/* left= */0, /* top= */0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());
return drawable;
}
private void initAnimatedImagePreference() {
initAnimatedImagePreference(mImageUri, new IllustrationPreference(getPrefContext()) {
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
if (ThemeHelper.shouldApplyGlifExpressiveStyle(getContext())
&& isAnySetupWizard()) {
View illustrationFrame = holder.findViewById(R.id.illustration_frame);
final ViewGroup.LayoutParams lp = illustrationFrame.getLayoutParams();
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
illustrationFrame.setLayoutParams(lp);
}
}
});
}
@VisibleForTesting
void initAnimatedImagePreference(
@Nullable Uri imageUri,
@NonNull IllustrationPreference preference) {
if (imageUri == null) {
return;
}
final int displayHalfHeight =
AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2;
preference.setImageUri(imageUri);
preference.setSelectable(false);
preference.setMaxHeight(displayHalfHeight);
preference.setKey(KEY_ANIMATED_IMAGE);
preference.setOnBindListener(view -> {
// isAnimatable is decided in
// {@link IllustrationPreference#onBindViewHolder(PreferenceViewHolder)}. Therefore, we
// wait until the view is bond to set the content description for it.
// The content description is added for an animation illustration only. Since the static
// images are decorative.
ThreadUtils.getUiThreadHandler().post(() -> {
if (preference.isAnimatable()) {
preference.setContentDescription(
getContentDescriptionForAnimatedIllustration());
}
});
});
getPreferenceScreen().addPreference(preference);
}
@VisibleForTesting
void initTopIntroPreference() {
if (TextUtils.isEmpty(mTopIntroTitle)) {
return;
}
mTopIntroPreference = new TopIntroPreference(getPrefContext());
mTopIntroPreference.setKey(KEY_TOP_INTRO_PREFERENCE);
mTopIntroPreference.setTitle(mTopIntroTitle);
getPreferenceScreen().addPreference(mTopIntroPreference);
}
private void initToggleServiceSwitchPreference() {
mToggleServiceSwitchPreference = new SettingsMainSwitchPreference(getPrefContext());
mToggleServiceSwitchPreference.setKey(getUseServicePreferenceKey());
if (getArguments() != null
&& getArguments().containsKey(AccessibilitySettings.EXTRA_CHECKED)) {
final boolean enabled = getArguments().getBoolean(AccessibilitySettings.EXTRA_CHECKED);
mToggleServiceSwitchPreference.setChecked(enabled);
}
getPreferenceScreen().addPreference(mToggleServiceSwitchPreference);
}
private void initGeneralCategory() {
final PreferenceCategory generalCategory = new PreferenceCategory(getPrefContext());
generalCategory.setKey(KEY_GENERAL_CATEGORY);
generalCategory.setTitle(R.string.accessibility_screen_option);
getPreferenceScreen().addPreference(generalCategory);
}
protected void initShortcutPreference() {
// Initial the shortcut preference.
mShortcutPreference = new ShortcutPreference(getPrefContext(), /* attrs= */ null);
mShortcutPreference.setPersistent(false);
mShortcutPreference.setKey(getShortcutPreferenceKey());
mShortcutPreference.setOnClickCallback(this);
mShortcutPreference.setTitle(getShortcutTitle());
final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
generalCategory.addPreference(mShortcutPreference);
}
protected void initSettingsPreference() {
if (mSettingsTitle == null || mSettingsIntent == null) {
return;
}
// Show the "Settings" menu as if it were a preference screen.
mSettingsPreference = new Preference(getPrefContext());
mSettingsPreference.setTitle(mSettingsTitle);
mSettingsPreference.setIconSpaceReserved(false);
mSettingsPreference.setIntent(mSettingsIntent);
final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
generalCategory.addPreference(mSettingsPreference);
}
@VisibleForTesting
@Nullable
Preference createAppInfoPreference() {
if (!Flags.accessibilityShowAppInfoButton()) {
return null;
}
// App Info is not available in Setup Wizard.
if (isAnySetupWizard()) {
return null;
}
// Only show the button for pages with valid component package names.
if (mComponentName == null) {
return null;
}
final String packageName = mComponentName.getPackageName();
final PackageManager packageManager = getPrefContext().getPackageManager();
if (!packageManager.isPackageAvailable(packageName)) {
return null;
}
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setPackage(getContext().getPackageName());
intent.setData(Uri.parse("package:" + packageName));
final Preference appInfoPreference = new Preference(getPrefContext());
appInfoPreference.setTitle(getString(R.string.application_info_label));
appInfoPreference.setIconSpaceReserved(false);
appInfoPreference.setIntent(intent);
return appInfoPreference;
}
private void initAppInfoPreference() {
final Preference appInfoPreference = createAppInfoPreference();
if (appInfoPreference != null) {
final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
generalCategory.addPreference(appInfoPreference);
}
}
private void initHtmlTextPreference() {
if (TextUtils.isEmpty(getCurrentHtmlDescription())) {
return;
}
final PreferenceScreen screen = getPreferenceScreen();
mHtmlFooterPreference =
new AccessibilityFooterPreference(screen.getContext());
mHtmlFooterPreference.setKey(KEY_HTML_DESCRIPTION_PREFERENCE);
updateHtmlTextPreference();
screen.addPreference(mHtmlFooterPreference);
// TODO(b/171272809): Migrate to DashboardFragment.
final String title = getString(R.string.accessibility_introduction_title, mFeatureName);
mFooterPreferenceController = new AccessibilityFooterPreferenceController(
screen.getContext(), mHtmlFooterPreference.getKey());
mFooterPreferenceController.setIntroductionTitle(title);
mFooterPreferenceController.displayPreference(screen);
}
protected void updateHtmlTextPreference() {
if (mHtmlFooterPreference == null) {
return;
}
String description = getCurrentHtmlDescription().toString();
final CharSequence htmlDescription = Html.fromHtml(description,
Html.FROM_HTML_MODE_COMPACT, mImageGetter, /* tagHandler= */ null);
mHtmlFooterPreference.setSummary(htmlDescription);
}
CharSequence getCurrentHtmlDescription() {
return mHtmlDescription;
}
private void initFooterPreference() {
if (!TextUtils.isEmpty(mDescription)) {
createFooterPreference(getPreferenceScreen(), mDescription,
getString(R.string.accessibility_introduction_title, mFeatureName));
}
}
/**
* Creates {@link AccessibilityFooterPreference} and append into {@link PreferenceScreen}
*
* @param screen The preference screen to add the footer preference
* @param summary The summary of the preference summary
* @param introductionTitle The title of introduction in the footer
*/
@VisibleForTesting
void createFooterPreference(PreferenceScreen screen, CharSequence summary,
String introductionTitle) {
final AccessibilityFooterPreference footerPreference =
new AccessibilityFooterPreference(screen.getContext());
footerPreference.setSummary(summary);
screen.addPreference(footerPreference);
mFooterPreferenceController = new AccessibilityFooterPreferenceController(
screen.getContext(), footerPreference.getKey());
mFooterPreferenceController.setIntroductionTitle(introductionTitle);
mFooterPreferenceController.displayPreference(screen);
}
protected CharSequence getShortcutTypeSummary(Context context) {
if (!mShortcutPreference.isSettingsEditable()) {
return context.getText(R.string.accessibility_shortcut_edit_dialog_title_hardware);
}
if (!mShortcutPreference.isChecked()) {
return context.getText(R.string.accessibility_shortcut_state_off);
}
final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(
context, mComponentName.flattenToString(), getDefaultShortcutTypes());
return getShortcutSummaryList(context, shortcutTypes);
}
/**
* This method will be invoked when a button in the tutorial dialog is clicked.
*
* @param dialog The dialog that received the click
* @param which The button that was clicked
*/
private void callOnTutorialDialogButtonClicked(DialogInterface dialog, int which) {
dialog.dismiss();
}
protected void updateShortcutPreferenceData() {
if (mComponentName == null) {
return;
}
final int shortcutTypes = AccessibilityUtil.getUserShortcutTypesFromSettings(
getPrefContext(), mComponentName);
if (shortcutTypes != DEFAULT) {
final PreferredShortcut shortcut = new PreferredShortcut(
mComponentName.flattenToString(), shortcutTypes);
PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut);
}
}
protected void updateShortcutPreference() {
if (mComponentName == null) {
return;
}
final int shortcutTypes = getUserPreferredShortcutTypes();
mShortcutPreference.setChecked(
ShortcutUtils.isShortcutContained(
getPrefContext(), shortcutTypes, mComponentName.flattenToString()));
mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
}
protected String getShortcutPreferenceKey() {
return KEY_SHORTCUT_PREFERENCE;
}
@Override
public void onToggleClicked(ShortcutPreference preference) {
if (mComponentName == null) {
return;
}
final int shortcutTypes = getUserPreferredShortcutTypes();
final boolean isChecked = preference.isChecked();
getPrefContext().getSystemService(AccessibilityManager.class).enableShortcutsForTargets(
isChecked, shortcutTypes,
Set.of(mComponentName.flattenToString()), getPrefContext().getUserId());
if (isChecked) {
showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
}
mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
}
@Override
public void onSettingsClicked(ShortcutPreference preference) {
EditShortcutsPreferenceFragment.showEditShortcutScreen(
requireContext(), getMetricsCategory(), getShortcutTitle(),
mComponentName, getIntent());
}
/**
* Setups {@link com.android.internal.R.string#config_defaultAccessibilityService} into
* {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE} if that settings key has never
* been set and only write the key when user enter into corresponding page.
*/
@VisibleForTesting
void writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(Context context) {
if (mComponentName == null) {
return;
}
// It might be shortened form (with a leading '.'). Need to unflatten back to ComponentName
// first, or it will encounter errors when getting service from
// `ACCESSIBILITY_SHORTCUT_TARGET_SERVICE`.
final ComponentName configDefaultService = ComponentName.unflattenFromString(
getString(com.android.internal.R.string.config_defaultAccessibilityService));
if (!mComponentName.equals(configDefaultService)) {
return;
}
final String targetKey = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
final String targetString = Settings.Secure.getString(context.getContentResolver(),
targetKey);
// By intentional, we only need to write the config string when the Settings key has never
// been set (== null). Empty string also means someone already wrote it before, so we need
// to respect the value.
if (targetString == null) {
Settings.Secure.putString(context.getContentResolver(), targetKey,
configDefaultService.flattenToString());
}
}
/** Returns user visible name of the tile by given {@link ComponentName}. */
protected CharSequence loadTileLabel(Context context, ComponentName componentName) {
final PackageManager packageManager = context.getPackageManager();
final Intent queryIntent = new Intent(TileService.ACTION_QS_TILE);
final List resolveInfos =
packageManager.queryIntentServices(queryIntent, PackageManager.GET_META_DATA);
for (ResolveInfo info : resolveInfos) {
final ServiceInfo serviceInfo = info.serviceInfo;
if (TextUtils.equals(componentName.getPackageName(), serviceInfo.packageName)
&& TextUtils.equals(componentName.getClassName(), serviceInfo.name)) {
return serviceInfo.loadLabel(packageManager);
}
}
return null;
}
@VisibleForTesting
boolean isAnySetupWizard() {
return WizardManagerHelper.isAnySetupWizard(getIntent());
}
/**
* Returns the default preferred shortcut types when the user doesn't have a preferred shortcut
* types
*/
@ShortcutConstants.UserShortcutType
protected int getDefaultShortcutTypes() {
return SOFTWARE;
}
/**
* Returns the user preferred shortcut types or the default shortcut types if not set
*/
@ShortcutConstants.UserShortcutType
protected int getUserPreferredShortcutTypes() {
return PreferredShortcuts.retrieveUserShortcutType(
getPrefContext(), mComponentName.flattenToString(), getDefaultShortcutTypes());
}
@Override
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
RecyclerView recyclerView =
super.onCreateRecyclerView(inflater, parent, savedInstanceState);
return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView);
}
}