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