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.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 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 63 /** 64 * The interface to execute the uninstallation action. 65 */ 66 interface UninstallActionPerformer { uninstallPackage()67 void uninstallPackage(); 68 } 69 70 /** 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)71 public static Dialog createCapabilitiesDialog(@NonNull Context context, 72 @NonNull AccessibilityServiceInfo info, @NonNull View.OnClickListener listener, 73 @NonNull UninstallActionPerformer performer) { 74 final AlertDialog ad = new AlertDialog.Builder(context) 75 .setView(createEnableDialogContentView(context, info, listener, performer)) 76 .create(); 77 78 Window window = ad.getWindow(); 79 WindowManager.LayoutParams params = window.getAttributes(); 80 params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 81 window.setAttributes(params); 82 ad.create(); 83 ad.setCanceledOnTouchOutside(true); 84 85 return ad; 86 } 87 createEnableDialogContentView(Context context, @NonNull AccessibilityServiceInfo info, View.OnClickListener listener, UninstallActionPerformer performer)88 private static View createEnableDialogContentView(Context context, 89 @NonNull AccessibilityServiceInfo info, View.OnClickListener listener, 90 UninstallActionPerformer performer) { 91 LayoutInflater inflater = (LayoutInflater) context.getSystemService( 92 Context.LAYOUT_INFLATER_SERVICE); 93 94 View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content, 95 null); 96 97 final Drawable icon; 98 if (info.getResolveInfo().getIconResource() == 0) { 99 icon = ContextCompat.getDrawable(context, R.drawable.ic_accessibility_generic); 100 } else { 101 icon = info.getResolveInfo().loadIcon(context.getPackageManager()); 102 } 103 104 ImageView permissionDialogIcon = content.findViewById( 105 R.id.permissionDialog_icon); 106 permissionDialogIcon.setImageDrawable(icon); 107 108 TextView permissionDialogTitle = content.findViewById(R.id.permissionDialog_title); 109 permissionDialogTitle.setText(context.getString(R.string.enable_service_title, 110 getServiceName(context, info))); 111 112 Button permissionAllowButton = content.findViewById( 113 R.id.permission_enable_allow_button); 114 Button permissionDenyButton = content.findViewById( 115 R.id.permission_enable_deny_button); 116 permissionAllowButton.setOnClickListener(listener); 117 permissionAllowButton.setOnTouchListener(filterTouchListener); 118 permissionDenyButton.setOnClickListener(listener); 119 120 final Button uninstallButton = content.findViewById( 121 R.id.permission_enable_uninstall_button); 122 // Shows an uninstall button to help users quickly remove the non-system App due to the 123 // required permissions. 124 if (!AccessibilityUtil.isSystemApp(info)) { 125 uninstallButton.setVisibility(View.VISIBLE); 126 uninstallButton.setOnClickListener(v -> performer.uninstallPackage()); 127 } 128 return content; 129 } 130 131 /** Returns a {@link Dialog} to be shown to confirm that they want to disable a service. */ createDisableDialog(Context context, AccessibilityServiceInfo info, DialogInterface.OnClickListener listener)132 public static Dialog createDisableDialog(Context context, 133 AccessibilityServiceInfo info, DialogInterface.OnClickListener listener) { 134 final AlertDialog dialog = new AlertDialog.Builder(context) 135 .setTitle(context.getString(R.string.disable_service_title, 136 info.getResolveInfo().loadLabel(context.getPackageManager()))) 137 .setMessage(context.getString(R.string.disable_service_message, 138 context.getString(R.string.accessibility_dialog_button_stop), 139 getServiceName(context, info))) 140 .setCancelable(true) 141 .setPositiveButton(R.string.accessibility_dialog_button_stop, listener) 142 .setNegativeButton(R.string.accessibility_dialog_button_cancel, listener) 143 .create(); 144 145 return dialog; 146 } 147 148 // Get the service name and bidi wrap it to protect from bidi side effects. getServiceName(Context context, AccessibilityServiceInfo info)149 private static CharSequence getServiceName(Context context, AccessibilityServiceInfo info) { 150 final Locale locale = context.getResources().getConfiguration().getLocales().get(0); 151 final CharSequence label = 152 info.getResolveInfo().loadLabel(context.getPackageManager()); 153 return BidiFormatter.getInstance(locale).unicodeWrap(label); 154 } 155 } 156