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.AlertDialog; 21 import android.app.Dialog; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.content.pm.ResolveInfo; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.provider.Settings; 31 import android.text.TextUtils; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.accessibility.AccessibilityManager; 35 import android.widget.ImageView; 36 import android.widget.LinearLayout; 37 import android.widget.TextView; 38 39 import com.android.settings.R; 40 import com.android.settings.accessibility.ToggleSwitch.OnBeforeCheckedChangeListener; 41 42 import java.util.Collections; 43 import java.util.HashSet; 44 import java.util.List; 45 import java.util.Set; 46 47 public class ToggleAccessibilityServicePreferenceFragment 48 extends ToggleFeaturePreferenceFragment implements DialogInterface.OnClickListener { 49 50 private static final int DIALOG_ID_ENABLE_WARNING = 1; 51 private static final int DIALOG_ID_DISABLE_WARNING = 2; 52 53 private final SettingsContentObserver mSettingsContentObserver = 54 new SettingsContentObserver(new Handler()) { 55 @Override 56 public void onChange(boolean selfChange, Uri uri) { 57 String settingValue = Settings.Secure.getString(getContentResolver(), 58 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 59 final boolean enabled = settingValue.contains(mComponentName.flattenToString()); 60 mToggleSwitch.setCheckedInternal(enabled); 61 } 62 }; 63 64 private ComponentName mComponentName; 65 66 private int mShownDialogId; 67 68 @Override 69 public void onResume() { 70 mSettingsContentObserver.register(getContentResolver()); 71 super.onResume(); 72 } 73 74 @Override 75 public void onPause() { 76 mSettingsContentObserver.unregister(getContentResolver()); 77 super.onPause(); 78 } 79 80 @Override 81 public void onPreferenceToggled(String preferenceKey, boolean enabled) { 82 // Parse the enabled services. 83 Set<ComponentName> enabledServices = AccessibilityUtils.getEnabledServicesFromSettings( 84 getActivity()); 85 86 if (enabledServices == (Set<?>) Collections.emptySet()) { 87 enabledServices = new HashSet<ComponentName>(); 88 } 89 90 // Determine enabled services and accessibility state. 91 ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey); 92 boolean accessibilityEnabled = false; 93 if (enabled) { 94 enabledServices.add(toggledService); 95 // Enabling at least one service enables accessibility. 96 accessibilityEnabled = true; 97 } else { 98 enabledServices.remove(toggledService); 99 // Check how many enabled and installed services are present. 100 Set<ComponentName> installedServices = AccessibilitySettings.sInstalledServices; 101 for (ComponentName enabledService : enabledServices) { 102 if (installedServices.contains(enabledService)) { 103 // Disabling the last service disables accessibility. 104 accessibilityEnabled = true; 105 break; 106 } 107 } 108 } 109 110 // Update the enabled services setting. 111 StringBuilder enabledServicesBuilder = new StringBuilder(); 112 // Keep the enabled services even if they are not installed since we 113 // have no way to know whether the application restore process has 114 // completed. In general the system should be responsible for the 115 // clean up not settings. 116 for (ComponentName enabledService : enabledServices) { 117 enabledServicesBuilder.append(enabledService.flattenToString()); 118 enabledServicesBuilder.append( 119 AccessibilitySettings.ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR); 120 } 121 final int enabledServicesBuilderLength = enabledServicesBuilder.length(); 122 if (enabledServicesBuilderLength > 0) { 123 enabledServicesBuilder.deleteCharAt(enabledServicesBuilderLength - 1); 124 } 125 Settings.Secure.putString(getContentResolver(), 126 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 127 enabledServicesBuilder.toString()); 128 129 // Update accessibility enabled. 130 Settings.Secure.putInt(getContentResolver(), 131 Settings.Secure.ACCESSIBILITY_ENABLED, accessibilityEnabled ? 1 : 0); 132 } 133 134 // IMPORTANT: Refresh the info since there are dynamically changing 135 // capabilities. For 136 // example, before JellyBean MR2 the user was granting the explore by touch 137 // one. 138 private AccessibilityServiceInfo getAccessibilityServiceInfo() { 139 List<AccessibilityServiceInfo> serviceInfos = AccessibilityManager.getInstance( 140 getActivity()).getInstalledAccessibilityServiceList(); 141 final int serviceInfoCount = serviceInfos.size(); 142 for (int i = 0; i < serviceInfoCount; i++) { 143 AccessibilityServiceInfo serviceInfo = serviceInfos.get(i); 144 ResolveInfo resolveInfo = serviceInfo.getResolveInfo(); 145 if (mComponentName.getPackageName().equals(resolveInfo.serviceInfo.packageName) 146 && mComponentName.getClassName().equals(resolveInfo.serviceInfo.name)) { 147 return serviceInfo; 148 } 149 } 150 return null; 151 } 152 153 @Override 154 public Dialog onCreateDialog(int dialogId) { 155 switch (dialogId) { 156 case DIALOG_ID_ENABLE_WARNING: { 157 mShownDialogId = DIALOG_ID_ENABLE_WARNING; 158 AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 159 if (info == null) { 160 return null; 161 } 162 return new AlertDialog.Builder(getActivity()) 163 .setTitle(getString(R.string.enable_service_title, 164 info.getResolveInfo().loadLabel(getPackageManager()))) 165 .setIconAttribute(android.R.attr.alertDialogIcon) 166 .setView(createEnableDialogContentView(info)) 167 .setCancelable(true) 168 .setPositiveButton(android.R.string.ok, this) 169 .setNegativeButton(android.R.string.cancel, this) 170 .create(); 171 } 172 case DIALOG_ID_DISABLE_WARNING: { 173 mShownDialogId = DIALOG_ID_DISABLE_WARNING; 174 AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 175 if (info == null) { 176 return null; 177 } 178 return new AlertDialog.Builder(getActivity()) 179 .setTitle(getString(R.string.disable_service_title, 180 info.getResolveInfo().loadLabel(getPackageManager()))) 181 .setIconAttribute(android.R.attr.alertDialogIcon) 182 .setMessage(getString(R.string.disable_service_message, 183 info.getResolveInfo().loadLabel(getPackageManager()))) 184 .setCancelable(true) 185 .setPositiveButton(android.R.string.ok, this) 186 .setNegativeButton(android.R.string.cancel, this) 187 .create(); 188 } 189 default: { 190 throw new IllegalArgumentException(); 191 } 192 } 193 } 194 195 private View createEnableDialogContentView(AccessibilityServiceInfo info) { 196 LayoutInflater inflater = (LayoutInflater) getSystemService( 197 Context.LAYOUT_INFLATER_SERVICE); 198 199 View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content, 200 null); 201 202 TextView capabilitiesHeaderView = (TextView) content.findViewById( 203 R.id.capabilities_header); 204 capabilitiesHeaderView.setText(getString(R.string.capabilities_list_title, 205 info.getResolveInfo().loadLabel(getPackageManager()))); 206 207 LinearLayout capabilitiesView = (LinearLayout) content.findViewById(R.id.capabilities); 208 209 // This capability is implicit for all services. 210 View capabilityView = inflater.inflate( 211 com.android.internal.R.layout.app_permission_item_old, null); 212 213 ImageView imageView = (ImageView) capabilityView.findViewById( 214 com.android.internal.R.id.perm_icon); 215 imageView.setImageDrawable(getResources().getDrawable( 216 com.android.internal.R.drawable.ic_text_dot)); 217 218 TextView labelView = (TextView) capabilityView.findViewById( 219 com.android.internal.R.id.permission_group); 220 labelView.setText(getString(R.string.capability_title_receiveAccessibilityEvents)); 221 222 TextView descriptionView = (TextView) capabilityView.findViewById( 223 com.android.internal.R.id.permission_list); 224 descriptionView.setText(getString(R.string.capability_desc_receiveAccessibilityEvents)); 225 226 List<AccessibilityServiceInfo.CapabilityInfo> capabilities = 227 info.getCapabilityInfos(); 228 229 capabilitiesView.addView(capabilityView); 230 231 // Service specific capabilities. 232 final int capabilityCount = capabilities.size(); 233 for (int i = 0; i < capabilityCount; i++) { 234 AccessibilityServiceInfo.CapabilityInfo capability = capabilities.get(i); 235 236 capabilityView = inflater.inflate( 237 com.android.internal.R.layout.app_permission_item_old, null); 238 239 imageView = (ImageView) capabilityView.findViewById( 240 com.android.internal.R.id.perm_icon); 241 imageView.setImageDrawable(getResources().getDrawable( 242 com.android.internal.R.drawable.ic_text_dot)); 243 244 labelView = (TextView) capabilityView.findViewById( 245 com.android.internal.R.id.permission_group); 246 labelView.setText(getString(capability.titleResId)); 247 248 descriptionView = (TextView) capabilityView.findViewById( 249 com.android.internal.R.id.permission_list); 250 descriptionView.setText(getString(capability.descResId)); 251 252 capabilitiesView.addView(capabilityView); 253 } 254 255 return content; 256 } 257 258 @Override 259 public void onClick(DialogInterface dialog, int which) { 260 final boolean checked; 261 switch (which) { 262 case DialogInterface.BUTTON_POSITIVE: 263 checked = (mShownDialogId == DIALOG_ID_ENABLE_WARNING); 264 mToggleSwitch.setCheckedInternal(checked); 265 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, checked); 266 onPreferenceToggled(mPreferenceKey, checked); 267 break; 268 case DialogInterface.BUTTON_NEGATIVE: 269 checked = (mShownDialogId == DIALOG_ID_DISABLE_WARNING); 270 mToggleSwitch.setCheckedInternal(checked); 271 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, checked); 272 onPreferenceToggled(mPreferenceKey, checked); 273 break; 274 default: 275 throw new IllegalArgumentException(); 276 } 277 } 278 279 @Override 280 protected void onInstallActionBarToggleSwitch() { 281 super.onInstallActionBarToggleSwitch(); 282 mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { 283 @Override 284 public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { 285 if (checked) { 286 toggleSwitch.setCheckedInternal(false); 287 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, false); 288 showDialog(DIALOG_ID_ENABLE_WARNING); 289 } else { 290 toggleSwitch.setCheckedInternal(true); 291 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, true); 292 showDialog(DIALOG_ID_DISABLE_WARNING); 293 } 294 return true; 295 } 296 }); 297 } 298 299 @Override 300 protected void onProcessArguments(Bundle arguments) { 301 super.onProcessArguments(arguments); 302 // Settings title and intent. 303 String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE); 304 String settingsComponentName = arguments.getString( 305 AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME); 306 if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) { 307 Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent( 308 ComponentName.unflattenFromString(settingsComponentName.toString())); 309 if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) { 310 mSettingsTitle = settingsTitle; 311 mSettingsIntent = settingsIntent; 312 setHasOptionsMenu(true); 313 } 314 } 315 316 mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME); 317 } 318 } 319