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

Commit 3960d7ae authored by Peter_Liang's avatar Peter_Liang
Browse files

Multiple shortcut menu for android R (2/n).

1. Update AccessibilityButtonChooserActivity:
  - Refactor the design
  - Add shortcut type checker and the corresponding UI
2. Add new icons

Bug: 142528666
Test: maunal
Change-Id: Ia58742f094535d42b748f56dfef84e5e06f2b0aa
parent 9e87abdc
Loading
Loading
Loading
Loading
+322 −49
Original line number Diff line number Diff line
@@ -15,14 +15,20 @@
 */
package com.android.internal.app;

import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
import static android.view.accessibility.AccessibilityManager.ShortcutType;

import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.LayoutInflater;
@@ -30,12 +36,17 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.accessibility.AccessibilityManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView;

import com.android.internal.R;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -48,11 +59,50 @@ public class AccessibilityButtonChooserActivity extends Activity {
    private static final String MAGNIFICATION_COMPONENT_ID =
            "com.android.server.accessibility.MagnificationController";

    private AccessibilityButtonTarget mMagnificationTarget = null;
    private int mShortcutType;
    private List<AccessibilityButtonTarget> mTargets = new ArrayList<>();
    private List<AccessibilityButtonTarget> mReadyToBeDisabledTargets = new ArrayList<>();
    private AlertDialog mAlertDialog;
    private TargetAdapter mTargetAdapter;

    private List<AccessibilityButtonTarget> mTargets = null;
    /**
     * Annotation for different accessibilityService fragment UI type.
     *
     * {@code LEGACY} for displaying appearance aligned with sdk version Q accessibility service
     * page, but only hardware shortcut allowed and under service in version Q or early.
     * {@code INVISIBLE} for displaying appearance without switch bar.
     * {@code INTUITIVE} for displaying appearance with version R accessibility design.
     * {@code BOUNCE} for displaying appearance with pop-up action.
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            AccessibilityServiceFragmentType.LEGACY,
            AccessibilityServiceFragmentType.INVISIBLE,
            AccessibilityServiceFragmentType.INTUITIVE,
            AccessibilityServiceFragmentType.BOUNCE,
    })
    public @interface AccessibilityServiceFragmentType {
        int LEGACY = 0;
        int INVISIBLE = 1;
        int INTUITIVE = 2;
        int BOUNCE = 3;
    }

    private AlertDialog mAlertDialog;
    /**
     * Annotation for different shortcut menu mode.
     *
     * {@code LAUNCH} for clicking list item to trigger the service callback.
     * {@code EDIT} for clicking list item and save button to disable the service.
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            ShortcutMenuMode.LAUNCH,
            ShortcutMenuMode.EDIT,
    })
    public @interface ShortcutMenuMode {
        int LAUNCH = 0;
        int EDIT = 1;
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -63,25 +113,20 @@ public class AccessibilityButtonChooserActivity extends Activity {
            requestWindowFeature(Window.FEATURE_NO_TITLE);
        }

        // TODO(b/146815874): Will replace it with white list services
        mMagnificationTarget = new AccessibilityButtonTarget(this, MAGNIFICATION_COMPONENT_ID,
                R.string.accessibility_magnification_chooser_text,
                R.drawable.ic_accessibility_magnification);

        // TODO(b/146815544): Will use shortcut type or button type to get the corresponding
        //  services
        mTargets = getServiceAccessibilityButtonTargets(this);
        if (Settings.Secure.getInt(getContentResolver(),
                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1) {
            mTargets.add(mMagnificationTarget);
        }
        mShortcutType = getIntent().getIntExtra(AccessibilityManager.EXTRA_SHORTCUT_TYPE,
                ACCESSIBILITY_BUTTON);
        mTargets.addAll(getServiceTargets(this, mShortcutType));

        // TODO(b/146815548): Will add title to separate which one type
        mTargetAdapter = new TargetAdapter(mTargets);
        mAlertDialog = new AlertDialog.Builder(this)
                .setAdapter(new TargetAdapter(),
                        (dialog, position) -> onTargetSelected(mTargets.get(position)))
                .setAdapter(mTargetAdapter, /* listener= */ null)
                .setPositiveButton(
                        getString(R.string.edit_accessibility_shortcut_menu_button),
                        /* listener= */ null)
                .setOnDismissListener(dialog -> finish())
                .create();
        mAlertDialog.setOnShowListener(dialog -> updateDialogListeners());
        mAlertDialog.show();
    }

@@ -91,41 +136,95 @@ public class AccessibilityButtonChooserActivity extends Activity {
        super.onDestroy();
    }

    private static List<AccessibilityButtonTarget> getServiceAccessibilityButtonTargets(
            @NonNull Context context) {
        AccessibilityManager ams = (AccessibilityManager) context.getSystemService(
    /**
     * Gets the corresponding fragment type of a given accessibility service.
     *
     * @param accessibilityServiceInfo The accessibilityService's info.
     * @return int from {@link AccessibilityServiceFragmentType}.
     */
    private static @AccessibilityServiceFragmentType int getAccessibilityServiceFragmentType(
            AccessibilityServiceInfo accessibilityServiceInfo) {
        final int targetSdk = accessibilityServiceInfo.getResolveInfo()
                .serviceInfo.applicationInfo.targetSdkVersion;
        final boolean requestA11yButton = (accessibilityServiceInfo.flags
                & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;

        if (targetSdk <= Build.VERSION_CODES.Q) {
            return AccessibilityServiceFragmentType.LEGACY;
        }
        return requestA11yButton
                ? AccessibilityServiceFragmentType.INVISIBLE
                : AccessibilityServiceFragmentType.INTUITIVE;
    }

    private static List<AccessibilityButtonTarget> getServiceTargets(@NonNull Context context,
            @ShortcutType int shortcutType) {
        final AccessibilityManager ams = (AccessibilityManager) context.getSystemService(
                Context.ACCESSIBILITY_SERVICE);
        List<AccessibilityServiceInfo> services = ams.getEnabledAccessibilityServiceList(
                AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
        if (services == null) {
        final List<AccessibilityServiceInfo> installedServices =
                ams.getInstalledAccessibilityServiceList();
        if (installedServices == null) {
            return Collections.emptyList();
        }

        ArrayList<AccessibilityButtonTarget> targets = new ArrayList<>(services.size());
        for (AccessibilityServiceInfo info : services) {
        final List<AccessibilityButtonTarget> targets = new ArrayList<>(installedServices.size());
        for (AccessibilityServiceInfo info : installedServices) {
            if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) {
                targets.add(new AccessibilityButtonTarget(context, info));
            }
        }

        final List<String> requiredTargets = ams.getAccessibilityShortcutTargets(shortcutType);
        targets.removeIf(target -> !requiredTargets.contains(target.getId()));

        // TODO(b/146815874): Will replace it with white list services.
        if (Settings.Secure.getInt(context.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1) {
            final AccessibilityButtonTarget magnificationTarget = new AccessibilityButtonTarget(
                    context,
                    MAGNIFICATION_COMPONENT_ID,
                    R.string.accessibility_magnification_chooser_text,
                    R.drawable.ic_accessibility_magnification,
                    AccessibilityServiceFragmentType.INTUITIVE);
            targets.add(magnificationTarget);
        }

        return targets;
    }

    private void onTargetSelected(AccessibilityButtonTarget target) {
        Settings.Secure.putString(getContentResolver(),
                Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT, target.getId());
        finish();
    private static class ViewHolder {
        ImageView mIconView;
        TextView mLabelView;
        FrameLayout mItemContainer;
        ImageView mViewItem;
        Switch mSwitchItem;
    }

    private static class TargetAdapter extends BaseAdapter {
        @ShortcutMenuMode
        private int mShortcutMenuMode = ShortcutMenuMode.LAUNCH;
        private List<AccessibilityButtonTarget> mButtonTargets;

        TargetAdapter(List<AccessibilityButtonTarget> targets) {
            this.mButtonTargets = targets;
        }

        void setShortcutMenuMode(int shortcutMenuMode) {
            mShortcutMenuMode = shortcutMenuMode;
        }

        int getShortcutMenuMode() {
            return mShortcutMenuMode;
        }

    private class TargetAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return mTargets.size();
            return mButtonTargets.size();
        }

        @Override
        public Object getItem(int position) {
            return null;
            return mButtonTargets.get(position);
        }

        @Override
@@ -135,36 +234,110 @@ public class AccessibilityButtonChooserActivity extends Activity {

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            LayoutInflater inflater = AccessibilityButtonChooserActivity.this.getLayoutInflater();
            View root = inflater.inflate(R.layout.accessibility_button_chooser_item, parent, false);
            final AccessibilityButtonTarget target = mTargets.get(position);
            ImageView iconView = root.findViewById(R.id.accessibility_button_target_icon);
            TextView labelView = root.findViewById(R.id.accessibility_button_target_label);
            iconView.setImageDrawable(target.getDrawable());
            labelView.setText(target.getLabel());
            final Context context = parent.getContext();
            ViewHolder holder;
            if (convertView == null) {
                convertView = LayoutInflater.from(context).inflate(
                        R.layout.accessibility_button_chooser_item, parent, /* attachToRoot= */
                        false);
                holder = new ViewHolder();
                holder.mIconView = convertView.findViewById(R.id.accessibility_button_target_icon);
                holder.mLabelView = convertView.findViewById(
                        R.id.accessibility_button_target_label);
                holder.mItemContainer = convertView.findViewById(
                        R.id.accessibility_button_target_item_container);
                holder.mViewItem = convertView.findViewById(
                        R.id.accessibility_button_target_view_item);
                holder.mSwitchItem = convertView.findViewById(
                        R.id.accessibility_button_target_switch_item);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            final AccessibilityButtonTarget target = mButtonTargets.get(position);
            holder.mIconView.setImageDrawable(target.getDrawable());
            holder.mLabelView.setText(target.getLabel());

            // TODO(b/146815874): Need to get every service status to update UI
            return root;
            updateActionItem(context, holder, target);

            return convertView;
        }

        private void updateActionItem(@NonNull Context context,
                @NonNull ViewHolder holder, AccessibilityButtonTarget target) {

            switch (target.getFragmentType()) {
                case AccessibilityServiceFragmentType.LEGACY:
                case AccessibilityServiceFragmentType.INVISIBLE:
                    updateLegacyOrInvisibleActionItemVisibility(context, holder);
                    break;
                case AccessibilityServiceFragmentType.INTUITIVE:
                    updateIntuitiveActionItemVisibility(context, holder, target);
                    break;
                case AccessibilityServiceFragmentType.BOUNCE:
                    updateBounceActionItemVisibility(context, holder);
                    break;
                default:
                    throw new IllegalStateException("Unexpected fragment type");
            }
        }

        private void updateLegacyOrInvisibleActionItemVisibility(@NonNull Context context,
                @NonNull ViewHolder holder) {
            final boolean isEditMenuMode = mShortcutMenuMode == ShortcutMenuMode.EDIT;

            holder.mViewItem.setImageDrawable(context.getDrawable(R.drawable.ic_delete_item));
            holder.mViewItem.setVisibility(View.VISIBLE);
            holder.mSwitchItem.setVisibility(View.GONE);
            holder.mItemContainer.setVisibility(isEditMenuMode ? View.VISIBLE : View.GONE);
        }

        private void updateIntuitiveActionItemVisibility(@NonNull Context context,
                @NonNull ViewHolder holder, AccessibilityButtonTarget target) {
            final boolean isEditMenuMode = mShortcutMenuMode == ShortcutMenuMode.EDIT;

            holder.mViewItem.setImageDrawable(context.getDrawable(R.drawable.ic_delete_item));
            holder.mViewItem.setVisibility(isEditMenuMode ? View.VISIBLE : View.GONE);
            holder.mSwitchItem.setVisibility(isEditMenuMode ? View.GONE : View.VISIBLE);
            holder.mSwitchItem.setChecked(!isEditMenuMode && isServiceEnabled(context, target));
            holder.mItemContainer.setVisibility(View.VISIBLE);
        }

        private void updateBounceActionItemVisibility(@NonNull Context context,
                @NonNull ViewHolder holder) {
            final boolean isEditMenuMode = mShortcutMenuMode == ShortcutMenuMode.EDIT;

            holder.mViewItem.setImageDrawable(
                    isEditMenuMode ? context.getDrawable(R.drawable.ic_delete_item)
                            : context.getDrawable(R.drawable.ic_open_in_new));
            holder.mViewItem.setVisibility(isEditMenuMode ? View.VISIBLE : View.GONE);
            holder.mSwitchItem.setVisibility(View.GONE);
            holder.mItemContainer.setVisibility(View.VISIBLE);
        }
    }

    private static class AccessibilityButtonTarget {
        public String mId;
        public CharSequence mLabel;
        public Drawable mDrawable;
        // TODO(b/146815874): Will add fragment type and related functions
        public AccessibilityButtonTarget(@NonNull Context context,
        private String mId;
        private CharSequence mLabel;
        private Drawable mDrawable;
        @AccessibilityServiceFragmentType
        private int mFragmentType;

        AccessibilityButtonTarget(@NonNull Context context,
                @NonNull AccessibilityServiceInfo serviceInfo) {
            this.mId = serviceInfo.getComponentName().flattenToString();
            this.mLabel = serviceInfo.getResolveInfo().loadLabel(context.getPackageManager());
            this.mDrawable = serviceInfo.getResolveInfo().loadIcon(context.getPackageManager());
            this.mFragmentType = getAccessibilityServiceFragmentType(serviceInfo);
        }

        public AccessibilityButtonTarget(Context context, @NonNull String id, int labelResId,
                int iconRes) {
        AccessibilityButtonTarget(Context context, @NonNull String id, int labelResId,
                int iconRes, @AccessibilityServiceFragmentType int fragmentType) {
            this.mId = id;
            this.mLabel = context.getText(labelResId);
            this.mDrawable = context.getDrawable(iconRes);
            this.mFragmentType = fragmentType;
        }

        public String getId() {
@@ -178,5 +351,105 @@ public class AccessibilityButtonChooserActivity extends Activity {
        public Drawable getDrawable() {
            return mDrawable;
        }

        public int getFragmentType() {
            return mFragmentType;
        }
    }

    private static boolean isServiceEnabled(@NonNull Context context,
            AccessibilityButtonTarget target) {
        final AccessibilityManager ams = (AccessibilityManager) context.getSystemService(
                Context.ACCESSIBILITY_SERVICE);
        final List<AccessibilityServiceInfo> enabledServices =
                ams.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);

        for (AccessibilityServiceInfo info : enabledServices) {
            final String id = info.getComponentName().flattenToString();
            if (id.equals(target.getId())) {
                return true;
            }
        }

        return false;
    }

    private void onTargetSelected(AdapterView<?> parent, View view, int position, long id) {
        Settings.Secure.putString(getContentResolver(),
                Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT,
                mTargets.get(position).getId());
        // TODO(b/146969684): notify accessibility button clicked.
        mAlertDialog.dismiss();
    }

    private void onTargetDeleted(AdapterView<?> parent, View view, int position, long id) {
        // TODO(b/147027236): Will discuss with UX designer what UX behavior about deleting item
        //  is good for user.
        mReadyToBeDisabledTargets.add(mTargets.get(position));
        mTargets.remove(position);
        mTargetAdapter.notifyDataSetChanged();
    }

    private void onCancelButtonClicked() {
        resetAndUpdateTargets();

        mTargetAdapter.setShortcutMenuMode(ShortcutMenuMode.LAUNCH);
        mTargetAdapter.notifyDataSetChanged();

        mAlertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.GONE);
        mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(
                getString(R.string.edit_accessibility_shortcut_menu_button));

        updateDialogListeners();
    }

    private void onEditButtonClicked() {
        mTargetAdapter.setShortcutMenuMode(ShortcutMenuMode.EDIT);
        mTargetAdapter.notifyDataSetChanged();

        mAlertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText(
                getString(R.string.cancel_accessibility_shortcut_menu_button));
        mAlertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.VISIBLE);
        mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(
                getString(R.string.save_accessibility_shortcut_menu_button));

        updateDialogListeners();
    }

    private void onSaveButtonClicked() {
        disableTargets();
        resetAndUpdateTargets();

        mTargetAdapter.setShortcutMenuMode(ShortcutMenuMode.LAUNCH);
        mTargetAdapter.notifyDataSetChanged();

        mAlertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.GONE);
        mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setText(
                getString(R.string.edit_accessibility_shortcut_menu_button));

        updateDialogListeners();
    }

    private void updateDialogListeners() {
        final boolean isEditMenuMode =
                mTargetAdapter.getShortcutMenuMode() == ShortcutMenuMode.EDIT;

        mAlertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(
                view -> onCancelButtonClicked());
        mAlertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(
                isEditMenuMode ? view -> onSaveButtonClicked() : view -> onEditButtonClicked());
        mAlertDialog.getListView().setOnItemClickListener(
                isEditMenuMode ? this::onTargetDeleted : this::onTargetSelected);
    }

    private void disableTargets() {
        for (AccessibilityButtonTarget service : mReadyToBeDisabledTargets) {
            // TODO(b/146967898): disable services.
        }
    }

    private void resetAndUpdateTargets() {
        mTargets.clear();
        mTargets.addAll(getServiceTargets(this, mShortcutType));
    }
}
+26 −0
Original line number Diff line number Diff line
<!--
    Copyright (C) 2020 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.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24"
    android:tint="?attr/colorControlNormal">
  <path
      android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"
      android:fillColor="#FFFFFF"/>
</vector>
+26 −0
Original line number Diff line number Diff line
<!--
    Copyright (C) 2020 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.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24"
    android:tint="?attr/colorControlNormal">
  <path
      android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"
      android:fillColor="#FFFFFF"/>
</vector>
+24 −3
Original line number Diff line number Diff line
@@ -21,10 +21,10 @@
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:paddingBottom="12dp"
    android:paddingEnd="16dp"
    android:paddingStart="16dp"
    android:paddingTop="12dp">
    android:paddingEnd="16dp"
    android:paddingTop="12dp"
    android:paddingBottom="12dp">

    <ImageView
        android:id="@+id/accessibility_button_target_icon"
@@ -39,5 +39,26 @@
        android:layout_marginStart="8dp"
        android:layout_weight="1"
        android:textColor="?attr/textColorPrimary"/>

    <FrameLayout
        android:id="@+id/accessibility_button_target_item_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:minWidth="56dp">

        <ImageView
            android:id="@+id/accessibility_button_target_view_item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"/>

        <Switch android:id="@+id/accessibility_button_target_switch_item"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:background="@null"
                android:clickable="false"
                android:focusable="false"/>
    </FrameLayout>
</LinearLayout>
+12 −0
Original line number Diff line number Diff line
@@ -4354,6 +4354,18 @@
        You can change the feature in Settings > Accessibility.
    </string>

    <!-- Text in button that edit the accessibility shortcut menu. [CHAR LIMIT=100] -->
    <string name="accessibility_shortcut_menu_button">Empty</string>

    <!-- Text in button that edit the accessibility shortcut menu. [CHAR LIMIT=100] -->
    <string name="edit_accessibility_shortcut_menu_button">Edit</string>

    <!-- Text in button that save the accessibility shortcut menu changed status. [CHAR LIMIT=100] -->
    <string name="save_accessibility_shortcut_menu_button">Save</string>

    <!-- Text in button that cancel the accessibility shortcut menu changed status. [CHAR LIMIT=100] -->
    <string name="cancel_accessibility_shortcut_menu_button">Cancel</string>

    <!-- Text in button that turns off the accessibility shortcut -->
    <string name="disable_accessibility_shortcut">Turn off Shortcut</string>

Loading