1 /*
2  * Copyright (C) 2020 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 package com.android.internal.accessibility.dialog;
17 
18 import static com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode;
19 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
20 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
21 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
22 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getInstalledTargets;
23 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
24 import static com.android.internal.accessibility.util.AccessibilityUtils.isUserSetupCompleted;
25 
26 import android.annotation.Nullable;
27 import android.app.Activity;
28 import android.app.AlertDialog;
29 import android.app.Dialog;
30 import android.app.KeyguardManager;
31 import android.content.Context;
32 import android.content.DialogInterface;
33 import android.content.res.TypedArray;
34 import android.os.Bundle;
35 import android.view.View;
36 import android.view.Window;
37 import android.view.WindowManager;
38 import android.view.accessibility.AccessibilityManager;
39 import android.widget.AdapterView;
40 
41 import com.android.internal.R;
42 import com.android.internal.annotations.VisibleForTesting;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 
47 /**
48  * Activity used to display various targets related to accessibility service, accessibility
49  * activity or allowlisting feature for volume key shortcut.
50  */
51 public class AccessibilityShortcutChooserActivity extends Activity {
52     @UserShortcutType
53     private final int mShortcutType = HARDWARE;
54     private static final String KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE =
55             "accessibility_shortcut_menu_mode";
56     private final List<AccessibilityTarget> mTargets = new ArrayList<>();
57     private AlertDialog mMenuDialog;
58     private Dialog mPermissionDialog;
59     private ShortcutTargetAdapter mTargetAdapter;
60 
61     @Override
onCreate(@ullable Bundle savedInstanceState)62     protected void onCreate(@Nullable Bundle savedInstanceState) {
63         super.onCreate(savedInstanceState);
64 
65         final TypedArray theme = getTheme().obtainStyledAttributes(android.R.styleable.Theme);
66         if (!theme.getBoolean(android.R.styleable.Theme_windowNoTitle, /* defValue= */ false)) {
67             requestWindowFeature(Window.FEATURE_NO_TITLE);
68         }
69 
70         mTargets.addAll(getTargets(this, mShortcutType));
71         mTargetAdapter = new ShortcutTargetAdapter(mTargets);
72         mMenuDialog = createMenuDialog();
73         mMenuDialog.setOnShowListener(dialog -> updateDialogListeners());
74         mMenuDialog.show();
75 
76         if (savedInstanceState != null) {
77             final int restoreShortcutMenuMode =
78                     savedInstanceState.getInt(KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE,
79                             ShortcutMenuMode.LAUNCH);
80             if (restoreShortcutMenuMode == ShortcutMenuMode.EDIT) {
81                 onEditButtonClicked();
82             }
83         }
84     }
85 
86     @Override
onDestroy()87     protected void onDestroy() {
88         mMenuDialog.setOnDismissListener(null);
89         mMenuDialog.dismiss();
90         super.onDestroy();
91     }
92 
93     @Override
onSaveInstanceState(Bundle outState)94     protected void onSaveInstanceState(Bundle outState) {
95         super.onSaveInstanceState(outState);
96         outState.putInt(KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE, mTargetAdapter.getShortcutMenuMode());
97     }
98 
onTargetSelected(AdapterView<?> parent, View view, int position, long id)99     private void onTargetSelected(AdapterView<?> parent, View view, int position, long id) {
100         final AccessibilityTarget target = mTargets.get(position);
101         if (target instanceof AccessibilityServiceTarget
102                 || target instanceof AccessibilityActivityTarget) {
103             if (sendRestrictedDialogIntentIfNeeded(target)) {
104                 return;
105             }
106         }
107 
108         target.onSelected();
109         mMenuDialog.dismiss();
110     }
111 
onTargetChecked(AdapterView<?> parent, View view, int position, long id)112     private void onTargetChecked(AdapterView<?> parent, View view, int position, long id) {
113         final AccessibilityTarget target = mTargets.get(position);
114 
115         if (target instanceof AccessibilityServiceTarget serviceTarget) {
116             if (sendRestrictedDialogIntentIfNeeded(target)) {
117                 return;
118             }
119             final AccessibilityManager am = getSystemService(AccessibilityManager.class);
120             if (am.isAccessibilityServiceWarningRequired(
121                     serviceTarget.getAccessibilityServiceInfo())) {
122                 showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
123                         position, mTargetAdapter);
124                 return;
125             }
126         }
127         if (target instanceof AccessibilityActivityTarget activityTarget) {
128             if (!activityTarget.isShortcutEnabled()
129                     && sendRestrictedDialogIntentIfNeeded(activityTarget)) {
130                 return;
131             }
132         }
133 
134         target.onCheckedChanged(!target.isShortcutEnabled());
135         mTargetAdapter.notifyDataSetChanged();
136     }
137 
138     /**
139      * Sends restricted dialog intent if the accessibility target is disallowed.
140      *
141      * @return true if sends restricted dialog intent, otherwise false.
142      */
sendRestrictedDialogIntentIfNeeded(AccessibilityTarget target)143     private boolean sendRestrictedDialogIntentIfNeeded(AccessibilityTarget target) {
144         if (AccessibilityTargetHelper.isAccessibilityTargetAllowed(this,
145                 target.getComponentName().getPackageName(), target.getUid())) {
146             return false;
147         }
148 
149         AccessibilityTargetHelper.sendRestrictedDialogIntent(this,
150                 target.getComponentName().getPackageName(), target.getUid());
151         return true;
152     }
153 
showPermissionDialogIfNeeded(Context context, AccessibilityServiceTarget serviceTarget, int position, ShortcutTargetAdapter targetAdapter)154     private void showPermissionDialogIfNeeded(Context context,
155             AccessibilityServiceTarget serviceTarget, int position,
156             ShortcutTargetAdapter targetAdapter) {
157         if (mPermissionDialog != null) {
158             return;
159         }
160 
161         mPermissionDialog = AccessibilityServiceWarning
162                 .createAccessibilityServiceWarningDialog(context,
163                         serviceTarget.getAccessibilityServiceInfo(),
164                         v -> {
165                             serviceTarget.onCheckedChanged(true);
166                             targetAdapter.notifyDataSetChanged();
167                             mPermissionDialog.dismiss();
168                         }, v -> {
169                             serviceTarget.onCheckedChanged(false);
170                             mPermissionDialog.dismiss();
171                         },
172                         v -> {
173                             mTargets.remove(position);
174                             context.getPackageManager().getPackageInstaller().uninstall(
175                                     serviceTarget.getComponentName().getPackageName(), null);
176                             targetAdapter.notifyDataSetChanged();
177                             mPermissionDialog.dismiss();
178                         });
179         mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
180         mPermissionDialog.show();
181     }
182 
onDoneButtonClicked()183     private void onDoneButtonClicked() {
184         mTargets.clear();
185         mTargets.addAll(getTargets(this, mShortcutType));
186         if (mTargets.isEmpty()) {
187             mMenuDialog.dismiss();
188             return;
189         }
190 
191         mTargetAdapter.setShortcutMenuMode(ShortcutMenuMode.LAUNCH);
192         mTargetAdapter.notifyDataSetChanged();
193 
194         mMenuDialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(
195                 getString(R.string.edit_accessibility_shortcut_menu_button));
196 
197         updateDialogListeners();
198     }
199 
onEditButtonClicked()200     private void onEditButtonClicked() {
201         mTargets.clear();
202         mTargets.addAll(getInstalledTargets(this, mShortcutType));
203         mTargetAdapter.setShortcutMenuMode(ShortcutMenuMode.EDIT);
204         mTargetAdapter.notifyDataSetChanged();
205 
206         mMenuDialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(
207                 getString(R.string.done_accessibility_shortcut_menu_button));
208 
209         updateDialogListeners();
210     }
211 
updateDialogListeners()212     private void updateDialogListeners() {
213         final boolean isEditMenuMode =
214                 mTargetAdapter.getShortcutMenuMode() == ShortcutMenuMode.EDIT;
215         final int selectDialogTitleId = R.string.accessibility_select_shortcut_menu_title;
216         final int editDialogTitleId =
217                 mShortcutType == SOFTWARE
218                         ? R.string.accessibility_edit_shortcut_menu_button_title
219                         : R.string.accessibility_edit_shortcut_menu_volume_title;
220 
221         mMenuDialog.setTitle(getString(isEditMenuMode ? editDialogTitleId : selectDialogTitleId));
222         mMenuDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(
223                 isEditMenuMode ? view -> onDoneButtonClicked() : view -> onEditButtonClicked());
224         mMenuDialog.getListView().setOnItemClickListener(
225                 isEditMenuMode ? this::onTargetChecked : this::onTargetSelected);
226     }
227 
228     @VisibleForTesting
getMenuDialog()229     public AlertDialog getMenuDialog() {
230         return mMenuDialog;
231     }
232 
233     @VisibleForTesting
getPermissionDialog()234     public Dialog getPermissionDialog() {
235         return mPermissionDialog;
236     }
237 
createMenuDialog()238     private AlertDialog createMenuDialog() {
239         final String dialogTitle =
240                 getString(R.string.accessibility_select_shortcut_menu_title);
241 
242         final AlertDialog.Builder builder = new AlertDialog.Builder(this)
243                 .setTitle(dialogTitle)
244                 .setAdapter(mTargetAdapter, /* listener= */ null)
245                 .setOnDismissListener(dialog -> finish());
246 
247         boolean allowEditing = isUserSetupCompleted(this);
248         boolean showWhenLocked = false;
249         final KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
250         if (keyguardManager != null && keyguardManager.isKeyguardLocked()) {
251             allowEditing = false;
252             showWhenLocked = true;
253         }
254         if (allowEditing) {
255             final String positiveButtonText =
256                     getString(R.string.edit_accessibility_shortcut_menu_button);
257             builder.setPositiveButton(positiveButtonText, /* listener= */ null);
258         }
259 
260         final AlertDialog dialog = builder.create();
261         if (showWhenLocked) {
262             dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
263         }
264         return dialog;
265     }
266 }
267