1/*
2 * Copyright (C) 2009 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
17package com.android.settings;
18
19import android.accessibilityservice.AccessibilityServiceInfo;
20import android.app.ActionBar;
21import android.app.Activity;
22import android.app.ActivityManagerNative;
23import android.app.AlertDialog;
24import android.app.Dialog;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.content.SharedPreferences;
30import android.content.pm.ResolveInfo;
31import android.content.pm.ServiceInfo;
32import android.content.res.Configuration;
33import android.net.Uri;
34import android.os.Bundle;
35import android.os.Handler;
36import android.os.Message;
37import android.os.RemoteException;
38import android.os.ServiceManager;
39import android.os.SystemProperties;
40import android.preference.CheckBoxPreference;
41import android.preference.ListPreference;
42import android.preference.Preference;
43import android.preference.PreferenceActivity;
44import android.preference.PreferenceCategory;
45import android.preference.PreferenceScreen;
46import android.provider.Settings;
47import android.text.TextUtils;
48import android.text.TextUtils.SimpleStringSplitter;
49import android.util.Log;
50import android.view.Gravity;
51import android.view.IWindowManager;
52import android.view.KeyCharacterMap;
53import android.view.KeyEvent;
54import android.view.Menu;
55import android.view.MenuInflater;
56import android.view.MenuItem;
57import android.view.Surface;
58import android.view.View;
59import android.view.accessibility.AccessibilityEvent;
60import android.view.accessibility.AccessibilityManager;
61import android.widget.LinearLayout;
62import android.widget.Switch;
63import android.widget.TextView;
64
65import com.android.internal.content.PackageMonitor;
66import com.android.settings.AccessibilitySettings.ToggleSwitch.OnBeforeCheckedChangeListener;
67
68import java.util.HashMap;
69import java.util.HashSet;
70import java.util.List;
71import java.util.Map;
72import java.util.Set;
73
74/**
75 * Activity with the accessibility settings.
76 */
77public class AccessibilitySettings extends SettingsPreferenceFragment implements DialogCreatable,
78        Preference.OnPreferenceChangeListener {
79    private static final String TAG = "AccessibilitySettings";
80
81    private static final String DEFAULT_SCREENREADER_MARKET_LINK =
82        "market://search?q=pname:com.google.android.marvin.talkback";
83
84    private static final float LARGE_FONT_SCALE = 1.3f;
85
86    private static final String SYSTEM_PROPERTY_MARKET_URL = "ro.screenreader.market";
87
88    // Timeout before we update the services if packages are added/removed since
89    // the AccessibilityManagerService has to do that processing first to generate
90    // the AccessibilityServiceInfo we need for proper presentation.
91    private static final long DELAY_UPDATE_SERVICES_MILLIS = 1000;
92
93    private static final char ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':';
94
95    private static final String KEY_ACCESSIBILITY_TUTORIAL_LAUNCHED_ONCE =
96        "key_accessibility_tutorial_launched_once";
97
98    private static final String KEY_INSTALL_ACCESSIBILITY_SERVICE_OFFERED_ONCE =
99        "key_install_accessibility_service_offered_once";
100
101    // Preference categories
102    private static final String SERVICES_CATEGORY = "services_category";
103    private static final String SYSTEM_CATEGORY = "system_category";
104
105    // Preferences
106    private static final String TOGGLE_LARGE_TEXT_PREFERENCE = "toggle_large_text_preference";
107    private static final String TOGGLE_POWER_BUTTON_ENDS_CALL_PREFERENCE =
108        "toggle_power_button_ends_call_preference";
109    private static final String TOGGLE_AUTO_ROTATE_SCREEN_PREFERENCE =
110        "toggle_auto_rotate_screen_preference";
111    private static final String TOGGLE_SPEAK_PASSWORD_PREFERENCE =
112        "toggle_speak_password_preference";
113    private static final String TOGGLE_TOUCH_EXPLORATION_PREFERENCE =
114        "toggle_touch_exploration_preference";
115    private static final String SELECT_LONG_PRESS_TIMEOUT_PREFERENCE =
116        "select_long_press_timeout_preference";
117    private static final String TOGGLE_SCRIPT_INJECTION_PREFERENCE =
118        "toggle_script_injection_preference";
119
120    // Extras passed to sub-fragments.
121    private static final String EXTRA_PREFERENCE_KEY = "preference_key";
122    private static final String EXTRA_CHECKED = "checked";
123    private static final String EXTRA_TITLE = "title";
124    private static final String EXTRA_SUMMARY = "summary";
125    private static final String EXTRA_ENABLE_WARNING_TITLE = "enable_warning_title";
126    private static final String EXTRA_ENABLE_WARNING_MESSAGE = "enable_warning_message";
127    private static final String EXTRA_DISABLE_WARNING_TITLE = "disable_warning_title";
128    private static final String EXTRA_DISABLE_WARNING_MESSAGE = "disable_warning_message";
129    private static final String EXTRA_SETTINGS_TITLE = "settings_title";
130    private static final String EXTRA_SETTINGS_COMPONENT_NAME = "settings_component_name";
131
132    // Dialog IDs.
133    private static final int DIALOG_ID_NO_ACCESSIBILITY_SERVICES = 1;
134
135    // Auxiliary members.
136    private final static SimpleStringSplitter sStringColonSplitter =
137        new SimpleStringSplitter(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
138
139    private static final Set<ComponentName> sInstalledServices = new HashSet<ComponentName>();
140
141    private final Map<String, String> mLongPressTimeoutValuetoTitleMap =
142        new HashMap<String, String>();
143
144    private final Configuration mCurConfig = new Configuration();
145
146    private final PackageMonitor mSettingsPackageMonitor = new SettingsPackageMonitor();
147
148    private final Handler mHandler = new Handler() {
149        @Override
150        public void dispatchMessage(Message msg) {
151            super.dispatchMessage(msg);
152            loadInstalledServices();
153            updateServicesPreferences();
154        }
155    };
156
157    // Preference controls.
158    private PreferenceCategory mServicesCategory;
159    private PreferenceCategory mSystemsCategory;
160
161    private CheckBoxPreference mToggleLargeTextPreference;
162    private CheckBoxPreference mTogglePowerButtonEndsCallPreference;
163    private CheckBoxPreference mToggleAutoRotateScreenPreference;
164    private CheckBoxPreference mToggleSpeakPasswordPreference;
165    private Preference mToggleTouchExplorationPreference;
166    private ListPreference mSelectLongPressTimeoutPreference;
167    private AccessibilityEnableScriptInjectionPreference mToggleScriptInjectionPreference;
168    private Preference mNoServicesMessagePreference;
169
170    private int mLongPressTimeoutDefault;
171
172    @Override
173    public void onCreate(Bundle icicle) {
174        super.onCreate(icicle);
175        addPreferencesFromResource(R.xml.accessibility_settings);
176        initializeAllPreferences();
177    }
178
179    @Override
180    public void onResume() {
181        super.onResume();
182        loadInstalledServices();
183        updateAllPreferences();
184        if (mServicesCategory.getPreference(0) == mNoServicesMessagePreference) {
185            offerInstallAccessibilitySerivceOnce();
186        }
187        mSettingsPackageMonitor.register(getActivity(), false);
188    }
189
190    @Override
191    public void onPause() {
192        mSettingsPackageMonitor.unregister();
193        super.onPause();
194    }
195
196    public boolean onPreferenceChange(Preference preference, Object newValue) {
197        if (preference == mSelectLongPressTimeoutPreference) {
198            String stringValue = (String) newValue;
199            Settings.Secure.putInt(getContentResolver(),
200                    Settings.Secure.LONG_PRESS_TIMEOUT, Integer.parseInt(stringValue));
201            mSelectLongPressTimeoutPreference.setSummary(
202                    mLongPressTimeoutValuetoTitleMap.get(stringValue));
203            return true;
204        }
205        return false;
206    }
207
208    @Override
209    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
210        if (mToggleLargeTextPreference == preference) {
211            handleToggleLargeTextPreferenceClick();
212            return true;
213        } else if (mTogglePowerButtonEndsCallPreference == preference) {
214            handleTogglePowerButtonEndsCallPreferenceClick();
215            return true;
216        } else if (mToggleAutoRotateScreenPreference == preference) {
217            handleToggleAutoRotateScreenPreferenceClick();
218            return true;
219        } else if (mToggleSpeakPasswordPreference == preference) {
220            handleToggleSpeakPasswordPreferenceClick();
221        }
222        return super.onPreferenceTreeClick(preferenceScreen, preference);
223    }
224
225    private void handleToggleLargeTextPreferenceClick() {
226        try {
227            mCurConfig.fontScale = mToggleLargeTextPreference.isChecked() ? LARGE_FONT_SCALE : 1;
228            ActivityManagerNative.getDefault().updatePersistentConfiguration(mCurConfig);
229        } catch (RemoteException re) {
230            /* ignore */
231        }
232    }
233
234    private void handleTogglePowerButtonEndsCallPreferenceClick() {
235        Settings.Secure.putInt(getContentResolver(),
236                Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR,
237                (mTogglePowerButtonEndsCallPreference.isChecked()
238                        ? Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP
239                        : Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_SCREEN_OFF));
240    }
241
242    private void handleToggleAutoRotateScreenPreferenceClick() {
243        try {
244            IWindowManager wm = IWindowManager.Stub.asInterface(
245                    ServiceManager.getService(Context.WINDOW_SERVICE));
246            if (mToggleAutoRotateScreenPreference.isChecked()) {
247                wm.thawRotation();
248            } else {
249                wm.freezeRotation(Surface.ROTATION_0);
250            }
251        } catch (RemoteException exc) {
252            Log.w(TAG, "Unable to save auto-rotate setting");
253        }
254    }
255
256    private void handleToggleSpeakPasswordPreferenceClick() {
257        Settings.Secure.putInt(getContentResolver(),
258                Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD,
259                mToggleSpeakPasswordPreference.isChecked() ? 1 : 0);
260    }
261
262    private void initializeAllPreferences() {
263        mServicesCategory = (PreferenceCategory) findPreference(SERVICES_CATEGORY);
264        mSystemsCategory = (PreferenceCategory) findPreference(SYSTEM_CATEGORY);
265
266        // Large text.
267        mToggleLargeTextPreference =
268            (CheckBoxPreference) findPreference(TOGGLE_LARGE_TEXT_PREFERENCE);
269
270        // Power button ends calls.
271        mTogglePowerButtonEndsCallPreference =
272            (CheckBoxPreference) findPreference(TOGGLE_POWER_BUTTON_ENDS_CALL_PREFERENCE);
273        if (!KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_POWER)
274                || !Utils.isVoiceCapable(getActivity())) {
275            mSystemsCategory.removePreference(mTogglePowerButtonEndsCallPreference);
276        }
277
278        // Auto-rotate screen
279        mToggleAutoRotateScreenPreference =
280            (CheckBoxPreference) findPreference(TOGGLE_AUTO_ROTATE_SCREEN_PREFERENCE);
281
282        // Speak passwords.
283        mToggleSpeakPasswordPreference =
284            (CheckBoxPreference) findPreference(TOGGLE_SPEAK_PASSWORD_PREFERENCE);
285
286        // Touch exploration enabled.
287        mToggleTouchExplorationPreference = findPreference(TOGGLE_TOUCH_EXPLORATION_PREFERENCE);
288
289        // Long press timeout.
290        mSelectLongPressTimeoutPreference =
291            (ListPreference) findPreference(SELECT_LONG_PRESS_TIMEOUT_PREFERENCE);
292        mSelectLongPressTimeoutPreference.setOnPreferenceChangeListener(this);
293        if (mLongPressTimeoutValuetoTitleMap.size() == 0) {
294            String[] timeoutValues = getResources().getStringArray(
295                    R.array.long_press_timeout_selector_values);
296            mLongPressTimeoutDefault = Integer.parseInt(timeoutValues[0]);
297            String[] timeoutTitles = getResources().getStringArray(
298                    R.array.long_press_timeout_selector_titles);
299            final int timeoutValueCount = timeoutValues.length;
300            for (int i = 0; i < timeoutValueCount; i++) {
301               mLongPressTimeoutValuetoTitleMap.put(timeoutValues[i], timeoutTitles[i]);
302            }
303        }
304
305        // Script injection.
306        mToggleScriptInjectionPreference = (AccessibilityEnableScriptInjectionPreference)
307            findPreference(TOGGLE_SCRIPT_INJECTION_PREFERENCE);
308    }
309
310    private void updateAllPreferences() {
311        updateServicesPreferences();
312        updateSystemPreferences();
313    }
314
315    private void updateServicesPreferences() {
316        // Since services category is auto generated we have to do a pass
317        // to generate it since services can come and go and then based on
318        // the global accessibility state to decided whether it is enabled.
319
320        // Generate.
321        mServicesCategory.removeAll();
322
323        AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(getActivity());
324
325        List<AccessibilityServiceInfo> installedServices =
326            accessibilityManager.getInstalledAccessibilityServiceList();
327        Set<ComponentName> enabledServices = getEnabledServicesFromSettings(getActivity());
328
329        final boolean accessibilityEnabled = Settings.Secure.getInt(getContentResolver(),
330                Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1;
331
332        for (int i = 0, count = installedServices.size(); i < count; ++i) {
333            AccessibilityServiceInfo info = installedServices.get(i);
334
335            PreferenceScreen preference = getPreferenceManager().createPreferenceScreen(
336                    getActivity());
337            String title = info.getResolveInfo().loadLabel(getPackageManager()).toString();
338
339            ServiceInfo serviceInfo = info.getResolveInfo().serviceInfo;
340            ComponentName componentName = new ComponentName(serviceInfo.packageName,
341                    serviceInfo.name);
342
343            preference.setKey(componentName.flattenToString());
344
345            preference.setTitle(title);
346            final boolean serviceEnabled = accessibilityEnabled
347                && enabledServices.contains(componentName);
348            if (serviceEnabled) {
349                preference.setSummary(getString(R.string.accessibility_service_state_on));
350            } else {
351                preference.setSummary(getString(R.string.accessibility_service_state_off));
352            }
353
354            preference.setOrder(i);
355            preference.setFragment(ToggleAccessibilityServiceFragment.class.getName());
356            preference.setPersistent(true);
357
358            Bundle extras = preference.getExtras();
359            extras.putString(EXTRA_PREFERENCE_KEY, preference.getKey());
360            extras.putBoolean(EXTRA_CHECKED, serviceEnabled);
361            extras.putString(EXTRA_TITLE, title);
362
363            String description = info.getDescription();
364            if (TextUtils.isEmpty(description)) {
365                description = getString(R.string.accessibility_service_default_description);
366            }
367            extras.putString(EXTRA_SUMMARY, description);
368
369            CharSequence applicationLabel = info.getResolveInfo().loadLabel(getPackageManager());
370
371            extras.putString(EXTRA_ENABLE_WARNING_TITLE, getString(
372                    R.string.accessibility_service_security_warning_title, applicationLabel));
373            extras.putString(EXTRA_ENABLE_WARNING_MESSAGE, getString(
374                    R.string.accessibility_service_security_warning_summary, applicationLabel));
375
376            extras.putString(EXTRA_DISABLE_WARNING_TITLE, getString(
377                    R.string.accessibility_service_disable_warning_title,
378                    applicationLabel));
379            extras.putString(EXTRA_DISABLE_WARNING_MESSAGE, getString(
380                    R.string.accessibility_service_disable_warning_summary,
381                    applicationLabel));
382
383            String settingsClassName = info.getSettingsActivityName();
384            if (!TextUtils.isEmpty(settingsClassName)) {
385                extras.putString(EXTRA_SETTINGS_TITLE,
386                        getString(R.string.accessibility_menu_item_settings));
387                extras.putString(EXTRA_SETTINGS_COMPONENT_NAME,
388                        new ComponentName(info.getResolveInfo().serviceInfo.packageName,
389                                settingsClassName).flattenToString());
390            }
391
392            mServicesCategory.addPreference(preference);
393        }
394
395        if (mServicesCategory.getPreferenceCount() == 0) {
396            if (mNoServicesMessagePreference == null) {
397                mNoServicesMessagePreference = new Preference(getActivity()) {
398                    @Override
399                    protected void onBindView(View view) {
400                        super.onBindView(view);
401
402                        LinearLayout containerView =
403                            (LinearLayout) view.findViewById(R.id.message_container);
404                        containerView.setGravity(Gravity.CENTER);
405
406                        TextView summaryView = (TextView) view.findViewById(R.id.summary);
407                        String title = getString(R.string.accessibility_no_services_installed);
408                        summaryView.setText(title);
409                    }
410                };
411                mNoServicesMessagePreference.setPersistent(false);
412                mNoServicesMessagePreference.setLayoutResource(
413                        R.layout.text_description_preference);
414                mNoServicesMessagePreference.setSelectable(false);
415            }
416            mServicesCategory.addPreference(mNoServicesMessagePreference);
417        }
418    }
419
420    private void updateSystemPreferences() {
421        // Large text.
422        try {
423            mCurConfig.updateFrom(ActivityManagerNative.getDefault().getConfiguration());
424        } catch (RemoteException re) {
425            /* ignore */
426        }
427        mToggleLargeTextPreference.setChecked(mCurConfig.fontScale == LARGE_FONT_SCALE);
428
429        // Power button ends calls.
430        if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_POWER)
431                && Utils.isVoiceCapable(getActivity())) {
432            final int incallPowerBehavior = Settings.Secure.getInt(getContentResolver(),
433                    Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR,
434                    Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_DEFAULT);
435            final boolean powerButtonEndsCall =
436                (incallPowerBehavior == Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP);
437            mTogglePowerButtonEndsCallPreference.setChecked(powerButtonEndsCall);
438        }
439
440        // Auto-rotate screen
441        final boolean autoRotationEnabled = Settings.System.getInt(getContentResolver(),
442                Settings.System.ACCELEROMETER_ROTATION, 0) != 0;
443        mToggleAutoRotateScreenPreference.setChecked(autoRotationEnabled);
444
445        // Speak passwords.
446        final boolean speakPasswordEnabled = Settings.Secure.getInt(getContentResolver(),
447                Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
448        mToggleSpeakPasswordPreference.setChecked(speakPasswordEnabled);
449
450        // Touch exploration enabled.
451        if (AccessibilityManager.getInstance(getActivity()).isEnabled()) {
452            mSystemsCategory.addPreference(mToggleTouchExplorationPreference);
453            final boolean touchExplorationEnabled = (Settings.Secure.getInt(getContentResolver(),
454                    Settings.Secure.TOUCH_EXPLORATION_ENABLED, 0) == 1);
455            if (touchExplorationEnabled) {
456                mToggleTouchExplorationPreference.setSummary(
457                        getString(R.string.accessibility_service_state_on));
458                mToggleTouchExplorationPreference.getExtras().putBoolean(EXTRA_CHECKED, true);
459            } else {
460                mToggleTouchExplorationPreference.setSummary(
461                        getString(R.string.accessibility_service_state_off));
462                mToggleTouchExplorationPreference.getExtras().putBoolean(EXTRA_CHECKED, false);
463            }
464
465        } else {
466            mSystemsCategory.removePreference(mToggleTouchExplorationPreference);
467        }
468
469        // Long press timeout.
470        final int longPressTimeout = Settings.Secure.getInt(getContentResolver(),
471                Settings.Secure.LONG_PRESS_TIMEOUT, mLongPressTimeoutDefault);
472        String value = String.valueOf(longPressTimeout);
473        mSelectLongPressTimeoutPreference.setValue(value);
474        mSelectLongPressTimeoutPreference.setSummary(mLongPressTimeoutValuetoTitleMap.get(value));
475
476        // Script injection.
477        final boolean  scriptInjectionAllowed = (Settings.Secure.getInt(getContentResolver(),
478                Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 0) == 1);
479        mToggleScriptInjectionPreference.setInjectionAllowed(scriptInjectionAllowed);
480    }
481
482    private void offerInstallAccessibilitySerivceOnce() {
483        // There is always one preference - if no services it is just a message.
484        if (mServicesCategory.getPreference(0) != mNoServicesMessagePreference) {
485            return;
486        }
487        SharedPreferences preferences = getActivity().getPreferences(Context.MODE_PRIVATE);
488        final boolean offerInstallService = !preferences.getBoolean(
489                KEY_INSTALL_ACCESSIBILITY_SERVICE_OFFERED_ONCE, false);
490        if (offerInstallService) {
491            preferences.edit().putBoolean(KEY_INSTALL_ACCESSIBILITY_SERVICE_OFFERED_ONCE,
492                    true).commit();
493            // Notify user that they do not have any accessibility
494            // services installed and direct them to Market to get TalkBack.
495            showDialog(DIALOG_ID_NO_ACCESSIBILITY_SERVICES);
496        }
497    }
498
499    @Override
500    public Dialog onCreateDialog(int dialogId) {
501        switch (dialogId) {
502            case DIALOG_ID_NO_ACCESSIBILITY_SERVICES:
503                return new AlertDialog.Builder(getActivity())
504                    .setTitle(R.string.accessibility_service_no_apps_title)
505                    .setMessage(R.string.accessibility_service_no_apps_message)
506                    .setPositiveButton(android.R.string.ok,
507                        new DialogInterface.OnClickListener() {
508                            public void onClick(DialogInterface dialog, int which) {
509                                // dismiss the dialog before launching the activity otherwise
510                                // the dialog removal occurs after onSaveInstanceState which
511                                // triggers an exception
512                                removeDialog(DIALOG_ID_NO_ACCESSIBILITY_SERVICES);
513                                String screenreaderMarketLink = SystemProperties.get(
514                                        SYSTEM_PROPERTY_MARKET_URL,
515                                        DEFAULT_SCREENREADER_MARKET_LINK);
516                                Uri marketUri = Uri.parse(screenreaderMarketLink);
517                                Intent marketIntent = new Intent(Intent.ACTION_VIEW, marketUri);
518                                startActivity(marketIntent);
519                            }
520                    })
521                    .setNegativeButton(android.R.string.cancel, null)
522                    .create();
523            default:
524                return null;
525        }
526    }
527
528    private void loadInstalledServices() {
529        List<AccessibilityServiceInfo> installedServiceInfos =
530            AccessibilityManager.getInstance(getActivity())
531                .getInstalledAccessibilityServiceList();
532        Set<ComponentName> installedServices = sInstalledServices;
533        installedServices.clear();
534        final int installedServiceInfoCount = installedServiceInfos.size();
535        for (int i = 0; i < installedServiceInfoCount; i++) {
536            ResolveInfo resolveInfo = installedServiceInfos.get(i).getResolveInfo();
537            ComponentName installedService = new ComponentName(
538                    resolveInfo.serviceInfo.packageName,
539                    resolveInfo.serviceInfo.name);
540            installedServices.add(installedService);
541        }
542    }
543
544    private static Set<ComponentName> getEnabledServicesFromSettings(Context context) {
545        String enabledServicesSetting = Settings.Secure.getString(context.getContentResolver(),
546                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
547        if (enabledServicesSetting == null) {
548            enabledServicesSetting = "";
549        }
550        Set<ComponentName> enabledServices = new HashSet<ComponentName>();
551        SimpleStringSplitter colonSplitter = sStringColonSplitter;
552        colonSplitter.setString(enabledServicesSetting);
553        while (colonSplitter.hasNext()) {
554            String componentNameString = colonSplitter.next();
555            ComponentName enabledService = ComponentName.unflattenFromString(
556                    componentNameString);
557            if (enabledService != null) {
558                enabledServices.add(enabledService);
559            }
560        }
561        return enabledServices;
562    }
563
564    private class SettingsPackageMonitor extends PackageMonitor {
565
566        @Override
567        public void onPackageAdded(String packageName, int uid) {
568            Message message = mHandler.obtainMessage();
569            mHandler.sendMessageDelayed(message, DELAY_UPDATE_SERVICES_MILLIS);
570        }
571
572        @Override
573        public void onPackageAppeared(String packageName, int reason) {
574            Message message = mHandler.obtainMessage();
575            mHandler.sendMessageDelayed(message, DELAY_UPDATE_SERVICES_MILLIS);
576        }
577
578        @Override
579        public void onPackageDisappeared(String packageName, int reason) {
580            Message message = mHandler.obtainMessage();
581            mHandler.sendMessageDelayed(message, DELAY_UPDATE_SERVICES_MILLIS);
582        }
583
584        @Override
585        public void onPackageRemoved(String packageName, int uid) {
586            Message message = mHandler.obtainMessage();
587            mHandler.sendMessageDelayed(message, DELAY_UPDATE_SERVICES_MILLIS);
588        }
589    }
590
591    private static ToggleSwitch createAndAddActionBarToggleSwitch(Activity activity) {
592        ToggleSwitch toggleSwitch = new ToggleSwitch(activity);
593        final int padding = activity.getResources().getDimensionPixelSize(
594                R.dimen.action_bar_switch_padding);
595        toggleSwitch.setPadding(0, 0, padding, 0);
596        activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
597                ActionBar.DISPLAY_SHOW_CUSTOM);
598        activity.getActionBar().setCustomView(toggleSwitch,
599                new ActionBar.LayoutParams(ActionBar.LayoutParams.WRAP_CONTENT,
600                        ActionBar.LayoutParams.WRAP_CONTENT,
601                        Gravity.CENTER_VERTICAL | Gravity.RIGHT));
602        return toggleSwitch;
603    }
604
605    public static class ToggleSwitch extends Switch {
606
607        private OnBeforeCheckedChangeListener mOnBeforeListener;
608
609        public static interface OnBeforeCheckedChangeListener {
610            public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
611        }
612
613        public ToggleSwitch(Context context) {
614            super(context);
615        }
616
617        public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) {
618            mOnBeforeListener = listener;
619        }
620
621        @Override
622        public void setChecked(boolean checked) {
623            if (mOnBeforeListener != null
624                    && mOnBeforeListener.onBeforeCheckedChanged(this, checked)) {
625                return;
626            }
627            super.setChecked(checked);
628        }
629
630        public void setCheckedInternal(boolean checked) {
631            super.setChecked(checked);
632        }
633    }
634
635    public static class ToggleAccessibilityServiceFragment extends TogglePreferenceFragment {
636        @Override
637        public void onPreferenceToggled(String preferenceKey, boolean enabled) {
638            // Parse the enabled services.
639            Set<ComponentName> enabledServices = getEnabledServicesFromSettings(getActivity());
640
641            // Determine enabled services and accessibility state.
642            ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey);
643            final boolean accessibilityEnabled;
644            if (enabled) {
645                // Enabling at least one service enables accessibility.
646                accessibilityEnabled = true;
647                enabledServices.add(toggledService);
648            } else {
649                // Check how many enabled and installed services are present.
650                int enabledAndInstalledServiceCount = 0;
651                Set<ComponentName> installedServices = sInstalledServices;
652                for (ComponentName enabledService : enabledServices) {
653                    if (installedServices.contains(enabledService)) {
654                        enabledAndInstalledServiceCount++;
655                    }
656                }
657                // Disabling the last service disables accessibility.
658                accessibilityEnabled = enabledAndInstalledServiceCount > 1
659                    || (enabledAndInstalledServiceCount == 1
660                            && !installedServices.contains(toggledService));
661                enabledServices.remove(toggledService);
662            }
663
664            // Update the enabled services setting.
665            StringBuilder enabledServicesBuilder = new StringBuilder();
666            // Keep the enabled services even if they are not installed since we have
667            // no way to know whether the application restore process has completed.
668            // In general the system should be responsible for the clean up not settings.
669            for (ComponentName enabledService : enabledServices) {
670                enabledServicesBuilder.append(enabledService.flattenToString());
671                enabledServicesBuilder.append(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
672            }
673            final int enabledServicesBuilderLength = enabledServicesBuilder.length();
674            if (enabledServicesBuilderLength > 0) {
675                enabledServicesBuilder.deleteCharAt(enabledServicesBuilderLength - 1);
676            }
677            Settings.Secure.putString(getContentResolver(),
678                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
679                    enabledServicesBuilder.toString());
680
681            // Update accessibility enabled.
682            Settings.Secure.putInt(getContentResolver(),
683                    Settings.Secure.ACCESSIBILITY_ENABLED, accessibilityEnabled ? 1 : 0);
684        }
685    }
686
687    public static class ToggleTouchExplorationFragment extends TogglePreferenceFragment {
688        @Override
689        public void onPreferenceToggled(String preferenceKey, boolean enabled) {
690            Settings.Secure.putInt(getContentResolver(),
691                    Settings.Secure.TOUCH_EXPLORATION_ENABLED, enabled ? 1 : 0);
692            if (enabled) {
693                SharedPreferences preferences = getActivity().getPreferences(Context.MODE_PRIVATE);
694                final boolean launchAccessibilityTutorial = !preferences.getBoolean(
695                        KEY_ACCESSIBILITY_TUTORIAL_LAUNCHED_ONCE, false);
696                if (launchAccessibilityTutorial) {
697                    preferences.edit().putBoolean(KEY_ACCESSIBILITY_TUTORIAL_LAUNCHED_ONCE,
698                            true).commit();
699                    Intent intent = new Intent(AccessibilityTutorialActivity.ACTION);
700                    getActivity().startActivity(intent);
701                }
702            }
703        }
704    }
705
706    private abstract static class TogglePreferenceFragment extends SettingsPreferenceFragment
707            implements DialogInterface.OnClickListener {
708
709        private static final int DIALOG_ID_ENABLE_WARNING = 1;
710        private static final int DIALOG_ID_DISABLE_WARNING = 2;
711
712        private String mPreferenceKey;
713
714        private ToggleSwitch mToggleSwitch;
715
716        private CharSequence mEnableWarningTitle;
717        private CharSequence mEnableWarningMessage;
718        private CharSequence mDisableWarningTitle;
719        private CharSequence mDisableWarningMessage;
720        private Preference mSummaryPreference;
721
722        private CharSequence mSettingsTitle;
723        private Intent mSettingsIntent;
724
725        private int mShownDialogId;
726
727        // TODO: Showing sub-sub fragment does not handle the activity title
728        //       so we do it but this is wrong. Do a real fix when there is time.
729        private CharSequence mOldActivityTitle;
730
731        @Override
732        public void onCreate(Bundle savedInstanceState) {
733            super.onCreate(savedInstanceState);
734            PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(
735                    getActivity());
736            setPreferenceScreen(preferenceScreen);
737            mSummaryPreference = new Preference(getActivity()) {
738                @Override
739                protected void onBindView(View view) {
740                    super.onBindView(view);
741                    TextView summaryView = (TextView) view.findViewById(R.id.summary);
742                    summaryView.setText(getSummary());
743                    sendAccessibilityEvent(summaryView);
744                }
745
746                private void sendAccessibilityEvent(View view) {
747                    // Since the view is still not attached we create, populate,
748                    // and send the event directly since we do not know when it
749                    // will be attached and posting commands is not as clean.
750                    AccessibilityManager accessibilityManager =
751                        AccessibilityManager.getInstance(getActivity());
752                    if (accessibilityManager.isEnabled()) {
753                        AccessibilityEvent event = AccessibilityEvent.obtain();
754                        event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
755                        view.onInitializeAccessibilityEvent(event);
756                        view.dispatchPopulateAccessibilityEvent(event);
757                        accessibilityManager.sendAccessibilityEvent(event);
758                    }
759                }
760            };
761            mSummaryPreference.setPersistent(false);
762            mSummaryPreference.setLayoutResource(R.layout.text_description_preference);
763            preferenceScreen.addPreference(mSummaryPreference);
764        }
765
766        @Override
767        public void onViewCreated(View view, Bundle savedInstanceState) {
768            super.onViewCreated(view, savedInstanceState);
769            installActionBarToggleSwitch();
770            processArguments();
771            getListView().setDivider(null);
772            getListView().setEnabled(false);
773        }
774
775        @Override
776        public void onDestroyView() {
777            getActivity().getActionBar().setCustomView(null);
778            if (mOldActivityTitle != null) {
779                getActivity().getActionBar().setTitle(mOldActivityTitle);
780            }
781            mToggleSwitch.setOnBeforeCheckedChangeListener(null);
782            super.onDestroyView();
783        }
784
785        public abstract void onPreferenceToggled(String preferenceKey, boolean value);
786
787        @Override
788        public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
789            super.onCreateOptionsMenu(menu, inflater);
790            MenuItem menuItem = menu.add(mSettingsTitle);
791            menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
792            menuItem.setIntent(mSettingsIntent);
793        }
794
795        @Override
796        public Dialog onCreateDialog(int dialogId) {
797            CharSequence title = null;
798            CharSequence message = null;
799            switch (dialogId) {
800                case DIALOG_ID_ENABLE_WARNING:
801                    mShownDialogId = DIALOG_ID_ENABLE_WARNING;
802                    title = mEnableWarningTitle;
803                    message = mEnableWarningMessage;
804                    break;
805                case DIALOG_ID_DISABLE_WARNING:
806                    mShownDialogId = DIALOG_ID_DISABLE_WARNING;
807                    title = mDisableWarningTitle;
808                    message = mDisableWarningMessage;
809                    break;
810                default:
811                    throw new IllegalArgumentException();
812            }
813            return new AlertDialog.Builder(getActivity())
814                .setTitle(title)
815                .setIcon(android.R.drawable.ic_dialog_alert)
816                .setMessage(message)
817                .setCancelable(true)
818                .setPositiveButton(android.R.string.ok, this)
819                .setNegativeButton(android.R.string.cancel, this)
820                .create();
821        }
822
823        @Override
824        public void onClick(DialogInterface dialog, int which) {
825            final boolean checked;
826            switch (which) {
827                case DialogInterface.BUTTON_POSITIVE:
828                    checked = (mShownDialogId == DIALOG_ID_ENABLE_WARNING);
829                    mToggleSwitch.setCheckedInternal(checked);
830                    getArguments().putBoolean(EXTRA_CHECKED, checked);
831                    onPreferenceToggled(mPreferenceKey, checked);
832                    break;
833                case DialogInterface.BUTTON_NEGATIVE:
834                    checked = (mShownDialogId == DIALOG_ID_DISABLE_WARNING);
835                    mToggleSwitch.setCheckedInternal(checked);
836                    getArguments().putBoolean(EXTRA_CHECKED, checked);
837                    onPreferenceToggled(mPreferenceKey, checked);
838                    break;
839                default:
840                    throw new IllegalArgumentException();
841            }
842        }
843
844        private void installActionBarToggleSwitch() {
845            mToggleSwitch = createAndAddActionBarToggleSwitch(getActivity());
846            mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() {
847                @Override
848                public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) {
849                    if (checked) {
850                        if (!TextUtils.isEmpty(mEnableWarningMessage)) {
851                            toggleSwitch.setCheckedInternal(false);
852                            getArguments().putBoolean(EXTRA_CHECKED, false);
853                            showDialog(DIALOG_ID_ENABLE_WARNING);
854                            return true;
855                        }
856                        onPreferenceToggled(mPreferenceKey, true);
857                    } else {
858                        if (!TextUtils.isEmpty(mDisableWarningMessage)) {
859                            toggleSwitch.setCheckedInternal(true);
860                            getArguments().putBoolean(EXTRA_CHECKED, true);
861                            showDialog(DIALOG_ID_DISABLE_WARNING);
862                            return true;
863                        }
864                        onPreferenceToggled(mPreferenceKey, false);
865                    }
866                    return false;
867                }
868            });
869        }
870
871        private void processArguments() {
872            Bundle arguments = getArguments();
873
874            // Key.
875            mPreferenceKey = arguments.getString(EXTRA_PREFERENCE_KEY);
876
877            // Enabled.
878            final boolean enabled = arguments.getBoolean(EXTRA_CHECKED);
879            mToggleSwitch.setCheckedInternal(enabled);
880
881            // Title.
882            PreferenceActivity activity = (PreferenceActivity) getActivity();
883            if (!activity.onIsMultiPane() || activity.onIsHidingHeaders()) {
884                mOldActivityTitle = getActivity().getTitle();
885                String title = arguments.getString(EXTRA_TITLE);
886                getActivity().getActionBar().setTitle(title);
887            }
888
889            // Summary.
890            String summary = arguments.getString(EXTRA_SUMMARY);
891            mSummaryPreference.setSummary(summary);
892
893            // Settings title and intent.
894            String settingsTitle = arguments.getString(EXTRA_SETTINGS_TITLE);
895            String settingsComponentName = arguments.getString(EXTRA_SETTINGS_COMPONENT_NAME);
896            if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) {
897                Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent(
898                        ComponentName.unflattenFromString(settingsComponentName.toString()));
899                if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) {
900                    mSettingsTitle = settingsTitle;
901                    mSettingsIntent = settingsIntent;
902                    setHasOptionsMenu(true);
903                }
904            }
905
906            // Enable warning title.
907            mEnableWarningTitle = arguments.getCharSequence(
908                    AccessibilitySettings.EXTRA_ENABLE_WARNING_TITLE);
909
910            // Enable warning message.
911            mEnableWarningMessage = arguments.getCharSequence(
912                    AccessibilitySettings.EXTRA_ENABLE_WARNING_MESSAGE);
913
914            // Disable warning title.
915            mDisableWarningTitle = arguments.getString(
916                    AccessibilitySettings.EXTRA_DISABLE_WARNING_TITLE);
917
918            // Disable warning message.
919            mDisableWarningMessage = arguments.getString(
920                    AccessibilitySettings.EXTRA_DISABLE_WARNING_MESSAGE);
921        }
922    }
923}
924