Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fd3b9c05 authored by Daniel Norman's avatar Daniel Norman
Browse files

Tech debt cleanup: Consolidates A11yService warning dialog.

Both frameworks/base and the Settings app define almost-identical copies
of a warning dialog shown when enabling an accessibility service.
The frameworks/base version was used for contexts outside of Settings
(e.g. while editing the volume key shortcut after triggering it with
2+ features already enabled) while the Settings version was used in
the Settings app.

This change replaces the frameworks/base implementation with the more
full-featured Settings implementation.
The other change in this topic in packages/apps/Settings changes
Settings to use this consolidated single version.

Feature flag:
`adb shell device_config override accessibility android.view.accessibility.deduplicate_accessibility_warning_dialog true`

Bug: 303511250
Test: Use this dialog in Accessibility Settings
Test: Use this dialog in the a11y volume-key shortcut editor
Test: atest AccessibilityServiceWarningTest
Test: atest AccessibilityShortcutChooserActivityTest
Change-Id: I6e1acf7cecfaa0dce422f7dfac3f270b9902dfe9
parent e6f0e3ff
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -16,6 +16,13 @@ flag {
    bug: "303871725"
}

flag {
    name: "deduplicate_accessibility_warning_dialog"
    namespace: "accessibility"
    description: "Removes duplicate definition of the accessibility warning dialog."
    bug: "303511250"
}

flag {
    namespace: "accessibility"
    name: "force_invert_color"
+7 −0
Original line number Diff line number Diff line
@@ -34,6 +34,8 @@ import com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuM
 */
class AccessibilityServiceTarget extends AccessibilityTarget {

    private final AccessibilityServiceInfo mAccessibilityServiceInfo;

    AccessibilityServiceTarget(Context context, @ShortcutType int shortcutType,
            @AccessibilityFragmentType int fragmentType,
            @NonNull AccessibilityServiceInfo serviceInfo) {
@@ -47,6 +49,7 @@ class AccessibilityServiceTarget extends AccessibilityTarget {
                serviceInfo.getResolveInfo().loadLabel(context.getPackageManager()),
                serviceInfo.getResolveInfo().loadIcon(context.getPackageManager()),
                convertToKey(convertToUserType(shortcutType)));
        mAccessibilityServiceInfo = serviceInfo;
    }

    @Override
@@ -64,4 +67,8 @@ class AccessibilityServiceTarget extends AccessibilityTarget {
        holder.mLabelView.setEnabled(enabled);
        holder.mStatusView.setEnabled(enabled);
    }

    public AccessibilityServiceInfo getAccessibilityServiceInfo() {
        return mAccessibilityServiceInfo;
    }
}
+139 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.internal.accessibility.dialog;

import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.drawable.Drawable;
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 com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;

import java.util.Locale;

/**
 * Utility class for creating the dialog that asks the user for explicit permission
 * before an accessibility service is enabled.
 */
public class AccessibilityServiceWarning {

    /**
     * Returns an {@link AlertDialog} to be shown to confirm that the user
     * wants to enable an {@link android.accessibilityservice.AccessibilityService}.
     */
    public static AlertDialog createAccessibilityServiceWarningDialog(@NonNull Context context,
            @NonNull AccessibilityServiceInfo info,
            @NonNull View.OnClickListener allowListener,
            @NonNull View.OnClickListener denyListener,
            @NonNull View.OnClickListener uninstallListener) {
        final AlertDialog ad = new AlertDialog.Builder(context)
                .setView(createAccessibilityServiceWarningDialogContentView(
                                context, info, allowListener, denyListener, uninstallListener))
                .setCancelable(true)
                .create();
        Window window = ad.getWindow();
        WindowManager.LayoutParams params = window.getAttributes();
        params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
        params.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
        window.setAttributes(params);
        return ad;
    }

    @VisibleForTesting
    public static View createAccessibilityServiceWarningDialogContentView(Context context,
            AccessibilityServiceInfo info,
            View.OnClickListener allowListener,
            View.OnClickListener denyListener,
            View.OnClickListener uninstallListener) {
        final LayoutInflater inflater = context.getSystemService(LayoutInflater.class);
        final View content = inflater.inflate(R.layout.accessibility_service_warning, null);

        final Drawable icon;
        if (info.getResolveInfo().getIconResource() == 0) {
            icon = context.getDrawable(R.drawable.ic_accessibility_generic);
        } else {
            icon = info.getResolveInfo().loadIcon(context.getPackageManager());
        }
        final ImageView permissionDialogIcon = content.findViewById(
                R.id.accessibility_permissionDialog_icon);
        permissionDialogIcon.setImageDrawable(icon);

        final TextView permissionDialogTitle = content.findViewById(
                R.id.accessibility_permissionDialog_title);
        permissionDialogTitle.setText(context.getString(R.string.accessibility_enable_service_title,
                getServiceName(context, info)));

        final Button permissionAllowButton = content.findViewById(
                R.id.accessibility_permission_enable_allow_button);
        final Button permissionDenyButton = content.findViewById(
                R.id.accessibility_permission_enable_deny_button);
        permissionAllowButton.setOnClickListener(allowListener);
        permissionAllowButton.setOnTouchListener(getTouchConsumingListener());
        permissionDenyButton.setOnClickListener(denyListener);

        final Button uninstallButton = content.findViewById(
                R.id.accessibility_permission_enable_uninstall_button);
        // Show an uninstall button to help users quickly remove non-preinstalled apps.
        if (!info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp()) {
            uninstallButton.setVisibility(View.VISIBLE);
            uninstallButton.setOnClickListener(uninstallListener);
        }
        return content;
    }

    @VisibleForTesting
    @SuppressLint("ClickableViewAccessibility") // Touches are intentionally consumed
    public static View.OnTouchListener getTouchConsumingListener() {
        return (view, 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(view.getContext(),
                            R.string.accessibility_dialog_touch_filtered_warning,
                            Toast.LENGTH_SHORT).show();
                }
                return true;
            }
            return false;
        };
    }

    // 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);
    }
}
+36 −12
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import static com.android.internal.accessibility.util.AccessibilityUtils.isUserS
import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
@@ -56,7 +57,7 @@ public class AccessibilityShortcutChooserActivity extends Activity {
            "accessibility_shortcut_menu_mode";
    private final List<AccessibilityTarget> mTargets = new ArrayList<>();
    private AlertDialog mMenuDialog;
    private AlertDialog mPermissionDialog;
    private Dialog mPermissionDialog;
    private ShortcutTargetAdapter mTargetAdapter;

    @Override
@@ -123,7 +124,7 @@ public class AccessibilityShortcutChooserActivity extends Activity {

            if (target instanceof AccessibilityServiceTarget) {
                showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
                        mTargetAdapter);
                        position, mTargetAdapter);
                return;
            }
        }
@@ -149,11 +150,33 @@ public class AccessibilityShortcutChooserActivity extends Activity {
    }

    private void showPermissionDialogIfNeeded(Context context,
            AccessibilityServiceTarget serviceTarget, ShortcutTargetAdapter targetAdapter) {
            AccessibilityServiceTarget serviceTarget, int position,
            ShortcutTargetAdapter targetAdapter) {
        if (mPermissionDialog != null) {
            return;
        }

        if (Flags.deduplicateAccessibilityWarningDialog()) {
            mPermissionDialog = AccessibilityServiceWarning
                    .createAccessibilityServiceWarningDialog(context,
                            serviceTarget.getAccessibilityServiceInfo(),
                            v -> {
                                serviceTarget.onCheckedChanged(true);
                                targetAdapter.notifyDataSetChanged();
                                mPermissionDialog.dismiss();
                            }, v -> {
                                serviceTarget.onCheckedChanged(false);
                                mPermissionDialog.dismiss();
                            },
                            v -> {
                                mTargets.remove(position);
                                context.getPackageManager().getPackageInstaller().uninstall(
                                        serviceTarget.getComponentName().getPackageName(), null);
                                targetAdapter.notifyDataSetChanged();
                                mPermissionDialog.dismiss();
                            });
            mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
        } else {
            mPermissionDialog = new AlertDialog.Builder(context)
                    .setView(createEnableDialogContentView(context, serviceTarget,
                            v -> {
@@ -163,6 +186,7 @@ public class AccessibilityShortcutChooserActivity extends Activity {
                            v -> mPermissionDialog.dismiss()))
                    .setOnDismissListener(dialog -> mPermissionDialog = null)
                    .create();
        }
        mPermissionDialog.show();
    }

+4 −0
Original line number Diff line number Diff line
@@ -296,6 +296,10 @@ public final class AccessibilityTargetHelper {
        }
    }

    /**
     * @deprecated Use {@link AccessibilityServiceWarning}.
     */
    @Deprecated
    static View createEnableDialogContentView(Context context,
            AccessibilityServiceTarget target, View.OnClickListener allowListener,
            View.OnClickListener denyListener) {
Loading