summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Andrey Yepin <ayepin@google.com> 2025-03-03 18:01:34 -0800
committer Andrey Yepin <ayepin@google.com> 2025-03-06 11:02:37 -0800
commitf4aa685b65bc8ba11239056784ad93a01b0dc0d9 (patch)
tree4ab4d60b144170db6e7721993cf0b20688df33df
parent41a2018978cd0c160ffe673e475e104b49dcde6d (diff)
Fix a11y focus issue with partial scrolls and notifyDataSetChanged.
When using accessibility to navigate to an off-screen item, RecyclerView would attempt to scroll the entire view to bring the next item to the top. However, if there were insufficient items for a full scroll, and the scroll triggered a notifyDataSetChanged call, RecyclerView would incorrectly reset focus to the first visible item after the scroll. Empirically, the notifyDataSetChanged is triggred by the icon loading. The workaround for the issue is to make ChooserGridAdapter to invoke notifyItemChange when the corresponded icon gets loaded. Fix: 298193161 Test: manual testing Flag: android.service.chooser.notify_single_item_change_on_icon_load Change-Id: I6def9705e57122ae69c36d87f92cdb40b5439651
-rw-r--r--core/java/android/service/chooser/flags.aconfig10
-rw-r--r--core/java/com/android/internal/app/ChooserActivity.java76
-rw-r--r--core/java/com/android/internal/app/ChooserListAdapter.java20
-rw-r--r--core/java/com/android/internal/app/ResolverListAdapter.java6
4 files changed, 111 insertions, 1 deletions
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index ae0b56e6f009..45a21beabd89 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -44,6 +44,16 @@ flag {
}
flag {
+ name: "notify_single_item_change_on_icon_load"
+ namespace: "intentresolver"
+ description: "ChooserGridAdapter to notify specific items change when the target icon is loaded (instead of all-item change)."
+ bug: "298193161"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "fix_resolver_memory_leak"
is_exported: true
namespace: "intentresolver"
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index c009fc3b7e63..9bc6671bbc31 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -23,6 +23,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
import static android.content.ContentProvider.getUriWithoutUserId;
import static android.content.ContentProvider.getUserIdFromUri;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
@@ -163,9 +164,11 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -3212,6 +3215,8 @@ public class ChooserActivity extends ResolverActivity implements
private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
+ private final Set<ViewHolderBase> mBoundViewHolders = new HashSet<>();
+
ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
super();
mChooserListAdapter = wrappedAdapter;
@@ -3232,6 +3237,31 @@ public class ChooserActivity extends ResolverActivity implements
notifyDataSetChanged();
}
});
+ if (notifySingleItemChangeOnIconLoad()) {
+ wrappedAdapter.setOnIconLoadedListener(this::onTargetIconLoaded);
+ }
+ }
+
+ private void onTargetIconLoaded(DisplayResolveInfo info) {
+ for (ViewHolderBase holder : mBoundViewHolders) {
+ switch (holder.getViewType()) {
+ case VIEW_TYPE_NORMAL:
+ TargetInfo itemInfo =
+ mChooserListAdapter.getItem(
+ ((ItemViewHolder) holder).mListPosition);
+ if (info == itemInfo) {
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ break;
+ case VIEW_TYPE_CALLER_AND_RANK:
+ ItemGroupViewHolder groupHolder = (ItemGroupViewHolder) holder;
+ if (suggestedAppsGroupContainsTarget(groupHolder, info)) {
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ break;
+ }
+
+ }
}
public void setFooterHeight(int height) {
@@ -3382,6 +3412,9 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.add((ViewHolderBase) holder);
+ }
int viewType = ((ViewHolderBase) holder).getViewType();
switch (viewType) {
case VIEW_TYPE_DIRECT_SHARE:
@@ -3396,6 +3429,22 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.remove((ViewHolderBase) holder);
+ }
+ super.onViewRecycled(holder);
+ }
+
+ @Override
+ public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.remove((ViewHolderBase) holder);
+ }
+ return super.onFailedToRecycleView(holder);
+ }
+
+ @Override
public int getItemViewType(int position) {
int count;
@@ -3604,6 +3653,33 @@ public class ChooserActivity extends ResolverActivity implements
}
}
+ /**
+ * Checks whether the suggested apps group, {@code holder}, contains the target,
+ * {@code info}.
+ */
+ private boolean suggestedAppsGroupContainsTarget(
+ ItemGroupViewHolder holder, DisplayResolveInfo info) {
+
+ int position = holder.getAdapterPosition();
+ int start = getListPosition(position);
+ int startType = getRowType(start);
+
+ int columnCount = holder.getColumnCount();
+ int end = start + columnCount - 1;
+ while (getRowType(end) != startType && end >= start) {
+ end--;
+ }
+
+ for (int i = 0; i < columnCount; i++) {
+ if (start + i <= end) {
+ if (mChooserListAdapter.getItem(holder.getItemIndex(i)) == info) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
int getListPosition(int position) {
position -= getSystemRowCount() + getProfileRowCount();
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index d38689c7505b..1b8c36db3908 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -16,9 +16,12 @@
package com.android.internal.app;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
+
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.content.ComponentName;
import android.content.Context;
@@ -56,6 +59,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
public class ChooserListAdapter extends ResolverListAdapter {
private static final String TAG = "ChooserListAdapter";
@@ -108,6 +112,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
// Represents the UserSpace in which the Initial Intents should be resolved.
private final UserHandle mInitialIntentsUserSpace;
+ @Nullable
+ private Consumer<DisplayResolveInfo> mOnIconLoadedListener;
+
// For pinned direct share labels, if the text spans multiple lines, the TextView will consume
// the full width, even if the characters actually take up less than that. Measure the actual
// line widths and constrain the View's width based upon that so that the pin doesn't end up
@@ -218,6 +225,10 @@ public class ChooserListAdapter extends ResolverListAdapter {
true);
}
+ public void setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener) {
+ mOnIconLoadedListener = onIconLoadedListener;
+ }
+
AppPredictor getAppPredictor() {
return mAppPredictor;
}
@@ -329,6 +340,15 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
+ @Override
+ protected void onIconLoaded(DisplayResolveInfo info) {
+ if (notifySingleItemChangeOnIconLoad() && mOnIconLoadedListener != null) {
+ mOnIconLoadedListener.accept(info);
+ } else {
+ notifyDataSetChanged();
+ }
+ }
+
private void loadDirectShareIcon(SelectableTargetInfo info) {
LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
if (task == null) {
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 54c0e61fd5cd..4d9ce86096c7 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -680,6 +680,10 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
+ protected void onIconLoaded(DisplayResolveInfo info) {
+ notifyDataSetChanged();
+ }
+
private void loadLabel(DisplayResolveInfo info) {
LoadLabelTask task = mLabelLoaders.get(info);
if (task == null) {
@@ -1004,7 +1008,7 @@ public class ResolverListAdapter extends BaseAdapter {
mResolverListCommunicator.updateProfileViewButton();
} else if (!mDisplayResolveInfo.hasDisplayIcon()) {
mDisplayResolveInfo.setDisplayIcon(d);
- notifyDataSetChanged();
+ onIconLoaded(mDisplayResolveInfo);
}
}
}