blob: dcf78977c7468188c0b54d84828fe4d6f552f283 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.accessibility;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.os.storage.StorageManager;
import android.text.BidiFormatter;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.android.settings.R;
import java.util.Locale;
/**
* Utility class for creating the dialog that asks users for explicit permission for an
* accessibility service to access user data before the service is enabled
*/
public class AccessibilityServiceWarning {
private static final View.OnTouchListener filterTouchListener = (View v, MotionEvent event) -> {
// Filter obscured touches by consuming them.
if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)
|| ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) {
if (event.getAction() == MotionEvent.ACTION_UP) {
Toast.makeText(v.getContext(), R.string.touch_filtered_warning,
Toast.LENGTH_SHORT).show();
}
return true;
}
return false;
};
/**
* The interface to execute the uninstallation action.
*/
interface UninstallActionPerformer {
void uninstallPackage();
}
/** Returns a {@link Dialog} to be shown to confirm that they want to enable a service. */
public static Dialog createCapabilitiesDialog(@NonNull Context context,
@NonNull AccessibilityServiceInfo info, @NonNull View.OnClickListener listener,
@NonNull UninstallActionPerformer performer) {
final AlertDialog ad = new AlertDialog.Builder(context)
.setView(createEnableDialogContentView(context, info, listener, performer))
.create();
Window window = ad.getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
window.setAttributes(params);
ad.create();
ad.setCanceledOnTouchOutside(true);
return ad;
}
/**
* Returns whether the device is encrypted with legacy full disk encryption. Newer devices
* should be using File Based Encryption.
*
* @return true if device is encrypted
*/
private static boolean isFullDiskEncrypted() {
return StorageManager.isNonDefaultBlockEncrypted();
}
private static View createEnableDialogContentView(Context context,
@NonNull AccessibilityServiceInfo info, View.OnClickListener listener,
UninstallActionPerformer performer) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content,
null);
TextView encryptionWarningView = (TextView) content.findViewById(
R.id.encryption_warning);
if (isFullDiskEncrypted()) {
String text = context.getString(R.string.enable_service_encryption_warning,
getServiceName(context, info));
encryptionWarningView.setText(text);
encryptionWarningView.setVisibility(View.VISIBLE);
} else {
encryptionWarningView.setVisibility(View.GONE);
}
final Drawable icon;
if (info.getResolveInfo().getIconResource() == 0) {
icon = ContextCompat.getDrawable(context, R.drawable.ic_accessibility_generic);
} else {
icon = info.getResolveInfo().loadIcon(context.getPackageManager());
}
ImageView permissionDialogIcon = content.findViewById(
R.id.permissionDialog_icon);
permissionDialogIcon.setImageDrawable(icon);
TextView permissionDialogTitle = content.findViewById(R.id.permissionDialog_title);
permissionDialogTitle.setText(context.getString(R.string.enable_service_title,
getServiceName(context, info)));
Button permissionAllowButton = content.findViewById(
R.id.permission_enable_allow_button);
Button permissionDenyButton = content.findViewById(
R.id.permission_enable_deny_button);
permissionAllowButton.setOnClickListener(listener);
permissionAllowButton.setOnTouchListener(filterTouchListener);
permissionDenyButton.setOnClickListener(listener);
final Button uninstallButton = content.findViewById(
R.id.permission_enable_uninstall_button);
// Shows an uninstall button to help users quickly remove the non-system App due to the
// required permissions.
if (!AccessibilityUtil.isSystemApp(info)) {
uninstallButton.setVisibility(View.VISIBLE);
uninstallButton.setOnClickListener(v -> performer.uninstallPackage());
}
return content;
}
/** Returns a {@link Dialog} to be shown to confirm that they want to disable a service. */
public static Dialog createDisableDialog(Context context,
AccessibilityServiceInfo info, DialogInterface.OnClickListener listener) {
final AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(context.getString(R.string.disable_service_title,
info.getResolveInfo().loadLabel(context.getPackageManager())))
.setMessage(context.getString(R.string.disable_service_message,
context.getString(R.string.accessibility_dialog_button_stop),
getServiceName(context, info)))
.setCancelable(true)
.setPositiveButton(R.string.accessibility_dialog_button_stop, listener)
.setNegativeButton(R.string.accessibility_dialog_button_cancel, listener)
.create();
return dialog;
}
// Get the service name and bidi wrap it to protect from bidi side effects.
private static CharSequence getServiceName(Context context, AccessibilityServiceInfo info) {
final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
final CharSequence label =
info.getResolveInfo().loadLabel(context.getPackageManager());
return BidiFormatter.getInstance(locale).unicodeWrap(label);
}
}