1/*
2 * Copyright 2017 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.internal.accessibility;
18
19import android.accessibilityservice.AccessibilityServiceInfo;
20import android.app.ActivityManager;
21import android.app.ActivityThread;
22import android.app.AlertDialog;
23import android.content.ComponentName;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.pm.PackageManager;
28import android.database.ContentObserver;
29import android.media.AudioAttributes;
30import android.media.Ringtone;
31import android.media.RingtoneManager;
32import android.net.Uri;
33import android.os.Handler;
34import android.os.UserHandle;
35import android.os.Vibrator;
36import android.provider.Settings;
37import android.text.TextUtils;
38import android.util.ArrayMap;
39import android.util.Slog;
40import android.view.Window;
41import android.view.WindowManager;
42import android.view.accessibility.AccessibilityManager;
43
44import android.widget.Toast;
45import com.android.internal.R;
46
47import java.util.Collections;
48import java.util.Map;
49
50import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
51
52import static com.android.internal.util.ArrayUtils.convertToLongArray;
53
54/**
55 * Class to help manage the accessibility shortcut
56 */
57public class AccessibilityShortcutController {
58    private static final String TAG = "AccessibilityShortcutController";
59
60    // Dummy component names for framework features
61    public static final ComponentName COLOR_INVERSION_COMPONENT_NAME =
62            new ComponentName("com.android.server.accessibility", "ColorInversion");
63    public static final ComponentName DALTONIZER_COMPONENT_NAME =
64            new ComponentName("com.android.server.accessibility", "Daltonizer");
65
66    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
67            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
68            .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
69            .build();
70    private static Map<ComponentName, ToggleableFrameworkFeatureInfo> sFrameworkShortcutFeaturesMap;
71
72    private final Context mContext;
73    private AlertDialog mAlertDialog;
74    private boolean mIsShortcutEnabled;
75    private boolean mEnabledOnLockScreen;
76    private int mUserId;
77
78    // Visible for testing
79    public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
80
81    /**
82     * Get the component name string for the service or feature currently assigned to the
83     * accessiblity shortcut
84     *
85     * @param context A valid context
86     * @param userId The user ID of interest
87     * @return The flattened component name string of the service selected by the user, or the
88     *         string for the default service if the user has not made a selection
89     */
90    public static String getTargetServiceComponentNameString(
91            Context context, int userId) {
92        final String currentShortcutServiceId = Settings.Secure.getStringForUser(
93                context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
94                userId);
95        if (currentShortcutServiceId != null) {
96            return currentShortcutServiceId;
97        }
98        return context.getString(R.string.config_defaultAccessibilityService);
99    }
100
101    /**
102     * @return An immutable map from dummy component names to feature info for toggling a framework
103     *         feature
104     */
105    public static Map<ComponentName, ToggleableFrameworkFeatureInfo>
106        getFrameworkShortcutFeaturesMap() {
107        if (sFrameworkShortcutFeaturesMap == null) {
108            Map<ComponentName, ToggleableFrameworkFeatureInfo> featuresMap = new ArrayMap<>(2);
109            featuresMap.put(COLOR_INVERSION_COMPONENT_NAME,
110                    new ToggleableFrameworkFeatureInfo(
111                            Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
112                            "1" /* Value to enable */, "0" /* Value to disable */,
113                            R.string.color_inversion_feature_name));
114            featuresMap.put(DALTONIZER_COMPONENT_NAME,
115                    new ToggleableFrameworkFeatureInfo(
116                            Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
117                            "1" /* Value to enable */, "0" /* Value to disable */,
118                            R.string.color_correction_feature_name));
119            sFrameworkShortcutFeaturesMap = Collections.unmodifiableMap(featuresMap);
120        }
121        return sFrameworkShortcutFeaturesMap;
122    }
123
124    public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) {
125        mContext = context;
126        mUserId = initialUserId;
127
128        // Keep track of state of shortcut settings
129        final ContentObserver co = new ContentObserver(handler) {
130            @Override
131            public void onChange(boolean selfChange, Uri uri, int userId) {
132                if (userId == mUserId) {
133                    onSettingsChanged();
134                }
135            }
136        };
137        mContext.getContentResolver().registerContentObserver(
138                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
139                false, co, UserHandle.USER_ALL);
140        mContext.getContentResolver().registerContentObserver(
141                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED),
142                false, co, UserHandle.USER_ALL);
143        mContext.getContentResolver().registerContentObserver(
144                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN),
145                false, co, UserHandle.USER_ALL);
146        mContext.getContentResolver().registerContentObserver(
147                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN),
148                false, co, UserHandle.USER_ALL);
149        setCurrentUser(mUserId);
150    }
151
152    public void setCurrentUser(int currentUserId) {
153        mUserId = currentUserId;
154        onSettingsChanged();
155    }
156
157    /**
158     * Check if the shortcut is available.
159     *
160     * @param onLockScreen Whether or not the phone is currently locked.
161     *
162     * @return {@code true} if the shortcut is available
163     */
164    public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) {
165        return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen);
166    }
167
168    public void onSettingsChanged() {
169        final boolean haveValidService =
170                !TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId));
171        final ContentResolver cr = mContext.getContentResolver();
172        final boolean enabled = Settings.Secure.getIntForUser(
173                cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1;
174        // Enable the shortcut from the lockscreen by default if the dialog has been shown
175        final int dialogAlreadyShown = Settings.Secure.getIntForUser(
176                cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mUserId);
177        mEnabledOnLockScreen = Settings.Secure.getIntForUser(
178                cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
179                dialogAlreadyShown, mUserId) == 1;
180        mIsShortcutEnabled = enabled && haveValidService;
181    }
182
183    /**
184     * Called when the accessibility shortcut is activated
185     */
186    public void performAccessibilityShortcut() {
187        Slog.d(TAG, "Accessibility shortcut activated");
188        final ContentResolver cr = mContext.getContentResolver();
189        final int userId = ActivityManager.getCurrentUser();
190        final int dialogAlreadyShown = Settings.Secure.getIntForUser(
191                cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
192        // Use USAGE_ASSISTANCE_ACCESSIBILITY for TVs to ensure that TVs play the ringtone as they
193        // have less ways of providing feedback like vibration.
194        final int audioAttributesUsage = hasFeatureLeanback()
195                ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
196                : AudioAttributes.USAGE_NOTIFICATION_EVENT;
197
198        // Play a notification tone
199        final Ringtone tone =
200                RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
201        if (tone != null) {
202            tone.setAudioAttributes(new AudioAttributes.Builder()
203                .setUsage(audioAttributesUsage)
204                .build());
205            tone.play();
206        }
207
208        // Play a notification vibration
209        Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
210        if ((vibrator != null) && vibrator.hasVibrator()) {
211            // Don't check if haptics are disabled, as we need to alert the user that their
212            // way of interacting with the phone may change if they activate the shortcut
213            long[] vibePattern = convertToLongArray(
214                    mContext.getResources().getIntArray(R.array.config_longPressVibePattern));
215            vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
216        }
217
218
219        if (dialogAlreadyShown == 0) {
220            // The first time, we show a warning rather than toggle the service to give the user a
221            // chance to turn off this feature before stuff gets enabled.
222            mAlertDialog = createShortcutWarningDialog(userId);
223            if (mAlertDialog == null) {
224                return;
225            }
226            Window w = mAlertDialog.getWindow();
227            WindowManager.LayoutParams attr = w.getAttributes();
228            attr.type = TYPE_KEYGUARD_DIALOG;
229            w.setAttributes(attr);
230            mAlertDialog.show();
231            Settings.Secure.putIntForUser(
232                    cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1, userId);
233        } else {
234            if (mAlertDialog != null) {
235                mAlertDialog.dismiss();
236                mAlertDialog = null;
237            }
238
239            // Show a toast alerting the user to what's happening
240            final String serviceName = getShortcutFeatureDescription(false /* no summary */);
241            if (serviceName == null) {
242                Slog.e(TAG, "Accessibility shortcut set to invalid service");
243                return;
244            }
245            // For accessibility services, show a toast explaining what we're doing.
246            final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
247            if (serviceInfo != null) {
248                String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
249                        ? R.string.accessibility_shortcut_disabling_service
250                        : R.string.accessibility_shortcut_enabling_service);
251                String toastMessage = String.format(toastMessageFormatString, serviceName);
252                Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
253                        mContext, toastMessage, Toast.LENGTH_LONG);
254                warningToast.getWindowParams().privateFlags |=
255                        WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
256                warningToast.show();
257            }
258
259            mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
260                    .performAccessibilityShortcut();
261        }
262    }
263
264    private AlertDialog createShortcutWarningDialog(int userId) {
265        final String serviceDescription = getShortcutFeatureDescription(true /* Include summary */);
266
267        if (serviceDescription == null) {
268            return null;
269        }
270
271        final String warningMessage = String.format(
272                mContext.getString(R.string.accessibility_shortcut_toogle_warning),
273                serviceDescription);
274        final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(
275                // Use SystemUI context so we pick up any theme set in a vendor overlay
276                mFrameworkObjectProvider.getSystemUiContext())
277                .setTitle(R.string.accessibility_shortcut_warning_dialog_title)
278                .setMessage(warningMessage)
279                .setCancelable(false)
280                .setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
281                .setNegativeButton(R.string.disable_accessibility_shortcut,
282                        (DialogInterface d, int which) -> {
283                            Settings.Secure.putStringForUser(mContext.getContentResolver(),
284                                    Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
285                                    userId);
286                        })
287                .setOnCancelListener((DialogInterface d) -> {
288                    // If canceled, treat as if the dialog has never been shown
289                    Settings.Secure.putIntForUser(mContext.getContentResolver(),
290                        Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
291                })
292                .create();
293        return alertDialog;
294    }
295
296    private AccessibilityServiceInfo getInfoForTargetService() {
297        final String currentShortcutServiceString = getTargetServiceComponentNameString(
298                mContext, UserHandle.USER_CURRENT);
299        if (currentShortcutServiceString == null) {
300            return null;
301        }
302        AccessibilityManager accessibilityManager =
303                mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
304        return accessibilityManager.getInstalledServiceInfoWithComponentName(
305                        ComponentName.unflattenFromString(currentShortcutServiceString));
306    }
307
308    private String getShortcutFeatureDescription(boolean includeSummary) {
309        final String currentShortcutServiceString = getTargetServiceComponentNameString(
310                mContext, UserHandle.USER_CURRENT);
311        if (currentShortcutServiceString == null) {
312            return null;
313        }
314        final ComponentName targetComponentName =
315                ComponentName.unflattenFromString(currentShortcutServiceString);
316        final ToggleableFrameworkFeatureInfo frameworkFeatureInfo =
317                getFrameworkShortcutFeaturesMap().get(targetComponentName);
318        if (frameworkFeatureInfo != null) {
319            return frameworkFeatureInfo.getLabel(mContext);
320        }
321        final AccessibilityServiceInfo serviceInfo = mFrameworkObjectProvider
322                .getAccessibilityManagerInstance(mContext).getInstalledServiceInfoWithComponentName(
323                        targetComponentName);
324        if (serviceInfo == null) {
325            return null;
326        }
327        final PackageManager pm = mContext.getPackageManager();
328        String label = serviceInfo.getResolveInfo().loadLabel(pm).toString();
329        CharSequence summary = serviceInfo.loadSummary(pm);
330        if (!includeSummary || TextUtils.isEmpty(summary)) {
331            return label;
332        }
333        return String.format("%s\n%s", label, summary);
334    }
335
336    private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
337        AccessibilityManager accessibilityManager =
338                mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
339        return accessibilityManager.getEnabledAccessibilityServiceList(
340                AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
341    }
342
343    private boolean hasFeatureLeanback() {
344        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
345    }
346
347    /**
348     * Immutable class to hold info about framework features that can be controlled by shortcut
349     */
350    public static class ToggleableFrameworkFeatureInfo {
351        private final String mSettingKey;
352        private final String mSettingOnValue;
353        private final String mSettingOffValue;
354        private final int mLabelStringResourceId;
355        // These go to the settings wrapper
356        private int mIconDrawableId;
357
358        ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue,
359                String settingOffValue, int labelStringResourceId) {
360            mSettingKey = settingKey;
361            mSettingOnValue = settingOnValue;
362            mSettingOffValue = settingOffValue;
363            mLabelStringResourceId = labelStringResourceId;
364        }
365
366        /**
367         * @return The settings key to toggle between two values
368         */
369        public String getSettingKey() {
370            return mSettingKey;
371        }
372
373        /**
374         * @return The value to write to settings to turn the feature on
375         */
376        public String getSettingOnValue() {
377            return mSettingOnValue;
378        }
379
380        /**
381         * @return The value to write to settings to turn the feature off
382         */
383        public String getSettingOffValue() {
384            return mSettingOffValue;
385        }
386
387        public String getLabel(Context context) {
388            return context.getString(mLabelStringResourceId);
389        }
390    }
391
392    // Class to allow mocking of static framework calls
393    public static class FrameworkObjectProvider {
394        public AccessibilityManager getAccessibilityManagerInstance(Context context) {
395            return AccessibilityManager.getInstance(context);
396        }
397
398        public AlertDialog.Builder getAlertDialogBuilder(Context context) {
399            return new AlertDialog.Builder(context);
400        }
401
402        public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
403            return Toast.makeText(context, charSequence, duration);
404        }
405
406        public Context getSystemUiContext() {
407            return ActivityThread.currentActivityThread().getSystemUiContext();
408        }
409    }
410}
411