1 /*
2  * Copyright (C) 2016 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.settings.accessibility;
18 
19 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
20 
21 import android.accessibilityservice.AccessibilityServiceInfo;
22 import android.app.Activity;
23 import android.app.Dialog;
24 import android.content.Context;
25 import android.graphics.drawable.Drawable;
26 import android.os.storage.StorageManager;
27 import android.text.BidiFormatter;
28 import android.view.LayoutInflater;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.Window;
32 import android.view.WindowManager;
33 import android.widget.Button;
34 import android.widget.ImageView;
35 import android.widget.TextView;
36 import android.widget.Toast;
37 
38 import androidx.appcompat.app.AlertDialog;
39 import androidx.core.content.ContextCompat;
40 
41 import com.android.settings.R;
42 
43 import java.util.Locale;
44 
45 /**
46  * Utility class for creating the dialog that asks users for explicit permission for an
47  * accessibility service to access user data before the service is enabled
48  */
49 public class AccessibilityServiceWarning {
50     private static final View.OnTouchListener filterTouchListener = (View v, MotionEvent event) -> {
51         // Filter obscured touches by consuming them.
52         if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)
53                 || ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) {
54             if (event.getAction() == MotionEvent.ACTION_UP) {
55                 Toast.makeText(v.getContext(), R.string.touch_filtered_warning,
56                         Toast.LENGTH_SHORT).show();
57             }
58             return true;
59         }
60         return false;
61     };
62 
createCapabilitiesDialog(Activity parentActivity, AccessibilityServiceInfo info, View.OnClickListener listener)63     public static Dialog createCapabilitiesDialog(Activity parentActivity,
64             AccessibilityServiceInfo info, View.OnClickListener listener) {
65         final AlertDialog ad = new AlertDialog.Builder(parentActivity)
66                 .setView(createEnableDialogContentView(parentActivity, info, listener))
67                 .create();
68 
69         Window window = ad.getWindow();
70         WindowManager.LayoutParams params = window.getAttributes();
71         params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
72         window.setAttributes(params);
73         ad.create();
74         ad.setCanceledOnTouchOutside(true);
75 
76         return ad;
77     }
78 
createDisableDialog(Activity parentActivity, AccessibilityServiceInfo info, View.OnClickListener listener)79     public static Dialog createDisableDialog(Activity parentActivity,
80             AccessibilityServiceInfo info, View.OnClickListener listener) {
81         final AlertDialog ad = new AlertDialog.Builder(parentActivity)
82                 .setView(createDisableDialogContentView(parentActivity, info, listener))
83                 .setCancelable(true)
84                 .create();
85 
86         return ad;
87     }
88 
89     /**
90      * Return whether the device is encrypted with legacy full disk encryption. Newer devices
91      * should be using File Based Encryption.
92      *
93      * @return true if device is encrypted
94      */
isFullDiskEncrypted()95     private static boolean isFullDiskEncrypted() {
96         return StorageManager.isNonDefaultBlockEncrypted();
97     }
98 
99     /**
100      * Get a content View for a dialog to confirm that they want to enable a service.
101      *
102      * @param context A valid context
103      * @param info The info about a service
104      * @return A content view suitable for viewing
105      */
createEnableDialogContentView(Context context, AccessibilityServiceInfo info, View.OnClickListener listener)106     private static View createEnableDialogContentView(Context context,
107             AccessibilityServiceInfo info, View.OnClickListener listener) {
108         LayoutInflater inflater = (LayoutInflater) context.getSystemService(
109                 Context.LAYOUT_INFLATER_SERVICE);
110 
111         View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content,
112                 null);
113 
114         TextView encryptionWarningView = (TextView) content.findViewById(
115                 R.id.encryption_warning);
116         if (isFullDiskEncrypted()) {
117             String text = context.getString(R.string.enable_service_encryption_warning,
118                     getServiceName(context, info));
119             encryptionWarningView.setText(text);
120             encryptionWarningView.setVisibility(View.VISIBLE);
121         } else {
122             encryptionWarningView.setVisibility(View.GONE);
123         }
124 
125         final Drawable icon;
126         if (info.getResolveInfo().getIconResource() == 0) {
127             icon = ContextCompat.getDrawable(context, R.drawable.ic_accessibility_generic);
128         } else {
129             icon = info.getResolveInfo().loadIcon(context.getPackageManager());
130         }
131 
132         ImageView permissionDialogIcon = content.findViewById(
133                 R.id.permissionDialog_icon);
134         permissionDialogIcon.setImageDrawable(icon);
135 
136         TextView permissionDialogTitle = content.findViewById(R.id.permissionDialog_title);
137         permissionDialogTitle.setText(context.getString(R.string.enable_service_title,
138                 getServiceName(context, info)));
139 
140         Button permissionAllowButton = content.findViewById(
141                 R.id.permission_enable_allow_button);
142         Button permissionDenyButton = content.findViewById(
143                 R.id.permission_enable_deny_button);
144         permissionAllowButton.setOnClickListener(listener);
145         permissionAllowButton.setOnTouchListener(filterTouchListener);
146         permissionDenyButton.setOnClickListener(listener);
147 
148         return content;
149     }
150 
createDisableDialogContentView(Context context, AccessibilityServiceInfo info, View.OnClickListener listener)151     private static View createDisableDialogContentView(Context context,
152             AccessibilityServiceInfo info, View.OnClickListener listener) {
153         LayoutInflater inflater = (LayoutInflater) context.getSystemService(
154                 Context.LAYOUT_INFLATER_SERVICE);
155 
156         View content = inflater.inflate(R.layout.disable_accessibility_service_dialog_content,
157                 null);
158 
159         TextView permissionDialogTitle = content.findViewById(R.id.permissionDialog_disable_title);
160         permissionDialogTitle.setText(context.getString(R.string.disable_service_title,
161                 getServiceName(context, info)));
162         TextView permissionDialogMessage = content
163                 .findViewById(R.id.permissionDialog_disable_message);
164         permissionDialogMessage.setText(context.getString(R.string.disable_service_message,
165                 context.getString(R.string.accessibility_dialog_button_stop),
166                 getServiceName(context, info)));
167 
168         Button permissionAllowButton = content.findViewById(
169                 R.id.permission_disable_stop_button);
170         Button permissionDenyButton = content.findViewById(
171                 R.id.permission_disable_cancel_button);
172         permissionAllowButton.setOnClickListener(listener);
173         permissionDenyButton.setOnClickListener(listener);
174 
175         return content;
176     }
177 
178     // Get the service name and bidi wrap it to protect from bidi side effects.
getServiceName(Context context, AccessibilityServiceInfo info)179     private static CharSequence getServiceName(Context context, AccessibilityServiceInfo info) {
180         final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
181         final CharSequence label =
182                 info.getResolveInfo().loadLabel(context.getPackageManager());
183         return BidiFormatter.getInstance(locale).unicodeWrap(label);
184     }
185 }
186