1 /*
2  * Copyright (C) 2023 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.internal.accessibility.dialog;
18 
19 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
20 
21 import android.accessibilityservice.AccessibilityServiceInfo;
22 import android.annotation.SuppressLint;
23 import android.app.AlertDialog;
24 import android.content.Context;
25 import android.graphics.drawable.Drawable;
26 import android.text.BidiFormatter;
27 import android.view.LayoutInflater;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.Window;
31 import android.view.WindowManager;
32 import android.widget.Button;
33 import android.widget.ImageView;
34 import android.widget.TextView;
35 import android.widget.Toast;
36 
37 import androidx.annotation.NonNull;
38 
39 import com.android.internal.R;
40 import com.android.internal.annotations.VisibleForTesting;
41 
42 import java.util.Locale;
43 
44 /**
45  * Utility class for creating the dialog that asks the user for explicit permission
46  * before an accessibility service is enabled.
47  */
48 public class AccessibilityServiceWarning {
49 
50     /**
51      * Returns an {@link AlertDialog} to be shown to confirm that the user
52      * wants to enable an {@link android.accessibilityservice.AccessibilityService}.
53      */
createAccessibilityServiceWarningDialog(@onNull Context context, @NonNull AccessibilityServiceInfo info, @NonNull View.OnClickListener allowListener, @NonNull View.OnClickListener denyListener, @NonNull View.OnClickListener uninstallListener)54     public static AlertDialog createAccessibilityServiceWarningDialog(@NonNull Context context,
55             @NonNull AccessibilityServiceInfo info,
56             @NonNull View.OnClickListener allowListener,
57             @NonNull View.OnClickListener denyListener,
58             @NonNull View.OnClickListener uninstallListener) {
59         final AlertDialog ad = new AlertDialog.Builder(context)
60                 .setView(createAccessibilityServiceWarningDialogContentView(
61                                 context, info, allowListener, denyListener, uninstallListener))
62                 .setCancelable(true)
63                 .create();
64         Window window = ad.getWindow();
65         WindowManager.LayoutParams params = window.getAttributes();
66         params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
67         params.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
68         window.setAttributes(params);
69         return ad;
70     }
71 
72     @VisibleForTesting
createAccessibilityServiceWarningDialogContentView(Context context, AccessibilityServiceInfo info, View.OnClickListener allowListener, View.OnClickListener denyListener, View.OnClickListener uninstallListener)73     public static View createAccessibilityServiceWarningDialogContentView(Context context,
74             AccessibilityServiceInfo info,
75             View.OnClickListener allowListener,
76             View.OnClickListener denyListener,
77             View.OnClickListener uninstallListener) {
78         final LayoutInflater inflater = context.getSystemService(LayoutInflater.class);
79         final View content = inflater.inflate(R.layout.accessibility_service_warning, null);
80 
81         final Drawable icon;
82         if (info.getResolveInfo().getIconResource() == 0) {
83             icon = context.getDrawable(R.drawable.ic_accessibility_generic);
84         } else {
85             icon = info.getResolveInfo().loadIcon(context.getPackageManager());
86         }
87         final ImageView permissionDialogIcon = content.findViewById(
88                 R.id.accessibility_permissionDialog_icon);
89         permissionDialogIcon.setImageDrawable(icon);
90 
91         final TextView permissionDialogTitle = content.findViewById(
92                 R.id.accessibility_permissionDialog_title);
93         permissionDialogTitle.setText(context.getString(R.string.accessibility_enable_service_title,
94                 getServiceName(context, info)));
95 
96         final Button permissionAllowButton = content.findViewById(
97                 R.id.accessibility_permission_enable_allow_button);
98         final Button permissionDenyButton = content.findViewById(
99                 R.id.accessibility_permission_enable_deny_button);
100         permissionAllowButton.setOnClickListener(allowListener);
101         permissionAllowButton.setOnTouchListener(getTouchConsumingListener());
102         permissionDenyButton.setOnClickListener(denyListener);
103 
104         final Button uninstallButton = content.findViewById(
105                 R.id.accessibility_permission_enable_uninstall_button);
106         // Show an uninstall button to help users quickly remove non-preinstalled apps.
107         if (!info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp()) {
108             uninstallButton.setVisibility(View.VISIBLE);
109             uninstallButton.setOnClickListener(uninstallListener);
110         }
111         return content;
112     }
113 
114     @VisibleForTesting
115     @SuppressLint("ClickableViewAccessibility") // Touches are intentionally consumed
getTouchConsumingListener()116     public static View.OnTouchListener getTouchConsumingListener() {
117         return (view, event) -> {
118             // Filter obscured touches by consuming them.
119             if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)
120                     || ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) {
121                 if (event.getAction() == MotionEvent.ACTION_UP) {
122                     Toast.makeText(view.getContext(),
123                             R.string.accessibility_dialog_touch_filtered_warning,
124                             Toast.LENGTH_SHORT).show();
125                 }
126                 return true;
127             }
128             return false;
129         };
130     }
131 
132     // Get the service name and bidi wrap it to protect from bidi side effects.
133     private static CharSequence getServiceName(Context context, AccessibilityServiceInfo info) {
134         final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
135         final CharSequence label =
136                 info.getResolveInfo().loadLabel(context.getPackageManager());
137         return BidiFormatter.getInstance(locale).unicodeWrap(label);
138     }
139 }
140