Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2013 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.accessibility;
     18 
     19 import android.accessibilityservice.AccessibilityServiceInfo;
     20 import android.app.Activity;
     21 import android.app.AlertDialog;
     22 import android.app.Dialog;
     23 import android.app.admin.DevicePolicyManager;
     24 import android.content.ComponentName;
     25 import android.content.Context;
     26 import android.content.DialogInterface;
     27 import android.content.Intent;
     28 import android.content.pm.ResolveInfo;
     29 import android.net.Uri;
     30 import android.os.Bundle;
     31 import android.os.Handler;
     32 import android.provider.Settings;
     33 import android.text.TextUtils;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.accessibility.AccessibilityManager;
     37 import android.widget.ImageView;
     38 import android.widget.LinearLayout;
     39 import android.widget.TextView;
     40 
     41 import com.android.internal.widget.LockPatternUtils;
     42 import com.android.settings.ConfirmDeviceCredentialActivity;
     43 import com.android.settings.R;
     44 import com.android.settings.widget.ToggleSwitch;
     45 import com.android.settings.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
     46 
     47 import java.util.Collections;
     48 import java.util.HashSet;
     49 import java.util.List;
     50 import java.util.Set;
     51 
     52 public class ToggleAccessibilityServicePreferenceFragment
     53         extends ToggleFeaturePreferenceFragment implements DialogInterface.OnClickListener {
     54 
     55     private static final int DIALOG_ID_ENABLE_WARNING = 1;
     56     private static final int DIALOG_ID_DISABLE_WARNING = 2;
     57 
     58     public static final int ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION = 1;
     59 
     60     private LockPatternUtils mLockPatternUtils;
     61 
     62     private final SettingsContentObserver mSettingsContentObserver =
     63             new SettingsContentObserver(new Handler()) {
     64             @Override
     65                 public void onChange(boolean selfChange, Uri uri) {
     66                     updateSwitchBarToggleSwitch();
     67                 }
     68             };
     69 
     70     private ComponentName mComponentName;
     71 
     72     private int mShownDialogId;
     73 
     74     @Override
     75     public void onCreate(Bundle savedInstanceState) {
     76         super.onCreate(savedInstanceState);
     77         mLockPatternUtils = new LockPatternUtils(getActivity());
     78     }
     79 
     80     @Override
     81     public void onResume() {
     82         mSettingsContentObserver.register(getContentResolver());
     83         updateSwitchBarToggleSwitch();
     84         super.onResume();
     85     }
     86 
     87     @Override
     88     public void onPause() {
     89         mSettingsContentObserver.unregister(getContentResolver());
     90         super.onPause();
     91     }
     92 
     93     @Override
     94     public void onPreferenceToggled(String preferenceKey, boolean enabled) {
     95         // Parse the enabled services.
     96         Set<ComponentName> enabledServices = AccessibilityUtils.getEnabledServicesFromSettings(
     97                 getActivity());
     98 
     99         if (enabledServices == (Set<?>) Collections.emptySet()) {
    100             enabledServices = new HashSet<ComponentName>();
    101         }
    102 
    103         // Determine enabled services and accessibility state.
    104         ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey);
    105         boolean accessibilityEnabled = false;
    106         if (enabled) {
    107             enabledServices.add(toggledService);
    108             // Enabling at least one service enables accessibility.
    109             accessibilityEnabled = true;
    110         } else {
    111             enabledServices.remove(toggledService);
    112             // Check how many enabled and installed services are present.
    113             Set<ComponentName> installedServices = AccessibilitySettings.sInstalledServices;
    114             for (ComponentName enabledService : enabledServices) {
    115                 if (installedServices.contains(enabledService)) {
    116                     // Disabling the last service disables accessibility.
    117                     accessibilityEnabled = true;
    118                     break;
    119                 }
    120             }
    121         }
    122 
    123         // Update the enabled services setting.
    124         StringBuilder enabledServicesBuilder = new StringBuilder();
    125         // Keep the enabled services even if they are not installed since we
    126         // have no way to know whether the application restore process has
    127         // completed. In general the system should be responsible for the
    128         // clean up not settings.
    129         for (ComponentName enabledService : enabledServices) {
    130             enabledServicesBuilder.append(enabledService.flattenToString());
    131             enabledServicesBuilder.append(
    132                     AccessibilitySettings.ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
    133         }
    134         final int enabledServicesBuilderLength = enabledServicesBuilder.length();
    135         if (enabledServicesBuilderLength > 0) {
    136             enabledServicesBuilder.deleteCharAt(enabledServicesBuilderLength - 1);
    137         }
    138         Settings.Secure.putString(getContentResolver(),
    139                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
    140                 enabledServicesBuilder.toString());
    141 
    142         // Update accessibility enabled.
    143         Settings.Secure.putInt(getContentResolver(),
    144                 Settings.Secure.ACCESSIBILITY_ENABLED, accessibilityEnabled ? 1 : 0);
    145     }
    146 
    147     // IMPORTANT: Refresh the info since there are dynamically changing
    148     // capabilities. For
    149     // example, before JellyBean MR2 the user was granting the explore by touch
    150     // one.
    151     private AccessibilityServiceInfo getAccessibilityServiceInfo() {
    152         List<AccessibilityServiceInfo> serviceInfos = AccessibilityManager.getInstance(
    153                 getActivity()).getInstalledAccessibilityServiceList();
    154         final int serviceInfoCount = serviceInfos.size();
    155         for (int i = 0; i < serviceInfoCount; i++) {
    156             AccessibilityServiceInfo serviceInfo = serviceInfos.get(i);
    157             ResolveInfo resolveInfo = serviceInfo.getResolveInfo();
    158             if (mComponentName.getPackageName().equals(resolveInfo.serviceInfo.packageName)
    159                     && mComponentName.getClassName().equals(resolveInfo.serviceInfo.name)) {
    160                 return serviceInfo;
    161             }
    162         }
    163         return null;
    164     }
    165 
    166     @Override
    167     public Dialog onCreateDialog(int dialogId) {
    168         switch (dialogId) {
    169             case DIALOG_ID_ENABLE_WARNING: {
    170                 mShownDialogId = DIALOG_ID_ENABLE_WARNING;
    171                 AccessibilityServiceInfo info = getAccessibilityServiceInfo();
    172                 if (info == null) {
    173                     return null;
    174                 }
    175                 AlertDialog ad = new AlertDialog.Builder(getActivity())
    176                         .setTitle(getString(R.string.enable_service_title,
    177                                 info.getResolveInfo().loadLabel(getPackageManager())))
    178                         .setView(createEnableDialogContentView(info))
    179                         .setCancelable(true)
    180                         .setPositiveButton(android.R.string.ok, this)
    181                         .setNegativeButton(android.R.string.cancel, this)
    182                         .create();
    183                 ad.create();
    184                 ad.getButton(AlertDialog.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
    185                 return ad;
    186             }
    187             case DIALOG_ID_DISABLE_WARNING: {
    188                 mShownDialogId = DIALOG_ID_DISABLE_WARNING;
    189                 AccessibilityServiceInfo info = getAccessibilityServiceInfo();
    190                 if (info == null) {
    191                     return null;
    192                 }
    193                 return new AlertDialog.Builder(getActivity())
    194                         .setTitle(getString(R.string.disable_service_title,
    195                                 info.getResolveInfo().loadLabel(getPackageManager())))
    196                         .setMessage(getString(R.string.disable_service_message,
    197                                 info.getResolveInfo().loadLabel(getPackageManager())))
    198                         .setCancelable(true)
    199                         .setPositiveButton(android.R.string.ok, this)
    200                         .setNegativeButton(android.R.string.cancel, this)
    201                         .create();
    202             }
    203             default: {
    204                 throw new IllegalArgumentException();
    205             }
    206         }
    207     }
    208 
    209     private void updateSwitchBarToggleSwitch() {
    210         final String settingValue = Settings.Secure.getString(getContentResolver(),
    211                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
    212         final boolean checked = settingValue != null
    213                 && settingValue.contains(mComponentName.flattenToString());
    214         mSwitchBar.setCheckedInternal(checked);
    215     }
    216 
    217     private View createEnableDialogContentView(AccessibilityServiceInfo info) {
    218         LayoutInflater inflater = (LayoutInflater) getSystemService(
    219                 Context.LAYOUT_INFLATER_SERVICE);
    220 
    221         View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content,
    222                 null);
    223 
    224         TextView encryptionWarningView = (TextView) content.findViewById(
    225                 R.id.encryption_warning);
    226         if (LockPatternUtils.isDeviceEncrypted()) {
    227             String text = getString(R.string.enable_service_encryption_warning,
    228                     info.getResolveInfo().loadLabel(getPackageManager()));
    229             encryptionWarningView.setText(text);
    230             encryptionWarningView.setVisibility(View.VISIBLE);
    231         } else {
    232             encryptionWarningView.setVisibility(View.GONE);
    233         }
    234 
    235         TextView capabilitiesHeaderView = (TextView) content.findViewById(
    236                 R.id.capabilities_header);
    237         capabilitiesHeaderView.setText(getString(R.string.capabilities_list_title,
    238                 info.getResolveInfo().loadLabel(getPackageManager())));
    239 
    240         LinearLayout capabilitiesView = (LinearLayout) content.findViewById(R.id.capabilities);
    241 
    242         // This capability is implicit for all services.
    243         View capabilityView = inflater.inflate(
    244                 com.android.internal.R.layout.app_permission_item_old, null);
    245 
    246         ImageView imageView = (ImageView) capabilityView.findViewById(
    247                 com.android.internal.R.id.perm_icon);
    248         imageView.setImageDrawable(getActivity().getDrawable(
    249                 com.android.internal.R.drawable.ic_text_dot));
    250 
    251         TextView labelView = (TextView) capabilityView.findViewById(
    252                 com.android.internal.R.id.permission_group);
    253         labelView.setText(getString(R.string.capability_title_receiveAccessibilityEvents));
    254 
    255         TextView descriptionView = (TextView) capabilityView.findViewById(
    256                 com.android.internal.R.id.permission_list);
    257         descriptionView.setText(getString(R.string.capability_desc_receiveAccessibilityEvents));
    258 
    259         List<AccessibilityServiceInfo.CapabilityInfo> capabilities =
    260                 info.getCapabilityInfos();
    261 
    262         capabilitiesView.addView(capabilityView);
    263 
    264         // Service specific capabilities.
    265         final int capabilityCount = capabilities.size();
    266         for (int i = 0; i < capabilityCount; i++) {
    267             AccessibilityServiceInfo.CapabilityInfo capability = capabilities.get(i);
    268 
    269             capabilityView = inflater.inflate(
    270                     com.android.internal.R.layout.app_permission_item_old, null);
    271 
    272             imageView = (ImageView) capabilityView.findViewById(
    273                     com.android.internal.R.id.perm_icon);
    274             imageView.setImageDrawable(getActivity().getDrawable(
    275                     com.android.internal.R.drawable.ic_text_dot));
    276 
    277             labelView = (TextView) capabilityView.findViewById(
    278                     com.android.internal.R.id.permission_group);
    279             labelView.setText(getString(capability.titleResId));
    280 
    281             descriptionView = (TextView) capabilityView.findViewById(
    282                     com.android.internal.R.id.permission_list);
    283             descriptionView.setText(getString(capability.descResId));
    284 
    285             capabilitiesView.addView(capabilityView);
    286         }
    287 
    288         return content;
    289     }
    290 
    291     @Override
    292     public void onActivityResult(int requestCode, int resultCode, Intent data) {
    293         if (requestCode == ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION) {
    294             if (resultCode == Activity.RESULT_OK) {
    295                 handleConfirmServiceEnabled(true);
    296                 // The user confirmed that they accept weaker encryption when
    297                 // enabling the accessibility service, so change encryption.
    298                 // Since we came here asynchronously, check encryption again.
    299                 if (LockPatternUtils.isDeviceEncrypted()) {
    300                     mLockPatternUtils.clearEncryptionPassword();
    301                     Settings.Global.putInt(getContentResolver(),
    302                             Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, 0);
    303                 }
    304             } else {
    305                 handleConfirmServiceEnabled(false);
    306             }
    307         }
    308     }
    309 
    310     @Override
    311     public void onClick(DialogInterface dialog, int which) {
    312         final boolean checked;
    313         switch (which) {
    314             case DialogInterface.BUTTON_POSITIVE:
    315                 if (mShownDialogId == DIALOG_ID_ENABLE_WARNING) {
    316                     if (LockPatternUtils.isDeviceEncrypted()) {
    317                         String title = createConfirmCredentialReasonMessage();
    318                         Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, null);
    319                         startActivityForResult(intent,
    320                                 ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION);
    321                     } else {
    322                         handleConfirmServiceEnabled(true);
    323                     }
    324                 } else {
    325                     handleConfirmServiceEnabled(false);
    326                 }
    327                 break;
    328             case DialogInterface.BUTTON_NEGATIVE:
    329                 checked = (mShownDialogId == DIALOG_ID_DISABLE_WARNING);
    330                 handleConfirmServiceEnabled(checked);
    331                 break;
    332             default:
    333                 throw new IllegalArgumentException();
    334         }
    335     }
    336 
    337     private void handleConfirmServiceEnabled(boolean confirmed) {
    338         mSwitchBar.setCheckedInternal(confirmed);
    339         getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed);
    340         onPreferenceToggled(mPreferenceKey, confirmed);
    341     }
    342 
    343     private String createConfirmCredentialReasonMessage() {
    344         int resId = R.string.enable_service_password_reason;
    345         switch (mLockPatternUtils.getKeyguardStoredPasswordQuality()) {
    346             case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: {
    347                 resId = R.string.enable_service_pattern_reason;
    348             } break;
    349             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
    350             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: {
    351                 resId = R.string.enable_service_pin_reason;
    352             } break;
    353         }
    354         return getString(resId, getAccessibilityServiceInfo().getResolveInfo()
    355                 .loadLabel(getPackageManager()));
    356     }
    357 
    358     @Override
    359     protected void onInstallSwitchBarToggleSwitch() {
    360         super.onInstallSwitchBarToggleSwitch();
    361         mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() {
    362                 @Override
    363             public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) {
    364                 if (checked) {
    365                     mSwitchBar.setCheckedInternal(false);
    366                     getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, false);
    367                     showDialog(DIALOG_ID_ENABLE_WARNING);
    368                 } else {
    369                     mSwitchBar.setCheckedInternal(true);
    370                     getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, true);
    371                     showDialog(DIALOG_ID_DISABLE_WARNING);
    372                 }
    373                 return true;
    374             }
    375         });
    376     }
    377 
    378     @Override
    379     protected void onProcessArguments(Bundle arguments) {
    380         super.onProcessArguments(arguments);
    381         // Settings title and intent.
    382         String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE);
    383         String settingsComponentName = arguments.getString(
    384                 AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME);
    385         if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) {
    386             Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent(
    387                     ComponentName.unflattenFromString(settingsComponentName.toString()));
    388             if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) {
    389                 mSettingsTitle = settingsTitle;
    390                 mSettingsIntent = settingsIntent;
    391                 setHasOptionsMenu(true);
    392             }
    393         }
    394 
    395         mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME);
    396     }
    397 }
    398