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

Commit f4aa685b authored by Andrey Yepin's avatar Andrey Yepin
Browse files

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
parent 41a20189
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -43,6 +43,16 @@ flag {
  bug: "263474465"
}

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
+76 −0
Original line number Diff line number Diff line
@@ -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:
@@ -3395,6 +3428,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();

+20 −0
Original line number Diff line number Diff line
@@ -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) {
+5 −1
Original line number Diff line number Diff line
@@ -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);
            }
        }
    }