| /* | |
| * Copyright (C) 2013 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.android.settings.accessibility; | |
| import static com.android.settings.accessibility.AccessibilityDialogUtils.DialogEnums; | |
| import static com.android.settings.accessibility.AccessibilityStatsLogUtils.logAccessibilityServiceEnabled; | |
| import android.accessibilityservice.AccessibilityServiceInfo; | |
| import android.annotation.SuppressLint; | |
| import android.app.AlertDialog; | |
| import android.app.Dialog; | |
| import android.app.settings.SettingsEnums; | |
| import android.content.BroadcastReceiver; | |
| import android.content.ComponentName; | |
| import android.content.ContentResolver; | |
| import android.content.Context; | |
| import android.content.DialogInterface; | |
| import android.content.Intent; | |
| import android.content.IntentFilter; | |
| import android.content.pm.ApplicationInfo; | |
| import android.content.pm.ResolveInfo; | |
| import android.net.Uri; | |
| import android.os.Bundle; | |
| import android.os.SystemClock; | |
| import android.text.BidiFormatter; | |
| import android.text.TextUtils; | |
| import android.util.Log; | |
| import android.view.accessibility.AccessibilityManager; | |
| import android.widget.CompoundButton; | |
| import androidx.annotation.Nullable; | |
| import com.android.internal.accessibility.common.ShortcutConstants; | |
| import com.android.settings.R; | |
| import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment; | |
| import com.android.settingslib.accessibility.AccessibilityUtils; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Set; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| /** Fragment for providing toggle bar and basic accessibility service setup. */ | |
| public class ToggleAccessibilityServicePreferenceFragment extends | |
| ToggleFeaturePreferenceFragment { | |
| private static final String TAG = "ToggleAccessibilityServicePreferenceFragment"; | |
| private static final String KEY_HAS_LOGGED = "has_logged"; | |
| private final AtomicBoolean mIsDialogShown = new AtomicBoolean(/* initialValue= */ false); | |
| private Dialog mWarningDialog; | |
| private ComponentName mTileComponentName; | |
| private BroadcastReceiver mPackageRemovedReceiver; | |
| private boolean mDisabledStateLogged = false; | |
| private long mStartTimeMillsForLogging = 0; | |
| @Override | |
| public int getMetricsCategory() { | |
| return getArguments().getInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY); | |
| } | |
| @Override | |
| public int getFeedbackCategory() { | |
| return getArguments().getInt(AccessibilitySettings.EXTRA_FEEDBACK_CATEGORY); | |
| } | |
| @Override | |
| public void onCreate(Bundle savedInstanceState) { | |
| super.onCreate(savedInstanceState); | |
| if (savedInstanceState != null) { | |
| if (savedInstanceState.containsKey(KEY_HAS_LOGGED)) { | |
| mDisabledStateLogged = savedInstanceState.getBoolean(KEY_HAS_LOGGED); | |
| } | |
| } | |
| } | |
| @Override | |
| protected void registerKeysToObserverCallback( | |
| AccessibilitySettingsContentObserver contentObserver) { | |
| super.registerKeysToObserverCallback(contentObserver); | |
| contentObserver.registerObserverCallback(key -> updateSwitchBarToggleSwitch()); | |
| } | |
| @Override | |
| public void onStart() { | |
| super.onStart(); | |
| final AccessibilityServiceInfo serviceInfo = getAccessibilityServiceInfo(); | |
| if (serviceInfo == null) { | |
| getActivity().finishAndRemoveTask(); | |
| } else if (!AccessibilityUtil.isSystemApp(serviceInfo)) { | |
| registerPackageRemoveReceiver(); | |
| } | |
| } | |
| @Override | |
| public void onResume() { | |
| super.onResume(); | |
| updateSwitchBarToggleSwitch(); | |
| } | |
| @Override | |
| public void onSaveInstanceState(Bundle outState) { | |
| if (mStartTimeMillsForLogging > 0) { | |
| outState.putBoolean(KEY_HAS_LOGGED, mDisabledStateLogged); | |
| } | |
| super.onSaveInstanceState(outState); | |
| } | |
| @Override | |
| public void onPreferenceToggled(String preferenceKey, boolean enabled) { | |
| ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey); | |
| logAccessibilityServiceEnabled(toggledService, enabled); | |
| if (!enabled) { | |
| logDisabledState(toggledService.getPackageName()); | |
| } | |
| AccessibilityUtils.setAccessibilityServiceState(getPrefContext(), toggledService, enabled); | |
| } | |
| // IMPORTANT: Refresh the info since there are dynamically changing capabilities. For | |
| // example, before JellyBean MR2 the user was granting the explore by touch one. | |
| @Nullable | |
| AccessibilityServiceInfo getAccessibilityServiceInfo() { | |
| final List<AccessibilityServiceInfo> infos = AccessibilityManager.getInstance( | |
| getPrefContext()).getInstalledAccessibilityServiceList(); | |
| for (int i = 0, count = infos.size(); i < count; i++) { | |
| AccessibilityServiceInfo serviceInfo = infos.get(i); | |
| ResolveInfo resolveInfo = serviceInfo.getResolveInfo(); | |
| if (mComponentName.getPackageName().equals(resolveInfo.serviceInfo.packageName) | |
| && mComponentName.getClassName().equals(resolveInfo.serviceInfo.name)) { | |
| return serviceInfo; | |
| } | |
| } | |
| return null; | |
| } | |
| @Override | |
| public Dialog onCreateDialog(int dialogId) { | |
| final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); | |
| switch (dialogId) { | |
| case DialogEnums.ENABLE_WARNING_FROM_TOGGLE: | |
| if (info == null) { | |
| return null; | |
| } | |
| mWarningDialog = | |
| com.android.internal.accessibility.dialog.AccessibilityServiceWarning | |
| .createAccessibilityServiceWarningDialog(getPrefContext(), info, | |
| v -> onAllowButtonFromEnableToggleClicked(), | |
| v -> onDenyButtonFromEnableToggleClicked(), | |
| v -> onDialogButtonFromUninstallClicked()); | |
| return mWarningDialog; | |
| case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE: | |
| if (info == null) { | |
| return null; | |
| } | |
| mWarningDialog = | |
| com.android.internal.accessibility.dialog.AccessibilityServiceWarning | |
| .createAccessibilityServiceWarningDialog(getPrefContext(), info, | |
| v -> onAllowButtonFromShortcutToggleClicked(), | |
| v -> onDenyButtonFromShortcutToggleClicked(), | |
| v -> onDialogButtonFromUninstallClicked()); | |
| return mWarningDialog; | |
| case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT: | |
| if (info == null) { | |
| return null; | |
| } | |
| mWarningDialog = | |
| com.android.internal.accessibility.dialog.AccessibilityServiceWarning | |
| .createAccessibilityServiceWarningDialog(getPrefContext(), info, | |
| v -> onAllowButtonFromShortcutClicked(), | |
| v -> onDenyButtonFromShortcutClicked(), | |
| v -> onDialogButtonFromUninstallClicked()); | |
| return mWarningDialog; | |
| case DialogEnums.DISABLE_WARNING_FROM_TOGGLE: | |
| if (info == null) { | |
| return null; | |
| } | |
| mWarningDialog = createDisableDialog( | |
| getPrefContext(), info, this::onDialogButtonFromDisableToggleClicked); | |
| return mWarningDialog; | |
| default: | |
| return super.onCreateDialog(dialogId); | |
| } | |
| } | |
| /** Returns a {@link Dialog} to be shown to confirm that they want to disable a service. */ | |
| private static Dialog createDisableDialog(Context context, | |
| AccessibilityServiceInfo info, DialogInterface.OnClickListener listener) { | |
| final Locale locale = context.getResources().getConfiguration().getLocales().get(0); | |
| final CharSequence label = | |
| info.getResolveInfo().loadLabel(context.getPackageManager()); | |
| CharSequence serviceName = BidiFormatter.getInstance(locale).unicodeWrap(label); | |
| return new AlertDialog.Builder(context) | |
| .setTitle(context.getString(R.string.disable_service_title, serviceName)) | |
| .setCancelable(true) | |
| .setPositiveButton(R.string.accessibility_dialog_button_stop, listener) | |
| .setNegativeButton(R.string.accessibility_dialog_button_cancel, listener) | |
| .create(); | |
| } | |
| @Override | |
| public int getDialogMetricsCategory(int dialogId) { | |
| switch (dialogId) { | |
| case DialogEnums.ENABLE_WARNING_FROM_TOGGLE: | |
| case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT: | |
| case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE: | |
| return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_ENABLE; | |
| case DialogEnums.DISABLE_WARNING_FROM_TOGGLE: | |
| return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_DISABLE; | |
| case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: | |
| return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL; | |
| default: | |
| return super.getDialogMetricsCategory(dialogId); | |
| } | |
| } | |
| @Override | |
| int getUserShortcutTypes() { | |
| return AccessibilityUtil.getUserShortcutTypesFromSettings(getPrefContext(), | |
| mComponentName); | |
| } | |
| @Override | |
| ComponentName getTileComponentName() { | |
| return mTileComponentName; | |
| } | |
| @Override | |
| protected void updateSwitchBarToggleSwitch() { | |
| final boolean checked = isAccessibilityServiceEnabled(); | |
| if (mToggleServiceSwitchPreference.isChecked() == checked) { | |
| return; | |
| } | |
| mToggleServiceSwitchPreference.setChecked(checked); | |
| } | |
| private boolean isAccessibilityServiceEnabled() { | |
| return AccessibilityUtils.getEnabledServicesFromSettings(getPrefContext()) | |
| .contains(mComponentName); | |
| } | |
| @Override | |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { | |
| } | |
| private void registerPackageRemoveReceiver() { | |
| if (mPackageRemovedReceiver != null || getContext() == null) { | |
| return; | |
| } | |
| mPackageRemovedReceiver = new BroadcastReceiver() { | |
| @Override | |
| public void onReceive(Context context, Intent intent) { | |
| final String packageName = intent.getData().getSchemeSpecificPart(); | |
| if (TextUtils.equals(mComponentName.getPackageName(), packageName)) { | |
| getActivity().finishAndRemoveTask(); | |
| } | |
| } | |
| }; | |
| final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED); | |
| filter.addDataScheme("package"); | |
| getContext().registerReceiver(mPackageRemovedReceiver, filter); | |
| } | |
| private void unregisterPackageRemoveReceiver() { | |
| if (mPackageRemovedReceiver == null || getContext() == null) { | |
| return; | |
| } | |
| getContext().unregisterReceiver(mPackageRemovedReceiver); | |
| mPackageRemovedReceiver = null; | |
| } | |
| boolean serviceSupportsAccessibilityButton() { | |
| final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); | |
| return info != null | |
| && (info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; | |
| } | |
| private void handleConfirmServiceEnabled(boolean confirmed) { | |
| getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed); | |
| onPreferenceToggled(mPreferenceKey, confirmed); | |
| } | |
| @Override | |
| public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { | |
| if (isChecked != isAccessibilityServiceEnabled()) { | |
| onPreferenceClick(isChecked); | |
| } | |
| } | |
| @SuppressLint("MissingPermission") | |
| @Override | |
| public void onToggleClicked(ShortcutPreference preference) { | |
| final int shortcutTypes = getUserPreferredShortcutTypes(); | |
| if (preference.isChecked()) { | |
| final boolean isWarningRequired = | |
| getPrefContext().getSystemService(AccessibilityManager.class) | |
| .isAccessibilityServiceWarningRequired(getAccessibilityServiceInfo()); | |
| if (isWarningRequired) { | |
| preference.setChecked(false); | |
| showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE); | |
| } else { | |
| onAllowButtonFromShortcutToggleClicked(); | |
| } | |
| } else { | |
| getPrefContext().getSystemService(AccessibilityManager.class) | |
| .enableShortcutsForTargets(false, shortcutTypes, | |
| Set.of(mComponentName.flattenToString()), | |
| getPrefContext().getUserId()); | |
| } | |
| mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); | |
| } | |
| @Override | |
| public void onSettingsClicked(ShortcutPreference preference) { | |
| final boolean isWarningRequired = | |
| getPrefContext().getSystemService(AccessibilityManager.class) | |
| .isAccessibilityServiceWarningRequired(getAccessibilityServiceInfo()); | |
| if (isWarningRequired) { | |
| showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_SHORTCUT); | |
| } else { | |
| onAllowButtonFromShortcutClicked(); | |
| } | |
| } | |
| @Override | |
| protected void onProcessArguments(Bundle arguments) { | |
| super.onProcessArguments(arguments); | |
| // Settings title and intent. | |
| String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE); | |
| String settingsComponentName = arguments.getString( | |
| AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME); | |
| if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) { | |
| Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent( | |
| ComponentName.unflattenFromString(settingsComponentName.toString())); | |
| if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) { | |
| mSettingsTitle = settingsTitle; | |
| mSettingsIntent = settingsIntent; | |
| setHasOptionsMenu(true); | |
| } | |
| } | |
| mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME); | |
| // Settings animated image. | |
| final int animatedImageRes = arguments.getInt( | |
| AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES); | |
| if (animatedImageRes > 0) { | |
| mImageUri = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) | |
| .authority(mComponentName.getPackageName()) | |
| .appendPath(String.valueOf(animatedImageRes)) | |
| .build(); | |
| } | |
| // Get Accessibility service name. | |
| AccessibilityServiceInfo info = getAccessibilityServiceInfo(); | |
| mFeatureName = info == null ? "" : info.getResolveInfo().loadLabel(getPackageManager()); | |
| if (arguments.containsKey(AccessibilitySettings.EXTRA_TILE_SERVICE_COMPONENT_NAME)) { | |
| final String tileServiceComponentName = arguments.getString( | |
| AccessibilitySettings.EXTRA_TILE_SERVICE_COMPONENT_NAME); | |
| mTileComponentName = ComponentName.unflattenFromString(tileServiceComponentName); | |
| } | |
| mStartTimeMillsForLogging = arguments.getLong(AccessibilitySettings.EXTRA_TIME_FOR_LOGGING); | |
| } | |
| private void onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which) { | |
| switch (which) { | |
| case DialogInterface.BUTTON_POSITIVE: | |
| handleConfirmServiceEnabled(/* confirmed= */ false); | |
| break; | |
| case DialogInterface.BUTTON_NEGATIVE: | |
| handleConfirmServiceEnabled(/* confirmed= */ true); | |
| break; | |
| default: | |
| throw new IllegalArgumentException("Unexpected button identifier"); | |
| } | |
| } | |
| private void onDialogButtonFromUninstallClicked() { | |
| mWarningDialog.dismiss(); | |
| final Intent uninstallIntent = createUninstallPackageActivityIntent(); | |
| if (uninstallIntent == null) { | |
| return; | |
| } | |
| startActivity(uninstallIntent); | |
| } | |
| @Nullable | |
| private Intent createUninstallPackageActivityIntent() { | |
| final AccessibilityServiceInfo a11yServiceInfo = getAccessibilityServiceInfo(); | |
| if (a11yServiceInfo == null) { | |
| Log.w(TAG, "createUnInstallIntent -- invalid a11yServiceInfo"); | |
| return null; | |
| } | |
| final ApplicationInfo appInfo = | |
| a11yServiceInfo.getResolveInfo().serviceInfo.applicationInfo; | |
| final Uri packageUri = Uri.parse("package:" + appInfo.packageName); | |
| final Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); | |
| return uninstallIntent; | |
| } | |
| @Override | |
| public void onStop() { | |
| super.onStop(); | |
| unregisterPackageRemoveReceiver(); | |
| } | |
| @Override | |
| protected int getPreferenceScreenResId() { | |
| // TODO(b/171272809): Add back when controllers move to static type | |
| return 0; | |
| } | |
| @Override | |
| protected String getLogTag() { | |
| return TAG; | |
| } | |
| @Override | |
| protected int getDefaultShortcutTypes() { | |
| AccessibilityServiceInfo info = getAccessibilityServiceInfo(); | |
| boolean isAccessibilityTool = info != null && info.isAccessibilityTool(); | |
| return !isAccessibilityTool || getTileComponentName() == null | |
| ? super.getDefaultShortcutTypes() | |
| : ShortcutConstants.UserShortcutType.QUICK_SETTINGS; | |
| } | |
| private void onAllowButtonFromEnableToggleClicked() { | |
| handleConfirmServiceEnabled(/* confirmed= */ true); | |
| if (serviceSupportsAccessibilityButton()) { | |
| mIsDialogShown.set(false); | |
| showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); | |
| } | |
| if (mWarningDialog != null) { | |
| mWarningDialog.dismiss(); | |
| } | |
| } | |
| private void onDenyButtonFromEnableToggleClicked() { | |
| handleConfirmServiceEnabled(/* confirmed= */ false); | |
| mWarningDialog.dismiss(); | |
| } | |
| @SuppressLint("MissingPermission") | |
| void onAllowButtonFromShortcutToggleClicked() { | |
| mShortcutPreference.setChecked(true); | |
| final int shortcutTypes = getUserPreferredShortcutTypes(); | |
| getPrefContext().getSystemService(AccessibilityManager.class) | |
| .enableShortcutsForTargets(true, shortcutTypes, | |
| Set.of(mComponentName.flattenToString()), getPrefContext().getUserId()); | |
| mIsDialogShown.set(false); | |
| showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); | |
| if (mWarningDialog != null) { | |
| mWarningDialog.dismiss(); | |
| } | |
| mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); | |
| } | |
| private void onDenyButtonFromShortcutToggleClicked() { | |
| mShortcutPreference.setChecked(false); | |
| mWarningDialog.dismiss(); | |
| } | |
| private void onAllowButtonFromShortcutClicked() { | |
| mIsDialogShown.set(false); | |
| EditShortcutsPreferenceFragment.showEditShortcutScreen( | |
| getContext(), | |
| getMetricsCategory(), | |
| getShortcutTitle(), | |
| mComponentName, | |
| getIntent() | |
| ); | |
| if (mWarningDialog != null) { | |
| mWarningDialog.dismiss(); | |
| } | |
| } | |
| private void onDenyButtonFromShortcutClicked() { | |
| mWarningDialog.dismiss(); | |
| } | |
| private boolean onPreferenceClick(boolean isChecked) { | |
| if (isChecked) { | |
| mToggleServiceSwitchPreference.setChecked(false); | |
| getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, | |
| /* disableService */ false); | |
| final boolean isWarningRequired = | |
| getPrefContext().getSystemService(AccessibilityManager.class) | |
| .isAccessibilityServiceWarningRequired(getAccessibilityServiceInfo()); | |
| if (isWarningRequired) { | |
| showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_TOGGLE); | |
| } else { | |
| onAllowButtonFromEnableToggleClicked(); | |
| } | |
| } else { | |
| mToggleServiceSwitchPreference.setChecked(true); | |
| getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, | |
| /* enableService */ true); | |
| showDialog(DialogEnums.DISABLE_WARNING_FROM_TOGGLE); | |
| } | |
| return true; | |
| } | |
| private void showPopupDialog(int dialogId) { | |
| if (mIsDialogShown.compareAndSet(/* expect= */ false, /* update= */ true)) { | |
| showDialog(dialogId); | |
| setOnDismissListener( | |
| dialog -> mIsDialogShown.compareAndSet(/* expect= */ true, /* update= */ | |
| false)); | |
| } | |
| } | |
| private void logDisabledState(String packageName) { | |
| if (mStartTimeMillsForLogging > 0 && !mDisabledStateLogged) { | |
| AccessibilityStatsLogUtils.logDisableNonA11yCategoryService( | |
| packageName, | |
| SystemClock.elapsedRealtime() - mStartTimeMillsForLogging); | |
| mDisabledStateLogged = true; | |
| } | |
| } | |
| } |