Merge "Load direct-share icons asynchronously" into tm-qpr-dev am: 087c3d1918

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19920873

Change-Id: I0c5ca0aa635f8cfe51a16082187cf2476e6c3f47
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Andrey Epin
2022-10-13 21:25:39 +00:00
committed by Automerger Merge Worker
4 changed files with 286 additions and 11 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,6 +87,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
private final ChooserActivityLogger mChooserActivityLogger; private final ChooserActivityLogger mChooserActivityLogger;
private int mNumShortcutResults = 0; private int mNumShortcutResults = 0;
private final Map<SelectableTargetInfo, LoadDirectShareIconTask> 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
@@ -239,7 +241,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
mListViewDataChanged = false; mListViewDataChanged = false;
} }
private void createPlaceHolders() { private void createPlaceHolders() {
mNumShortcutResults = 0; mNumShortcutResults = 0;
mServiceTargets.clear(); mServiceTargets.clear();
@@ -268,12 +269,16 @@ public class ChooserListAdapter extends ResolverListAdapter {
holder.bindIcon(info); holder.bindIcon(info);
if (info instanceof SelectableTargetInfo) { if (info instanceof SelectableTargetInfo) {
// direct share targets should append the application name for a better readout // direct share targets should append the application name for a better readout
DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo(); SelectableTargetInfo sti = (SelectableTargetInfo) info;
DisplayResolveInfo rInfo = sti.getDisplayResolveInfo();
CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
CharSequence extendedInfo = info.getExtendedInfo(); CharSequence extendedInfo = info.getExtendedInfo();
String contentDescription = String.join(" ", info.getDisplayLabel(), String contentDescription = String.join(" ", info.getDisplayLabel(),
extendedInfo != null ? extendedInfo : "", appName); extendedInfo != null ? extendedInfo : "", appName);
holder.updateContentDescription(contentDescription); holder.updateContentDescription(contentDescription);
if (!sti.hasDisplayIcon()) {
loadDirectShareIcon(sti);
}
} else if (info instanceof DisplayResolveInfo) { } else if (info instanceof DisplayResolveInfo) {
DisplayResolveInfo dri = (DisplayResolveInfo) info; DisplayResolveInfo dri = (DisplayResolveInfo) info;
if (!dri.hasDisplayIcon()) { if (!dri.hasDisplayIcon()) {
@@ -318,6 +323,20 @@ public class ChooserListAdapter extends ResolverListAdapter {
} }
} }
private void loadDirectShareIcon(SelectableTargetInfo info) {
LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
if (task == null) {
task = createLoadDirectShareIconTask(info);
mIconLoaders.put(info, task);
task.loadIcon();
}
}
@VisibleForTesting
protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) {
return new LoadDirectShareIconTask(info);
}
void updateAlphabeticalList() { void updateAlphabeticalList() {
new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
@Override @Override
@@ -731,7 +750,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
* 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();
@@ -739,4 +759,35 @@ 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, Boolean> {
private final SelectableTargetInfo mTargetInfo;
private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) {
mTargetInfo = targetInfo;
}
@Override
protected Boolean doInBackground(Void... voids) {
return mTargetInfo.loadIcon();
}
@Override
protected void onPostExecute(Boolean isLoaded) {
if (isLoaded) {
notifyDataSetChanged();
}
}
/**
* An alias for execute to use with unit tests.
*/
public void loadIcon() {
execute();
}
}
} }

View File

@@ -870,7 +870,12 @@ public class ResolverListAdapter extends BaseAdapter {
void onHandlePackagesChanged(ResolverListAdapter listAdapter); void onHandlePackagesChanged(ResolverListAdapter listAdapter);
} }
static class ViewHolder { /**
* A view holder keeps a reference to a list view and provides functionality for managing its
* state.
*/
@VisibleForTesting
public static class ViewHolder {
public View itemView; public View itemView;
public Drawable defaultItemViewBackground; public Drawable defaultItemViewBackground;
@@ -878,7 +883,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;
synchronized (other) {
mShortcutInfo = other.mShortcutInfo;
mDisplayIcon = other.mDisplayIcon; mDisplayIcon = other.mDisplayIcon;
}
mFillInIntent = fillInIntent; mFillInIntent = fillInIntent;
mFillInFlags = flags; mFillInFlags = flags;
mModifiedScore = other.mModifiedScore; mModifiedScore = other.mModifiedScore;
@@ -141,6 +147,27 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
return mSourceInfo; return mSourceInfo;
} }
/**
* Load display icon, if needed.
*/
public boolean loadIcon() {
ShortcutInfo shortcutInfo;
Drawable icon;
synchronized (this) {
shortcutInfo = mShortcutInfo;
icon = mDisplayIcon;
}
boolean shouldLoadIcon = icon == null && shortcutInfo != null;
if (shouldLoadIcon) {
icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo);
synchronized (this) {
mDisplayIcon = icon;
mShortcutInfo = null;
}
}
return shouldLoadIcon;
}
private Drawable getChooserTargetIconDrawable(ChooserTarget target, private Drawable getChooserTargetIconDrawable(ChooserTarget target,
@Nullable ShortcutInfo shortcutInfo) { @Nullable ShortcutInfo shortcutInfo) {
Drawable directShareIcon = null; Drawable directShareIcon = null;
@@ -271,10 +298,17 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
} }
@Override @Override
public Drawable getDisplayIcon(Context context) { public synchronized Drawable getDisplayIcon(Context context) {
return mDisplayIcon; return mDisplayIcon;
} }
/**
* @return true if display icon is available
*/
public synchronized boolean hasDisplayIcon() {
return mDisplayIcon != null;
}
public ChooserTarget getChooserTarget() { public ChooserTarget getChooserTarget() {
return mChooserTarget; return mChooserTarget;
} }

View File

@@ -0,0 +1,184 @@
/*
* 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.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
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.ChooserListAdapter.LoadDirectShareIconTask
import com.android.internal.app.chooser.SelectableTargetInfo
import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator
import com.android.internal.app.chooser.TargetInfo
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.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 fun createChooserListAdapter(
taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
) =
ChooserListAdapterOverride(
context,
emptyList(),
emptyArray(),
emptyList(),
false,
resolverListController,
chooserListCommunicator,
selectableTargetInfoCommunicator,
packageManager,
chooserActivityLogger,
taskProvider
)
@Test
fun testDirectShareTargetLoadingIconIsStarted() {
val view = createView()
val viewHolder = ResolverListAdapter.ViewHolder(view)
view.tag = viewHolder
val targetInfo = createSelectableTargetInfo()
val iconTask = mock<LoadDirectShareIconTask>()
val testSubject = createChooserListAdapter { iconTask }
testSubject.testViewBind(view, targetInfo, 0)
verify(iconTask, times(1)).loadIcon()
}
@Test
fun testOnlyOneTaskPerTarget() {
val view = createView()
val viewHolderOne = ResolverListAdapter.ViewHolder(view)
view.tag = viewHolderOne
val targetInfo = createSelectableTargetInfo()
val iconTaskOne = mock<LoadDirectShareIconTask>()
val testTaskProvider = mock<() -> LoadDirectShareIconTask> {
whenever(invoke()).thenReturn(iconTaskOne)
}
val testSubject = createChooserListAdapter { testTaskProvider.invoke() }
testSubject.testViewBind(view, targetInfo, 0)
val viewHolderTwo = ResolverListAdapter.ViewHolder(view)
view.tag = viewHolderTwo
whenever(testTaskProvider()).thenReturn(mock())
testSubject.testViewBind(view, targetInfo, 0)
verify(iconTaskOne, times(1)).loadIcon()
verify(testTaskProvider, times(1)).invoke()
}
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
}
}
private class ChooserListAdapterOverride(
context: Context?,
payloadIntents: List<Intent>?,
initialIntents: Array<out Intent>?,
rList: List<ResolveInfo>?,
filterLastUsed: Boolean,
resolverListController: ResolverListController?,
chooserListCommunicator: ChooserListCommunicator?,
selectableTargetInfoCommunicator: SelectableTargetInfoCommunicator?,
packageManager: PackageManager?,
chooserActivityLogger: ChooserActivityLogger?,
private val taskProvider: (SelectableTargetInfo?) -> LoadDirectShareIconTask
) : ChooserListAdapter(
context,
payloadIntents,
initialIntents,
rList,
filterLastUsed,
resolverListController,
chooserListCommunicator,
selectableTargetInfoCommunicator,
packageManager,
chooserActivityLogger
) {
override fun createLoadDirectShareIconTask(
info: SelectableTargetInfo?
): LoadDirectShareIconTask =
taskProvider.invoke(info)
fun testViewBind(view: View?, info: TargetInfo?, position: Int) {
onBindView(view, info, position)
}
}