AccessibilityShortcutController.java revision 106fe732050f3d75a08c3bc48fdbcf84cac20b41
1/*
2 * Copyright (C) 2012 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.server.policy;
18
19import android.accessibilityservice.AccessibilityServiceInfo;
20import android.app.ActivityManager;
21import android.app.AlertDialog;
22import android.content.ComponentName;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.DialogInterface;
26import android.database.ContentObserver;
27import android.media.AudioAttributes;
28import android.media.Ringtone;
29import android.media.RingtoneManager;
30import android.os.Handler;
31import android.os.UserHandle;
32import android.provider.Settings;
33import android.text.TextUtils;
34import android.util.Slog;
35import android.view.Window;
36import android.view.WindowManager;
37import android.view.accessibility.AccessibilityManager;
38
39import android.widget.Toast;
40import com.android.internal.R;
41
42import java.util.List;
43
44import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
45
46/**
47 * Class to help manage the accessibility shortcut
48 */
49public class AccessibilityShortcutController {
50    private static final String TAG = "AccessibilityShortcutController";
51
52    private final Context mContext;
53    private AlertDialog mAlertDialog;
54    private boolean mIsShortcutEnabled;
55    // Visible for testing
56    public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
57
58    public static String getTargetServiceComponentNameString(
59            Context context, int userId) {
60        final String currentShortcutServiceId = Settings.Secure.getStringForUser(
61                context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
62                userId);
63        if (currentShortcutServiceId != null) {
64            return currentShortcutServiceId;
65        }
66        return context.getString(R.string.config_defaultAccessibilityService);
67    }
68
69    public AccessibilityShortcutController(Context context, Handler handler) {
70        mContext = context;
71
72        // Keep track of state of shortcut
73        mContext.getContentResolver().registerContentObserver(
74                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
75                false,
76                new ContentObserver(handler) {
77                    @Override
78                    public void onChange(boolean selfChange) {
79                        onSettingsChanged();
80                    }
81                },
82                UserHandle.USER_ALL);
83        updateShortcutEnabled();
84    }
85
86    public boolean isAccessibilityShortcutAvailable() {
87        return mIsShortcutEnabled;
88    }
89
90    public void onSettingsChanged() {
91        updateShortcutEnabled();
92    }
93
94    /**
95     * Called when the accessibility shortcut is activated
96     */
97    public void performAccessibilityShortcut() {
98        Slog.d(TAG, "Accessibility shortcut activated");
99        final ContentResolver cr = mContext.getContentResolver();
100        final int userId = ActivityManager.getCurrentUser();
101        final int dialogAlreadyShown = Settings.Secure.getIntForUser(
102                cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
103        final Ringtone tone =
104                RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
105        if (tone != null) {
106            tone.setAudioAttributes(new AudioAttributes.Builder()
107                .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
108                .build());
109            tone.play();
110        }
111        if (dialogAlreadyShown == 0) {
112            // The first time, we show a warning rather than toggle the service to give the user a
113            // chance to turn off this feature before stuff gets enabled.
114            mAlertDialog = createShortcutWarningDialog(userId);
115            if (mAlertDialog == null) {
116                return;
117            }
118            Window w = mAlertDialog.getWindow();
119            WindowManager.LayoutParams attr = w.getAttributes();
120            attr.type = TYPE_KEYGUARD_DIALOG;
121            w.setAttributes(attr);
122            mAlertDialog.show();
123            Settings.Secure.putIntForUser(
124                    cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1, userId);
125        } else {
126            if (mAlertDialog != null) {
127                mAlertDialog.dismiss();
128                mAlertDialog = null;
129            }
130
131            // Show a toast alerting the user to what's happening
132            final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
133            if (serviceInfo == null) {
134                Slog.e(TAG, "Accessibility shortcut set to invalid service");
135                return;
136            }
137            String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
138                    ? R.string.accessibility_shortcut_disabling_service
139                    : R.string.accessibility_shortcut_enabling_service);
140            String toastMessage = String.format(toastMessageFormatString,
141                    serviceInfo.getResolveInfo()
142                            .loadLabel(mContext.getPackageManager()).toString());
143            mFrameworkObjectProvider.makeToastFromText(mContext, toastMessage, Toast.LENGTH_LONG)
144                    .show();
145
146            mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
147                    .performAccessibilityShortcut();
148        }
149    }
150
151    private void updateShortcutEnabled() {
152        mIsShortcutEnabled = !TextUtils.isEmpty(getTargetServiceComponentNameString(
153                mContext, UserHandle.myUserId()));
154    }
155
156    private AlertDialog createShortcutWarningDialog(int userId) {
157        final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
158
159        if (serviceInfo == null) {
160            return null;
161        }
162
163        final String warningMessage = String.format(
164                mContext.getString(R.string.accessibility_shortcut_toogle_warning),
165                serviceInfo.getResolveInfo().loadLabel(mContext.getPackageManager()).toString());
166        final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(mContext)
167                .setTitle(R.string.accessibility_shortcut_warning_dialog_title)
168                .setMessage(warningMessage)
169                .setCancelable(false)
170                .setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
171                .setNegativeButton(R.string.disable_accessibility_shortcut,
172                        (DialogInterface d, int which) -> {
173                            Settings.Secure.putStringForUser(mContext.getContentResolver(),
174                                    Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
175                                    userId);
176                        })
177                .setOnCancelListener((DialogInterface d) -> {
178                    // If canceled, treat as if the dialog has never been shown
179                    Settings.Secure.putIntForUser(mContext.getContentResolver(),
180                        Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
181                })
182                .create();
183        return alertDialog;
184    }
185
186    private AccessibilityServiceInfo getInfoForTargetService() {
187        final String currentShortcutServiceString = getTargetServiceComponentNameString(
188                mContext, UserHandle.myUserId());
189        if (currentShortcutServiceString == null) {
190            return null;
191        }
192        AccessibilityManager accessibilityManager =
193                mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
194        return accessibilityManager.getInstalledServiceInfoWithComponentName(
195                        ComponentName.unflattenFromString(currentShortcutServiceString));
196    }
197
198    private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
199        AccessibilityManager accessibilityManager =
200                mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
201        return accessibilityManager.getEnabledAccessibilityServiceList(
202                AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
203    }
204
205    // Class to allow mocking of static framework calls
206    public static class FrameworkObjectProvider {
207        public AccessibilityManager getAccessibilityManagerInstance(Context context) {
208            return AccessibilityManager.getInstance(context);
209        }
210
211        public AlertDialog.Builder getAlertDialogBuilder(Context context) {
212            return new AlertDialog.Builder(context);
213        }
214
215        public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
216            return Toast.makeText(context, charSequence, duration);
217        }
218    }
219}
220