• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Dialog;
23 import android.content.Context;
24 import android.content.DialogInterface;
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.annotation.NonNull;
39 import androidx.appcompat.app.AlertDialog;
40 import androidx.core.content.ContextCompat;
41 
42 import com.android.settings.R;
43 
44 import java.util.Locale;
45 
46 /**
47  * Utility class for creating the dialog that asks users for explicit permission for an
48  * accessibility service to access user data before the service is enabled
49  */
50 public class AccessibilityServiceWarning {
51     private static final View.OnTouchListener filterTouchListener = (View v, MotionEvent event) -> {
52         // Filter obscured touches by consuming them.
53         if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)
54                 || ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) {
55             if (event.getAction() == MotionEvent.ACTION_UP) {
56                 Toast.makeText(v.getContext(), R.string.touch_filtered_warning,
57                         Toast.LENGTH_SHORT).show();
58             }
59             return true;
60         }
61         return false;
62     };
63 
64     /**
65      * The interface to execute the uninstallation action.
66      */
67     interface UninstallActionPerformer {
uninstallPackage()68         void uninstallPackage();
69     }
70 
71     /** Returns a {@link Dialog} to be shown to confirm that they want to enable a service. */
createCapabilitiesDialog(@onNull Context context, @NonNull AccessibilityServiceInfo info, @NonNull View.OnClickListener listener, @NonNull UninstallActionPerformer performer)72     public static Dialog createCapabilitiesDialog(@NonNull Context context,
73             @NonNull AccessibilityServiceInfo info, @NonNull View.OnClickListener listener,
74             @NonNull UninstallActionPerformer performer) {
75         final AlertDialog ad = new AlertDialog.Builder(context)
76                 .setView(createEnableDialogContentView(context, info, listener, performer))
77                 .create();
78 
79         Window window = ad.getWindow();
80         WindowManager.LayoutParams params = window.getAttributes();
81         params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
82         window.setAttributes(params);
83         ad.create();
84         ad.setCanceledOnTouchOutside(true);
85 
86         return ad;
87     }
88 
89     /**
90      * Returns 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 
createEnableDialogContentView(Context context, @NonNull AccessibilityServiceInfo info, View.OnClickListener listener, UninstallActionPerformer performer)99     private static View createEnableDialogContentView(Context context,
100             @NonNull AccessibilityServiceInfo info, View.OnClickListener listener,
101             UninstallActionPerformer performer) {
102         LayoutInflater inflater = (LayoutInflater) context.getSystemService(
103                 Context.LAYOUT_INFLATER_SERVICE);
104 
105         View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content,
106                 null);
107 
108         TextView encryptionWarningView = (TextView) content.findViewById(
109                 R.id.encryption_warning);
110         if (isFullDiskEncrypted()) {
111             String text = context.getString(R.string.enable_service_encryption_warning,
112                     getServiceName(context, info));
113             encryptionWarningView.setText(text);
114             encryptionWarningView.setVisibility(View.VISIBLE);
115         } else {
116             encryptionWarningView.setVisibility(View.GONE);
117         }
118 
119         final Drawable icon;
120         if (info.getResolveInfo().getIconResource() == 0) {
121             icon = ContextCompat.getDrawable(context, R.drawable.ic_accessibility_generic);
122         } else {
123             icon = info.getResolveInfo().loadIcon(context.getPackageManager());
124         }
125 
126         ImageView permissionDialogIcon = content.findViewById(
127                 R.id.permissionDialog_icon);
128         permissionDialogIcon.setImageDrawable(icon);
129 
130         TextView permissionDialogTitle = content.findViewById(R.id.permissionDialog_title);
131         permissionDialogTitle.setText(context.getString(R.string.enable_service_title,
132                 getServiceName(context, info)));
133 
134         Button permissionAllowButton = content.findViewById(
135                 R.id.permission_enable_allow_button);
136         Button permissionDenyButton = content.findViewById(
137                 R.id.permission_enable_deny_button);
138         permissionAllowButton.setOnClickListener(listener);
139         permissionAllowButton.setOnTouchListener(filterTouchListener);
140         permissionDenyButton.setOnClickListener(listener);
141 
142         final Button uninstallButton = content.findViewById(
143                 R.id.permission_enable_uninstall_button);
144         // Shows an uninstall button to help users quickly remove the non-system App due to the
145         // required permissions.
146         if (!AccessibilityUtil.isSystemApp(info)) {
147             uninstallButton.setVisibility(View.VISIBLE);
148             uninstallButton.setOnClickListener(v -> performer.uninstallPackage());
149         }
150         return content;
151     }
152 
153     /** Returns a {@link Dialog} to be shown to confirm that they want to disable a service. */
createDisableDialog(Context context, AccessibilityServiceInfo info, DialogInterface.OnClickListener listener)154     public static Dialog createDisableDialog(Context context,
155             AccessibilityServiceInfo info, DialogInterface.OnClickListener listener) {
156         final AlertDialog dialog = new AlertDialog.Builder(context)
157                 .setTitle(context.getString(R.string.disable_service_title,
158                         info.getResolveInfo().loadLabel(context.getPackageManager())))
159                 .setMessage(context.getString(R.string.disable_service_message,
160                         context.getString(R.string.accessibility_dialog_button_stop),
161                         getServiceName(context, info)))
162                 .setCancelable(true)
163                 .setPositiveButton(R.string.accessibility_dialog_button_stop, listener)
164                 .setNegativeButton(R.string.accessibility_dialog_button_cancel, listener)
165                 .create();
166 
167         return dialog;
168     }
169 
170     // Get the service name and bidi wrap it to protect from bidi side effects.
getServiceName(Context context, AccessibilityServiceInfo info)171     private static CharSequence getServiceName(Context context, AccessibilityServiceInfo info) {
172         final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
173         final CharSequence label =
174                 info.getResolveInfo().loadLabel(context.getPackageManager());
175         return BidiFormatter.getInstance(locale).unicodeWrap(label);
176     }
177 }
178