Merge "Load direct-share icons asynchronously" into tm-qpr-dev

This commit is contained in:
Andrey Epin
2022-09-07 02:44:24 +00:00
committed by Android (Google) Code Review
4 changed files with 309 additions and 33 deletions

View File

@@ -43,6 +43,7 @@ import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import com.android.internal.R; import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.ChooserTargetInfo; import com.android.internal.app.chooser.ChooserTargetInfo;
import com.android.internal.app.chooser.DisplayResolveInfo; import com.android.internal.app.chooser.DisplayResolveInfo;
@@ -86,7 +87,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
private final ChooserActivityLogger mChooserActivityLogger; private final ChooserActivityLogger mChooserActivityLogger;
private int mNumShortcutResults = 0; private int mNumShortcutResults = 0;
private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>(); private final Map<TargetInfo, AsyncTask> mIconLoaders = new HashMap<>();
private boolean mApplySharingAppLimits; private boolean mApplySharingAppLimits;
// Reserve spots for incoming direct share targets by adding placeholders // Reserve spots for incoming direct share targets by adding placeholders
@@ -104,6 +105,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
private AppPredictor mAppPredictor; private AppPredictor mAppPredictor;
private AppPredictor.Callback mAppPredictorCallback; private AppPredictor.Callback mAppPredictorCallback;
private LoadDirectShareIconTaskProvider mTestLoadDirectShareTaskProvider;
// For pinned direct share labels, if the text spans multiple lines, the TextView will consume // 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 // 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 // line widths and constrain the View's width based upon that so that the pin doesn't end up
@@ -240,7 +243,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
mListViewDataChanged = false; mListViewDataChanged = false;
} }
private void createPlaceHolders() { private void createPlaceHolders() {
mNumShortcutResults = 0; mNumShortcutResults = 0;
mServiceTargets.clear(); mServiceTargets.clear();
@@ -265,31 +267,25 @@ public class ChooserListAdapter extends ResolverListAdapter {
return; return;
} }
if (!(info instanceof DisplayResolveInfo)) { if (info instanceof DisplayResolveInfo) {
holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
holder.bindIcon(info);
if (info instanceof SelectableTargetInfo) {
// direct share targets should append the application name for a better readout
DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
CharSequence extendedInfo = info.getExtendedInfo();
String contentDescription = String.join(" ", info.getDisplayLabel(),
extendedInfo != null ? extendedInfo : "", appName);
holder.updateContentDescription(contentDescription);
}
} else {
DisplayResolveInfo dri = (DisplayResolveInfo) info; DisplayResolveInfo dri = (DisplayResolveInfo) info;
holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel()); holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
LoadIconTask task = mIconLoaders.get(dri); startDisplayResolveInfoIconLoading(holder, dri);
if (task == null) { } else {
task = new LoadIconTask(dri, holder); holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
mIconLoaders.put(dri, task);
task.execute(); if (info instanceof SelectableTargetInfo) {
SelectableTargetInfo selectableInfo = (SelectableTargetInfo) info;
// direct share targets should append the application name for a better readout
DisplayResolveInfo rInfo = selectableInfo.getDisplayResolveInfo();
CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
CharSequence extendedInfo = selectableInfo.getExtendedInfo();
String contentDescription = String.join(" ", selectableInfo.getDisplayLabel(),
extendedInfo != null ? extendedInfo : "", appName);
holder.updateContentDescription(contentDescription);
startSelectableTargetInfoIconLoading(holder, selectableInfo);
} else { } else {
// The holder was potentially changed as the underlying items were holder.bindIcon(info);
// reshuffled, so reset the target holder
task.setViewHolder(holder);
} }
} }
@@ -330,6 +326,32 @@ public class ChooserListAdapter extends ResolverListAdapter {
} }
} }
private void startDisplayResolveInfoIconLoading(ViewHolder holder, DisplayResolveInfo info) {
LoadIconTask task = (LoadIconTask) mIconLoaders.get(info);
if (task == null) {
task = new LoadIconTask(info, holder);
mIconLoaders.put(info, task);
task.execute();
} else {
// The holder was potentially changed as the underlying items were
// reshuffled, so reset the target holder
task.setViewHolder(holder);
}
}
private void startSelectableTargetInfoIconLoading(
ViewHolder holder, SelectableTargetInfo info) {
LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
if (task == null) {
task = mTestLoadDirectShareTaskProvider == null
? new LoadDirectShareIconTask(info)
: mTestLoadDirectShareTaskProvider.get();
mIconLoaders.put(info, task);
task.loadIcon();
}
task.setViewHolder(holder);
}
void updateAlphabeticalList() { void updateAlphabeticalList() {
new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
@Override @Override
@@ -344,7 +366,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
Map<String, DisplayResolveInfo> consolidated = new HashMap<>(); Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
for (DisplayResolveInfo info : allTargets) { for (DisplayResolveInfo info : allTargets) {
String resolvedTarget = info.getResolvedComponentName().getPackageName() String resolvedTarget = info.getResolvedComponentName().getPackageName()
+ '#' + info.getDisplayLabel(); + '#' + info.getDisplayLabel();
DisplayResolveInfo multiDri = consolidated.get(resolvedTarget); DisplayResolveInfo multiDri = consolidated.get(resolvedTarget);
if (multiDri == null) { if (multiDri == null) {
consolidated.put(resolvedTarget, info); consolidated.put(resolvedTarget, info);
@@ -353,7 +375,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
} else { } else {
// create consolidated target from the single DisplayResolveInfo // create consolidated target from the single DisplayResolveInfo
MultiDisplayResolveInfo multiDisplayResolveInfo = MultiDisplayResolveInfo multiDisplayResolveInfo =
new MultiDisplayResolveInfo(resolvedTarget, multiDri); new MultiDisplayResolveInfo(resolvedTarget, multiDri);
multiDisplayResolveInfo.addTarget(info); multiDisplayResolveInfo.addTarget(info);
consolidated.put(resolvedTarget, multiDisplayResolveInfo); consolidated.put(resolvedTarget, multiDisplayResolveInfo);
} }
@@ -739,11 +761,25 @@ public class ChooserListAdapter extends ResolverListAdapter {
} }
} }
/**
* An alias for onBindView to use with unit tests.
*/
@VisibleForTesting
public void testViewBind(View view, TargetInfo info, int position) {
onBindView(view, info, position);
}
@VisibleForTesting
public void setTestLoadDirectShareTaskProvider(LoadDirectShareIconTaskProvider provider) {
mTestLoadDirectShareTaskProvider = provider;
}
/** /**
* Necessary methods to communicate between {@link ChooserListAdapter} * Necessary methods to communicate between {@link ChooserListAdapter}
* and {@link ChooserActivity}. * and {@link ChooserActivity}.
*/ */
interface ChooserListCommunicator extends ResolverListCommunicator { @VisibleForTesting
public interface ChooserListCommunicator extends ResolverListCommunicator {
int getMaxRankedTargets(); int getMaxRankedTargets();
@@ -751,4 +787,59 @@ public class ChooserListAdapter extends ResolverListAdapter {
boolean isSendAction(Intent targetIntent); boolean isSendAction(Intent targetIntent);
} }
/**
* Loads direct share targets icons.
*/
@VisibleForTesting
public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Void> {
private final SelectableTargetInfo mTargetInfo;
private ViewHolder mViewHolder;
private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) {
mTargetInfo = targetInfo;
}
@Override
protected Void doInBackground(Void... voids) {
mTargetInfo.loadIcon();
return null;
}
@Override
protected void onPostExecute(Void arg) {
if (mViewHolder != null) {
mViewHolder.bindIcon(mTargetInfo);
notifyDataSetChanged();
}
}
/**
* Specifies a view holder that will be updated when the task is completed.
*/
public void setViewHolder(ViewHolder viewHolder) {
mViewHolder = viewHolder;
mViewHolder.bindIcon(mTargetInfo);
notifyDataSetChanged();
}
/**
* An alias for execute to use with unit tests.
*/
public void loadIcon() {
execute();
}
}
/**
* An interface for the unit tests to override icon loading task creation
*/
@VisibleForTesting
public interface LoadDirectShareIconTaskProvider {
/**
* Provides an instance of the task.
* @return
*/
LoadDirectShareIconTask get();
}
} }

View File

@@ -834,7 +834,11 @@ public class ResolverListAdapter extends BaseAdapter {
void onHandlePackagesChanged(ResolverListAdapter listAdapter); void onHandlePackagesChanged(ResolverListAdapter listAdapter);
} }
static class ViewHolder { /**
* A view holder.
*/
@VisibleForTesting
public static class ViewHolder {
public View itemView; public View itemView;
public Drawable defaultItemViewBackground; public Drawable defaultItemViewBackground;
@@ -842,7 +846,8 @@ public class ResolverListAdapter extends BaseAdapter {
public TextView text2; public TextView text2;
public ImageView icon; public ImageView icon;
ViewHolder(View view) { @VisibleForTesting
public ViewHolder(View view) {
itemView = view; itemView = view;
defaultItemViewBackground = view.getBackground(); defaultItemViewBackground = view.getBackground();
text = (TextView) view.findViewById(com.android.internal.R.id.text1); text = (TextView) view.findViewById(com.android.internal.R.id.text1);

View File

@@ -37,6 +37,7 @@ import android.service.chooser.ChooserTarget;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.Log; import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.ChooserActivity; import com.android.internal.app.ChooserActivity;
import com.android.internal.app.ResolverActivity; import com.android.internal.app.ResolverActivity;
import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter; import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
@@ -59,8 +60,11 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
private final String mDisplayLabel; private final String mDisplayLabel;
private final PackageManager mPm; private final PackageManager mPm;
private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator; private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
@GuardedBy("this")
private ShortcutInfo mShortcutInfo;
private Drawable mBadgeIcon = null; private Drawable mBadgeIcon = null;
private CharSequence mBadgeContentDescription; private CharSequence mBadgeContentDescription;
@GuardedBy("this")
private Drawable mDisplayIcon; private Drawable mDisplayIcon;
private final Intent mFillInIntent; private final Intent mFillInIntent;
private final int mFillInFlags; private final int mFillInFlags;
@@ -78,6 +82,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
mModifiedScore = modifiedScore; mModifiedScore = modifiedScore;
mPm = mContext.getPackageManager(); mPm = mContext.getPackageManager();
mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator; mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
mShortcutInfo = shortcutInfo;
mIsPinned = shortcutInfo != null && shortcutInfo.isPinned(); mIsPinned = shortcutInfo != null && shortcutInfo.isPinned();
if (sourceInfo != null) { if (sourceInfo != null) {
final ResolveInfo ri = sourceInfo.getResolveInfo(); final ResolveInfo ri = sourceInfo.getResolveInfo();
@@ -92,8 +97,6 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
} }
} }
} }
// TODO(b/121287224): do this in the background thread, and only for selected targets
mDisplayIcon = getChooserTargetIconDrawable(chooserTarget, shortcutInfo);
if (sourceInfo != null) { if (sourceInfo != null) {
mBackupResolveInfo = null; mBackupResolveInfo = null;
@@ -118,7 +121,10 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
mChooserTarget = other.mChooserTarget; mChooserTarget = other.mChooserTarget;
mBadgeIcon = other.mBadgeIcon; mBadgeIcon = other.mBadgeIcon;
mBadgeContentDescription = other.mBadgeContentDescription; mBadgeContentDescription = other.mBadgeContentDescription;
mDisplayIcon = other.mDisplayIcon; synchronized (other) {
mShortcutInfo = other.mShortcutInfo;
mDisplayIcon = other.mDisplayIcon;
}
mFillInIntent = fillInIntent; mFillInIntent = fillInIntent;
mFillInFlags = flags; mFillInFlags = flags;
mModifiedScore = other.mModifiedScore; mModifiedScore = other.mModifiedScore;
@@ -141,6 +147,25 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
return mSourceInfo; return mSourceInfo;
} }
/**
* Load display icon, if needed.
*/
public void loadIcon() {
ShortcutInfo shortcutInfo;
Drawable icon;
synchronized (this) {
shortcutInfo = mShortcutInfo;
icon = mDisplayIcon;
}
if (icon == null && shortcutInfo != null) {
icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo);
synchronized (this) {
mDisplayIcon = icon;
mShortcutInfo = null;
}
}
}
private Drawable getChooserTargetIconDrawable(ChooserTarget target, private Drawable getChooserTargetIconDrawable(ChooserTarget target,
@Nullable ShortcutInfo shortcutInfo) { @Nullable ShortcutInfo shortcutInfo) {
Drawable directShareIcon = null; Drawable directShareIcon = null;
@@ -270,7 +295,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
} }
@Override @Override
public Drawable getDisplayIcon(Context context) { public synchronized Drawable getDisplayIcon(Context context) {
return mDisplayIcon; return mDisplayIcon;
} }

View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2022 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.app
import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Bundle
import android.service.chooser.ChooserTarget
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.R
import com.android.internal.app.chooser.SelectableTargetInfo
import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator
import com.android.server.testutils.any
import com.android.server.testutils.mock
import com.android.server.testutils.whenever
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class ChooserListAdapterTest {
private val packageManager = mock<PackageManager> {
whenever(resolveActivity(any(), anyInt())).thenReturn(mock())
}
private val context = InstrumentationRegistry.getInstrumentation().getContext()
private val resolverListController = mock<ResolverListController>()
private val chooserListCommunicator = mock<ChooserListAdapter.ChooserListCommunicator> {
whenever(maxRankedTargets).thenReturn(0)
}
private val selectableTargetInfoCommunicator =
mock<SelectableTargetInfoCommunicator> {
whenever(targetIntent).thenReturn(mock())
}
private val chooserActivityLogger = mock<ChooserActivityLogger>()
private val testSubject = ChooserListAdapter(
context,
emptyList(),
emptyArray(),
emptyList(),
false,
resolverListController,
chooserListCommunicator,
selectableTargetInfoCommunicator,
packageManager,
chooserActivityLogger
)
@Test
fun testDirectShareTargetLoadingIconIsStarted() {
val view = createView()
val viewHolder = ResolverListAdapter.ViewHolder(view)
view.tag = viewHolder
val targetInfo = createSelectableTargetInfo()
val iconTask = mock<ChooserListAdapter.LoadDirectShareIconTask> {
doNothing().whenever(this).loadIcon()
}
testSubject.setTestLoadDirectShareTaskProvider(
mock {
whenever(get()).thenReturn(iconTask)
}
)
testSubject.testViewBind(view, targetInfo, 0)
verify(iconTask, times(1)).loadIcon()
verify(iconTask, times(1)).setViewHolder(viewHolder)
}
@Test
fun testOnlyOneTaskPerTarget() {
val view = createView()
val viewHolderOne = ResolverListAdapter.ViewHolder(view)
view.tag = viewHolderOne
val targetInfo = createSelectableTargetInfo()
val iconTaskOne = mock<ChooserListAdapter.LoadDirectShareIconTask> {
doNothing().whenever(this).loadIcon()
}
val testTaskProvider = mock<ChooserListAdapter.LoadDirectShareIconTaskProvider> {
whenever(get()).thenReturn(iconTaskOne)
}
testSubject.setTestLoadDirectShareTaskProvider(
testTaskProvider
)
testSubject.testViewBind(view, targetInfo, 0)
val viewHolderTwo = ResolverListAdapter.ViewHolder(view)
view.tag = viewHolderTwo
whenever(testTaskProvider.get()).thenReturn(mock())
testSubject.testViewBind(view, targetInfo, 0)
verify(iconTaskOne, times(1)).loadIcon()
verify(iconTaskOne, times(1)).setViewHolder(viewHolderOne)
verify(iconTaskOne, times(1)).setViewHolder(viewHolderTwo)
verify(testTaskProvider, times(1)).get()
}
private fun createSelectableTargetInfo(): SelectableTargetInfo =
SelectableTargetInfo(
context,
null,
createChooserTarget(),
1f,
selectableTargetInfoCommunicator,
null
)
private fun createChooserTarget(): ChooserTarget =
ChooserTarget(
"Title",
null,
1f,
ComponentName("package", "package.Class"),
Bundle()
)
private fun createView(): View {
val view = FrameLayout(context)
TextView(context).apply {
id = R.id.text1
view.addView(this)
}
TextView(context).apply {
id = R.id.text2
view.addView(this)
}
ImageView(context).apply {
id = R.id.icon
view.addView(this)
}
return view
}
}