Add TileAdapterDelegate
Use an AccessibilityDelegate for the actions in QSCustomizer. Adding or
removing a tile can now be done with the click action, whereas moving or
adding to a particular position require a context action.
This removes the old custom dialog and improves the overall
accessibility.
Test: manual
Test: atest TileAdapterDelegate
Bug: 168039987
Bug: 140366995
Change-Id: Ib5b19aeebb54c46573555563c3f39bd922b68896
Merged-In: Ib5b19aeebb54c46573555563c3f39bd922b68896
(cherry picked from commit ac22e5bf361291d67cf1e318ae3ef6249e5a4ff9)
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index a56f6f5..b8e8db5 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -177,6 +177,9 @@
<item type="id" name="accessibility_action_controls_move_before" />
<item type="id" name="accessibility_action_controls_move_after" />
+ <item type="id" name="accessibility_action_qs_move_to_position" />
+ <item type="id" name="accessibility_action_qs_add_to_position" />
+
<!-- Accessibility actions for PIP -->
<item type="id" name="action_pip_resize" />
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 38501eb..77ce39f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2267,23 +2267,26 @@
<!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] -->
<string name="accessibility_action_divider_bottom_full">Bottom full screen</string>
- <!-- Accessibility description of a QS tile while editing positions [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_tile_label">Position <xliff:g id="position" example="2">%1$d</xliff:g>, <xliff:g id="tile_name" example="Wi-Fi">%2$s</xliff:g>. Double tap to edit.</string>
+ <!-- Accessibility description of action to remove QS tile on click. It will read as "Double-tap to remove tile" in screen readers [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_remove_tile_action">remove tile</string>
- <!-- Accessibility description of a QS tile while editing positions [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_add_tile_label"><xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g>. Double tap to add.</string>
+ <!-- Accessibility action of action to add QS tile to end. It will read as "Double-tap to add tile to end" in screen readers [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_add_action">add tile to end</string>
- <!-- Accessibility description of option to move QS tile [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_move_tile">Move <xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g></string>
+ <!-- Accessibility action for context menu to move QS tile [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_start_move">Move tile</string>
- <!-- Accessibility description of option to remove QS tile [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_remove_tile">Remove <xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g></string>
+ <!-- Accessibility action for context menu to add QS tile [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_start_add">Add tile</string>
- <!-- Accessibility action when QS tile is to be added [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_tile_add">Add <xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g> to position <xliff:g id="position" example="5">%2$d</xliff:g></string>
+ <!-- Accessibility description when QS tile is to be moved, indicating the destination position [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_move_to_position">Move to <xliff:g id="position" example="5">%1$d</xliff:g></string>
- <!-- Accessibility action when QS tile is to be moved [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_tile_move">Move <xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g> to position <xliff:g id="position" example="5">%2$d</xliff:g></string>
+ <!-- Accessibility description when QS tile is to be added, indicating the destination position [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_add_to_position">Add to position <xliff:g id="position" example="5">%1$d</xliff:g></string>
+
+ <!-- Accessibility description indicating the currently selected tile's position. Only used for tiles that are currently in use [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_position">Position <xliff:g id="position" example="5">%1$d</xliff:g></string>
<!-- Accessibility label for window when QS editing is happening [CHAR LIMIT=NONE] -->
<string name="accessibility_desc_quick_settings_edit">Quick settings editor.</string>
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index e738cec..bffeb3e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -14,11 +14,8 @@
package com.android.systemui.qs.customize;
-import android.app.AlertDialog;
-import android.app.AlertDialog.Builder;
import android.content.ComponentName;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
@@ -28,10 +25,11 @@
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
import androidx.recyclerview.widget.ItemTouchHelper;
@@ -49,7 +47,6 @@
import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.tileimpl.QSIconViewImpl;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
import java.util.ArrayList;
import java.util.List;
@@ -78,10 +75,10 @@
private final List<TileInfo> mTiles = new ArrayList<>();
private final ItemTouchHelper mItemTouchHelper;
private final ItemDecoration mDecoration;
- private final AccessibilityManager mAccessibilityManager;
private final int mMinNumTiles;
private int mEditIndex;
private int mTileDividerIndex;
+ private int mFocusIndex;
private boolean mNeedsFocus;
private List<String> mCurrentSpecs;
private List<TileInfo> mOtherTiles;
@@ -90,17 +87,28 @@
private Holder mCurrentDrag;
private int mAccessibilityAction = ACTION_NONE;
private int mAccessibilityFromIndex;
- private CharSequence mAccessibilityFromLabel;
private QSTileHost mHost;
private final UiEventLogger mUiEventLogger;
+ private final AccessibilityDelegateCompat mAccessibilityDelegate;
+ private RecyclerView mRecyclerView;
public TileAdapter(Context context, UiEventLogger uiEventLogger) {
mContext = context;
mUiEventLogger = uiEventLogger;
- mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mItemTouchHelper = new ItemTouchHelper(mCallbacks);
mDecoration = new TileItemDecoration(context);
mMinNumTiles = context.getResources().getInteger(R.integer.quick_settings_min_num_tiles);
+ mAccessibilityDelegate = new TileAdapterDelegate();
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = null;
}
public void setHost(QSTileHost host) {
@@ -130,7 +138,6 @@
// Remove blank tile from last spot
mTiles.remove(--mEditIndex);
// Update the tile divider position
- mTileDividerIndex--;
notifyDataSetChanged();
}
mAccessibilityAction = ACTION_NONE;
@@ -241,14 +248,12 @@
}
private void setSelectableForHeaders(View view) {
- if (mAccessibilityManager.isTouchExplorationEnabled()) {
- final boolean selectable = mAccessibilityAction == ACTION_NONE;
- view.setFocusable(selectable);
- view.setImportantForAccessibility(selectable
- ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
- : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- view.setFocusableInTouchMode(selectable);
- }
+ final boolean selectable = mAccessibilityAction == ACTION_NONE;
+ view.setFocusable(selectable);
+ view.setImportantForAccessibility(selectable
+ ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ view.setFocusableInTouchMode(selectable);
}
@Override
@@ -285,12 +290,11 @@
holder.mTileView.setVisibility(View.VISIBLE);
holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
holder.mTileView.setContentDescription(mContext.getString(
- R.string.accessibility_qs_edit_tile_add, mAccessibilityFromLabel,
- position));
+ R.string.accessibility_qs_edit_tile_add_to_position, position));
holder.mTileView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- selectPosition(holder.getAdapterPosition(), v);
+ selectPosition(holder.getLayoutPosition());
}
});
focusOnHolder(holder);
@@ -299,54 +303,49 @@
TileInfo info = mTiles.get(position);
- if (position > mEditIndex) {
+ final boolean selectable = 0 < position && position < mEditIndex;
+ if (selectable && mAccessibilityAction == ACTION_ADD) {
info.state.contentDescription = mContext.getString(
- R.string.accessibility_qs_edit_add_tile_label, info.state.label);
- } else if (mAccessibilityAction == ACTION_ADD) {
+ R.string.accessibility_qs_edit_tile_add_to_position, position);
+ } else if (selectable && mAccessibilityAction == ACTION_MOVE) {
info.state.contentDescription = mContext.getString(
- R.string.accessibility_qs_edit_tile_add, mAccessibilityFromLabel, position);
- } else if (mAccessibilityAction == ACTION_MOVE) {
- info.state.contentDescription = mContext.getString(
- R.string.accessibility_qs_edit_tile_move, mAccessibilityFromLabel, position);
+ R.string.accessibility_qs_edit_tile_move_to_position, position);
} else {
- info.state.contentDescription = mContext.getString(
- R.string.accessibility_qs_edit_tile_label, position, info.state.label);
+ info.state.contentDescription = info.state.label;
}
+ info.state.expandedAccessibilityClassName = "";
+
holder.mTileView.handleStateChanged(info.state);
holder.mTileView.setShowAppLabel(position > mEditIndex && !info.isSystem);
+ holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ holder.mTileView.setClickable(true);
+ holder.mTileView.setOnClickListener(null);
+ holder.mTileView.setFocusable(true);
+ holder.mTileView.setFocusableInTouchMode(true);
- if (mAccessibilityManager.isTouchExplorationEnabled()) {
- final boolean selectable = mAccessibilityAction == ACTION_NONE || position < mEditIndex;
+ if (mAccessibilityAction != ACTION_NONE) {
holder.mTileView.setClickable(selectable);
holder.mTileView.setFocusable(selectable);
+ holder.mTileView.setFocusableInTouchMode(selectable);
holder.mTileView.setImportantForAccessibility(selectable
? View.IMPORTANT_FOR_ACCESSIBILITY_YES
: View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- holder.mTileView.setFocusableInTouchMode(selectable);
if (selectable) {
holder.mTileView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- int position = holder.getAdapterPosition();
+ int position = holder.getLayoutPosition();
if (position == RecyclerView.NO_POSITION) return;
if (mAccessibilityAction != ACTION_NONE) {
- selectPosition(position, v);
- } else {
- if (position < mEditIndex && canRemoveTiles()) {
- showAccessibilityDialog(position, v);
- } else if (position < mEditIndex && !canRemoveTiles()) {
- startAccessibleMove(position);
- } else {
- startAccessibleAdd(position);
- }
+ selectPosition(position);
}
}
});
- if (position == mAccessibilityFromIndex) {
- focusOnHolder(holder);
- }
}
}
+ if (position == mFocusIndex) {
+ focusOnHolder(holder);
+ }
}
private void focusOnHolder(Holder holder) {
@@ -360,9 +359,13 @@
int oldLeft, int oldTop, int oldRight, int oldBottom) {
holder.mTileView.removeOnLayoutChangeListener(this);
holder.mTileView.requestFocus();
+ if (mAccessibilityAction == ACTION_NONE) {
+ holder.mTileView.clearFocus();
+ }
}
});
mNeedsFocus = false;
+ mFocusIndex = RecyclerView.NO_POSITION;
}
}
@@ -370,72 +373,77 @@
return mCurrentSpecs.size() > mMinNumTiles;
}
- private void selectPosition(int position, View v) {
+ private void selectPosition(int position) {
if (mAccessibilityAction == ACTION_ADD) {
// Remove the placeholder.
mTiles.remove(mEditIndex--);
- notifyItemRemoved(mEditIndex);
}
mAccessibilityAction = ACTION_NONE;
- move(mAccessibilityFromIndex, position, v);
+ move(mAccessibilityFromIndex, position, false);
+ mFocusIndex = position;
+ mNeedsFocus = true;
notifyDataSetChanged();
}
- private void showAccessibilityDialog(final int position, final View v) {
- final TileInfo info = mTiles.get(position);
- CharSequence[] options = new CharSequence[] {
- mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label),
- mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label),
- };
- AlertDialog dialog = new Builder(mContext)
- .setItems(options, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- if (which == 0) {
- startAccessibleMove(position);
- } else {
- move(position, info.isSystem ? mEditIndex : mTileDividerIndex, v);
- notifyItemChanged(mTileDividerIndex);
- notifyDataSetChanged();
- }
- }
- }).setNegativeButton(android.R.string.cancel, null)
- .create();
- SystemUIDialog.setShowForAllUsers(dialog, true);
- SystemUIDialog.applyFlags(dialog);
- dialog.show();
- }
-
private void startAccessibleAdd(int position) {
mAccessibilityFromIndex = position;
- mAccessibilityFromLabel = mTiles.get(position).state.label;
mAccessibilityAction = ACTION_ADD;
// Add placeholder for last slot.
mTiles.add(mEditIndex++, null);
// Update the tile divider position
mTileDividerIndex++;
+ mFocusIndex = mEditIndex - 1;
mNeedsFocus = true;
+ if (mRecyclerView != null) {
+ mRecyclerView.post(() -> mRecyclerView.smoothScrollToPosition(mFocusIndex));
+ }
notifyDataSetChanged();
}
private void startAccessibleMove(int position) {
mAccessibilityFromIndex = position;
- mAccessibilityFromLabel = mTiles.get(position).state.label;
mAccessibilityAction = ACTION_MOVE;
+ mFocusIndex = position;
mNeedsFocus = true;
notifyDataSetChanged();
}
+ private boolean canRemoveFromPosition(int position) {
+ return canRemoveTiles() && isCurrentTile(position);
+ }
+
+ private boolean isCurrentTile(int position) {
+ return position < mEditIndex;
+ }
+
+ private boolean canAddFromPosition(int position) {
+ return position > mEditIndex;
+ }
+
+ private void addFromPosition(int position) {
+ if (!canAddFromPosition(position)) return;
+ move(position, mEditIndex);
+ }
+
+ private void removeFromPosition(int position) {
+ if (!canRemoveFromPosition(position)) return;
+ TileInfo info = mTiles.get(position);
+ move(position, info.isSystem ? mEditIndex : mTileDividerIndex);
+ }
+
public SpanSizeLookup getSizeLookup() {
return mSizeLookup;
}
- private boolean move(int from, int to, View v) {
+ private boolean move(int from, int to) {
+ return move(from, to, true);
+ }
+
+ private boolean move(int from, int to, boolean notify) {
if (to == from) {
return true;
}
- CharSequence fromLabel = mTiles.get(from).state.label;
- move(from, to, mTiles);
+ move(from, to, mTiles, notify);
updateDividerLocations();
if (to >= mEditIndex) {
mUiEventLogger.log(QSEditEvent.QS_EDIT_REMOVE, 0, strip(mTiles.get(to)));
@@ -477,9 +485,11 @@
return spec;
}
- private <T> void move(int from, int to, List<T> list) {
+ private <T> void move(int from, int to, List<T> list, boolean notify) {
list.add(to, list.remove(from));
- notifyItemMoved(from, to);
+ if (notify) {
+ notifyItemMoved(from, to);
+ }
}
public class Holder extends ViewHolder {
@@ -491,6 +501,8 @@
mTileView = (CustomizeTileView) ((FrameLayout) itemView).getChildAt(0);
mTileView.setBackground(null);
mTileView.getIcon().disableAnimation();
+ mTileView.setTag(this);
+ ViewCompat.setAccessibilityDelegate(mTileView, mAccessibilityDelegate);
}
}
@@ -527,6 +539,46 @@
.setDuration(DRAG_LENGTH)
.alpha(.6f);
}
+
+ boolean canRemove() {
+ return canRemoveFromPosition(getLayoutPosition());
+ }
+
+ boolean canAdd() {
+ return canAddFromPosition(getLayoutPosition());
+ }
+
+ void toggleState() {
+ if (canAdd()) {
+ add();
+ } else {
+ remove();
+ }
+ }
+
+ private void add() {
+ addFromPosition(getLayoutPosition());
+ }
+
+ private void remove() {
+ removeFromPosition(getLayoutPosition());
+ }
+
+ boolean isCurrentTile() {
+ return TileAdapter.this.isCurrentTile(getLayoutPosition());
+ }
+
+ void startAccessibleAdd() {
+ TileAdapter.this.startAccessibleAdd(getLayoutPosition());
+ }
+
+ void startAccessibleMove() {
+ TileAdapter.this.startAccessibleMove(getLayoutPosition());
+ }
+
+ boolean canTakeAccessibleAction() {
+ return mAccessibilityAction == ACTION_NONE;
+ }
}
private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
@@ -648,7 +700,7 @@
to == 0 || to == RecyclerView.NO_POSITION) {
return false;
}
- return move(from, to, target.itemView);
+ return move(from, to);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java
new file mode 100644
index 0000000..1e426ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.qs.customize;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * Accessibility delegate for {@link TileAdapter} views.
+ *
+ * This delegate will populate the accessibility info with the proper actions that can be taken for
+ * the different tiles:
+ * <ul>
+ * <li>Add to end if the tile is not a current tile (by double tap).</li>
+ * <li>Add to a given position (by context menu). This will let the user select a position.</li>
+ * <li>Remove, if the tile is a current tile (by double tap).</li>
+ * <li>Move to a given position (by context menu). This will let the user select a position.</li>
+ * </ul>
+ *
+ * This only handles generating the associated actions. The logic for selecting positions is handled
+ * by {@link TileAdapter}.
+ *
+ * In order for the delegate to work properly, the asociated {@link TileAdapter.Holder} should be
+ * passed along with the view using {@link View#setTag}.
+ */
+class TileAdapterDelegate extends AccessibilityDelegateCompat {
+
+ private static final int MOVE_TO_POSITION_ID = R.id.accessibility_action_qs_move_to_position;
+ private static final int ADD_TO_POSITION_ID = R.id.accessibility_action_qs_add_to_position;
+
+ private TileAdapter.Holder getHolder(View view) {
+ return (TileAdapter.Holder) view.getTag();
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ TileAdapter.Holder holder = getHolder(host);
+ info.setCollectionItemInfo(null);
+ info.setStateDescription("");
+ if (holder == null || !holder.canTakeAccessibleAction()) {
+ // If there's not a holder (not a regular Tile) or an action cannot be taken
+ // because we are in the middle of an accessibility action, don't create a special node.
+ return;
+ }
+
+ addClickAction(host, info, holder);
+ maybeAddActionAddToPosition(host, info, holder);
+ maybeAddActionMoveToPosition(host, info, holder);
+
+ if (holder.isCurrentTile()) {
+ info.setStateDescription(host.getContext().getString(
+ R.string.accessibility_qs_edit_position, holder.getLayoutPosition()));
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ TileAdapter.Holder holder = getHolder(host);
+
+ if (holder == null || !holder.canTakeAccessibleAction()) {
+ // If there's not a holder (not a regular Tile) or an action cannot be taken
+ // because we are in the middle of an accessibility action, perform the default action.
+ return super.performAccessibilityAction(host, action, args);
+ }
+ if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+ holder.toggleState();
+ return true;
+ } else if (action == MOVE_TO_POSITION_ID) {
+ holder.startAccessibleMove();
+ return true;
+ } else if (action == ADD_TO_POSITION_ID) {
+ holder.startAccessibleAdd();
+ return true;
+ } else {
+ return super.performAccessibilityAction(host, action, args);
+ }
+ }
+
+ private void addClickAction(
+ View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
+ String clickActionString;
+ if (holder.canAdd()) {
+ clickActionString = host.getContext().getString(
+ R.string.accessibility_qs_edit_tile_add_action);
+ } else if (holder.canRemove()) {
+ clickActionString = host.getContext().getString(
+ R.string.accessibility_qs_edit_remove_tile_action);
+ } else {
+ // Remove the default click action if tile can't either be added or removed (for example
+ // if there's the minimum number of tiles)
+ List<AccessibilityNodeInfoCompat.AccessibilityActionCompat> listOfActions =
+ info.getActionList(); // This is a copy
+ int numActions = listOfActions.size();
+ for (int i = 0; i < numActions; i++) {
+ if (listOfActions.get(i).getId() == AccessibilityNodeInfo.ACTION_CLICK) {
+ info.removeAction(listOfActions.get(i));
+ }
+ }
+ return;
+ }
+
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfo.ACTION_CLICK, clickActionString);
+ info.addAction(action);
+ }
+
+ private void maybeAddActionMoveToPosition(
+ View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
+ if (holder.isCurrentTile()) {
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(MOVE_TO_POSITION_ID,
+ host.getContext().getString(
+ R.string.accessibility_qs_edit_tile_start_move));
+ info.addAction(action);
+ }
+ }
+
+ private void maybeAddActionAddToPosition(
+ View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
+ if (holder.canAdd()) {
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(ADD_TO_POSITION_ID,
+ host.getContext().getString(
+ R.string.accessibility_qs_edit_tile_start_add));
+ info.addAction(action);
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
new file mode 100644
index 0000000..a5dead0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.qs.customize;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Bundle;
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class TileAdapterDelegateTest extends SysuiTestCase {
+
+ private static final int MOVE_TO_POSITION_ID = R.id.accessibility_action_qs_move_to_position;
+ private static final int ADD_TO_POSITION_ID = R.id.accessibility_action_qs_add_to_position;
+ private static final int POSITION_STRING_ID = R.string.accessibility_qs_edit_position;
+
+ @Mock
+ private TileAdapter.Holder mHolder;
+
+ private AccessibilityNodeInfoCompat mInfo;
+ private TileAdapterDelegate mDelegate;
+ private View mView;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mView = new View(mContext);
+ mDelegate = new TileAdapterDelegate();
+ mInfo = AccessibilityNodeInfoCompat.obtain();
+ }
+
+ @Test
+ public void testInfoNoSpecialActionsWhenNoHolder() {
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action : mInfo.getActionList()) {
+ if (action.getId() == MOVE_TO_POSITION_ID || action.getId() == ADD_TO_POSITION_ID
+ || action.getId() == AccessibilityNodeInfo.ACTION_CLICK) {
+ fail("It should not have special action " + action.getId());
+ }
+ }
+ }
+
+ @Test
+ public void testInfoNoSpecialActionsWhenCannotStartAccessibleAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(false);
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action : mInfo.getActionList()) {
+ if (action.getId() == MOVE_TO_POSITION_ID || action.getId() == ADD_TO_POSITION_ID
+ || action.getId() == AccessibilityNodeInfo.ACTION_CLICK) {
+ fail("It should not have special action " + action.getId());
+ }
+ }
+ }
+
+ @Test
+ public void testNoCollectionItemInfo() {
+ mInfo.setCollectionItemInfo(
+ AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(0, 1, 0, 1, false));
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(mInfo.getCollectionItemInfo()).isNull();
+ }
+
+ @Test
+ public void testStateDescriptionHasPositionForCurrentTile() {
+ mView.setTag(mHolder);
+ int position = 3;
+ when(mHolder.getLayoutPosition()).thenReturn(position);
+ when(mHolder.isCurrentTile()).thenReturn(true);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ String expectedString = mContext.getString(POSITION_STRING_ID, position);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(mInfo.getStateDescription()).isEqualTo(expectedString);
+ }
+
+ @Test
+ public void testStateDescriptionEmptyForNotCurrentTile() {
+ mView.setTag(mHolder);
+ int position = 3;
+ when(mHolder.getLayoutPosition()).thenReturn(position);
+ when(mHolder.isCurrentTile()).thenReturn(false);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(mInfo.getStateDescription()).isEqualTo("");
+ }
+
+ @Test
+ public void testClickAddAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(true);
+ when(mHolder.canRemove()).thenReturn(false);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+
+ String expectedString = mContext.getString(R.string.accessibility_qs_edit_tile_add_action);
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
+ assertThat(action.getLabel().toString()).contains(expectedString);
+ }
+
+ @Test
+ public void testClickRemoveAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(false);
+ when(mHolder.canRemove()).thenReturn(true);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+
+ String expectedString = mContext.getString(
+ R.string.accessibility_qs_edit_remove_tile_action);
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
+ assertThat(action.getLabel().toString()).contains(expectedString);
+ }
+
+ @Test
+ public void testNoClickAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(false);
+ when(mHolder.canRemove()).thenReturn(false);
+ mInfo.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
+ assertThat(action).isNull();
+ }
+
+ @Test
+ public void testAddToPositionAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(true);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(getActionForId(mInfo, ADD_TO_POSITION_ID)).isNotNull();
+ }
+
+ @Test
+ public void testNoAddToPositionAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(false);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(getActionForId(mInfo, ADD_TO_POSITION_ID)).isNull();
+ }
+
+ @Test
+ public void testMoveToPositionAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.isCurrentTile()).thenReturn(true);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(getActionForId(mInfo, MOVE_TO_POSITION_ID)).isNotNull();
+ }
+
+ @Test
+ public void testNoMoveToPositionAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.isCurrentTile()).thenReturn(false);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(getActionForId(mInfo, MOVE_TO_POSITION_ID)).isNull();
+ }
+
+ @Test
+ public void testNoInteractionsWhenCannotTakeAccessibleAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(false);
+
+ mDelegate.performAccessibilityAction(mView, AccessibilityNodeInfo.ACTION_CLICK, null);
+ mDelegate.performAccessibilityAction(mView, MOVE_TO_POSITION_ID, new Bundle());
+ mDelegate.performAccessibilityAction(mView, ADD_TO_POSITION_ID, new Bundle());
+
+ verify(mHolder, never()).toggleState();
+ verify(mHolder, never()).startAccessibleAdd();
+ verify(mHolder, never()).startAccessibleMove();
+ }
+
+ @Test
+ public void testClickActionTogglesState() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ mDelegate.performAccessibilityAction(mView, AccessibilityNodeInfo.ACTION_CLICK, null);
+
+ verify(mHolder).toggleState();
+ }
+
+ @Test
+ public void testAddToPositionActionStartsAccessibleAdd() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ mDelegate.performAccessibilityAction(mView, ADD_TO_POSITION_ID, null);
+
+ verify(mHolder).startAccessibleAdd();
+ }
+
+ @Test
+ public void testMoveToPositionActionStartsAccessibleMove() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ mDelegate.performAccessibilityAction(mView, MOVE_TO_POSITION_ID, null);
+
+ verify(mHolder).startAccessibleMove();
+ }
+
+ private AccessibilityNodeInfoCompat.AccessibilityActionCompat getActionForId(
+ AccessibilityNodeInfoCompat info, int action) {
+ for (AccessibilityNodeInfoCompat.AccessibilityActionCompat a : info.getActionList()) {
+ if (a.getId() == action) {
+ return a;
+ }
+ }
+ return null;
+ }
+}