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