diff options
13 files changed, 136 insertions, 33 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 4608f37b..0e519726 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -109,6 +109,7 @@ import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider; import com.android.intentresolver.emptystate.WorkProfilePausedEmptyStateProvider; import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.icons.Caching; import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.inject.Background; import com.android.intentresolver.logging.EventLog; @@ -166,6 +167,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import javax.inject.Inject; +import javax.inject.Provider; /** * The Chooser Activity handles intent resolution specifically for sharing intents - @@ -256,7 +258,11 @@ public class ChooserActivity extends Hilt_ChooserActivity implements @Inject @AppPredictionAvailable public boolean mAppPredictionAvailable; @Inject @ImageEditor public Optional<ComponentName> mImageEditor; @Inject @NearbyShare public Optional<ComponentName> mNearbyShare; - @Inject public TargetDataLoader mTargetDataLoader; + protected TargetDataLoader mTargetDataLoader; + @Inject public Provider<TargetDataLoader> mTargetDataLoaderProvider; + @Inject + @Caching + public Provider<TargetDataLoader> mCachingTargetDataLoaderProvider; @Inject public DevicePolicyResources mDevicePolicyResources; @Inject public ProfilePagerResources mProfilePagerResources; @Inject public PackageManager mPackageManager; @@ -331,6 +337,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements super.onCreate(savedInstanceState); Log.i(TAG, "onCreate"); + mTargetDataLoader = mChooserServiceFeatureFlags.chooserPayloadToggling() + ? mCachingTargetDataLoaderProvider.get() + : mTargetDataLoaderProvider.get(); + setTheme(R.style.Theme_DeviceDefault_Chooser); // Initializer is invoked when this function returns, via Lifecycle. @@ -767,6 +777,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mRequest.getInitialIntents(), mMaxTargetsPerRow); mChooserMultiProfilePagerAdapter.setCurrentPage(currentPage); + for (int i = 0, count = mChooserMultiProfilePagerAdapter.getItemCount(); i < count; i++) { + mChooserMultiProfilePagerAdapter.getPageAdapterForIndex(i) + .getListAdapter().setAnimateItems(false); + } if (mPersonalPackageMonitor != null) { mPersonalPackageMonitor.unregister(); } @@ -1375,17 +1389,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE); } - protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter() { - return createMultiProfilePagerAdapter( - /* context = */ this, - mProfilePagerResources, - mViewModel.getRequest().getValue(), - mProfiles, - mProfileAvailability, - mRequest.getInitialIntents(), - mMaxTargetsPerRow); - } - private ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter( Context context, ProfilePagerResources profilePagerResources, diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index 29b5698b..8b848e55 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -153,6 +153,8 @@ public class ChooserListAdapter extends ResolverListAdapter { } }; + private boolean mAnimateItems = true; + public ChooserListAdapter( Context context, List<Intent> payloadIntents, @@ -308,6 +310,10 @@ public class ChooserListAdapter extends ResolverListAdapter { } } + public void setAnimateItems(boolean animateItems) { + mAnimateItems = animateItems; + } + @Override public void handlePackagesChanged() { if (mPackageChangeCallback != null) { @@ -371,18 +377,15 @@ public class ChooserListAdapter extends ResolverListAdapter { final CharSequence displayLabel = Objects.requireNonNullElse(info.getDisplayLabel(), ""); final CharSequence extendedInfo = Objects.requireNonNullElse(info.getExtendedInfo(), ""); holder.bindLabel(displayLabel, extendedInfo); - if (!TextUtils.isEmpty(displayLabel)) { + if (mAnimateItems && !TextUtils.isEmpty(displayLabel)) { mAnimationTracker.animateLabel(holder.text, info); } - if (!TextUtils.isEmpty(extendedInfo) && holder.text2.getVisibility() == View.VISIBLE) { + if (mAnimateItems + && !TextUtils.isEmpty(extendedInfo) + && holder.text2.getVisibility() == View.VISIBLE) { mAnimationTracker.animateLabel(holder.text2, info); } - holder.bindIcon(info); - if (info.hasDisplayIcon()) { - mAnimationTracker.animateIcon(holder.icon, info); - } - if (info.isSelectableTargetInfo()) { // direct share targets should append the application name for a better readout DisplayResolveInfo rInfo = info.getDisplayResolveInfo(); @@ -418,6 +421,11 @@ public class ChooserListAdapter extends ResolverListAdapter { } } + holder.bindIcon(info); + if (mAnimateItems && info.hasDisplayIcon()) { + mAnimationTracker.animateIcon(holder.icon, info); + } + if (info.isPlaceHolderTargetInfo()) { bindPlaceholder(holder); } diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index e79cb2d1..a402fc72 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -1357,7 +1357,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements // Load the icon asynchronously ImageView icon = findViewById(com.android.internal.R.id.icon); - targetDataLoader.loadAppTargetIcon( + targetDataLoader.getOrLoadAppTargetIcon( otherProfileResolveInfo, inactiveAdapter.getUserHandle(), (drawable) -> { diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index 2a8fcfa4..5fd37d43 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -739,26 +739,31 @@ public class ResolverListAdapter extends BaseAdapter { holder.bindLabel("", ""); loadLabel(dri); } - holder.bindIcon(info); if (!dri.hasDisplayIcon()) { loadIcon(dri); } + holder.bindIcon(info); } } protected final void loadIcon(DisplayResolveInfo info) { if (mRequestedIcons.add(info)) { - mTargetDataLoader.loadAppTargetIcon( + Drawable icon = mTargetDataLoader.getOrLoadAppTargetIcon( info, getUserHandle(), - (drawable) -> onIconLoaded(info, drawable)); + (drawable) -> { + onIconLoaded(info, drawable); + notifyDataSetChanged(); + }); + if (icon != null) { + onIconLoaded(info, icon); + } } } private void onIconLoaded(DisplayResolveInfo displayResolveInfo, Drawable drawable) { if (!displayResolveInfo.hasDisplayIcon()) { displayResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable); - notifyDataSetChanged(); } } @@ -822,7 +827,7 @@ public class ResolverListAdapter extends BaseAdapter { public void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) { final DisplayResolveInfo iconInfo = getFilteredItem(); if (iconInfo != null) { - mTargetDataLoader.loadAppTargetIcon( + mTargetDataLoader.getOrLoadAppTargetIcon( iconInfo, getUserHandle(), iconView::setImageDrawable); } } diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java index 536f11ce..5e44c53e 100644 --- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java @@ -196,6 +196,7 @@ public class DisplayResolveInfo implements TargetInfo { } @Override + @NonNull public ComponentName getResolvedComponentName() { return new ComponentName(mResolveInfo.activityInfo.packageName, mResolveInfo.activityInfo.name); diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java index 4fe28384..95cb443e 100644 --- a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java @@ -23,6 +23,7 @@ import android.os.Bundle; import android.os.UserHandle; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; @@ -123,6 +124,7 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo { } @Override + @NonNull public ComponentName getResolvedComponentName() { if (hasSelected()) { return mTargetInfos.get(mSelected).getResolvedComponentName(); diff --git a/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt new file mode 100644 index 00000000..b3054231 --- /dev/null +++ b/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024 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.intentresolver.icons + +import android.content.ComponentName +import android.graphics.drawable.Drawable +import android.os.UserHandle +import androidx.collection.LruCache +import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.chooser.SelectableTargetInfo +import java.util.function.Consumer +import javax.annotation.concurrent.GuardedBy +import javax.inject.Qualifier + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.BINARY) annotation class Caching + +private typealias IconCache = LruCache<ComponentName, Drawable> + +class CachingTargetDataLoader( + private val targetDataLoader: TargetDataLoader, + private val cacheSize: Int = 100, +) : TargetDataLoader() { + @GuardedBy("self") private val perProfileIconCache = HashMap<UserHandle, IconCache>() + + override fun getOrLoadAppTargetIcon( + info: DisplayResolveInfo, + userHandle: UserHandle, + callback: Consumer<Drawable> + ): Drawable? { + val cacheKey = info.toCacheKey() + return getCachedAppIcon(cacheKey, userHandle) + ?: targetDataLoader.getOrLoadAppTargetIcon(info, userHandle) { drawable -> + getProfileIconCache(userHandle).put(cacheKey, drawable) + callback.accept(drawable) + } + } + + override fun loadDirectShareIcon( + info: SelectableTargetInfo, + userHandle: UserHandle, + callback: Consumer<Drawable> + ) = targetDataLoader.loadDirectShareIcon(info, userHandle, callback) + + override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>) = + targetDataLoader.loadLabel(info, callback) + + override fun getOrLoadLabel(info: DisplayResolveInfo) = targetDataLoader.getOrLoadLabel(info) + + private fun getCachedAppIcon(component: ComponentName, userHandle: UserHandle): Drawable? = + getProfileIconCache(userHandle)[component] + + private fun getProfileIconCache(userHandle: UserHandle): IconCache = + synchronized(perProfileIconCache) { + perProfileIconCache.getOrPut(userHandle) { IconCache(cacheSize) } + } + + private fun DisplayResolveInfo.toCacheKey() = + ComponentName( + resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name, + ) +} diff --git a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt index 054fbe71..1a724d73 100644 --- a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt +++ b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt @@ -62,11 +62,11 @@ class DefaultTargetDataLoader( ) } - override fun loadAppTargetIcon( + override fun getOrLoadAppTargetIcon( info: DisplayResolveInfo, userHandle: UserHandle, callback: Consumer<Drawable>, - ) { + ): Drawable? { val taskId = nextTaskId.getAndIncrement() LoadIconTask(context, info, userHandle, presentationFactory) { result -> removeTask(taskId) @@ -74,6 +74,7 @@ class DefaultTargetDataLoader( } .also { addTask(taskId, it) } .executeOnExecutor(executor) + return null } override fun loadDirectShareIcon( diff --git a/java/src/com/android/intentresolver/icons/LabelInfo.kt b/java/src/com/android/intentresolver/icons/LabelInfo.kt index a9c4cd77..4b60d607 100644 --- a/java/src/com/android/intentresolver/icons/LabelInfo.kt +++ b/java/src/com/android/intentresolver/icons/LabelInfo.kt @@ -16,4 +16,4 @@ package com.android.intentresolver.icons -class LabelInfo(val label: CharSequence?, val subLabel: CharSequence?) +data class LabelInfo(val label: CharSequence?, val subLabel: CharSequence?) diff --git a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt index 07c62177..7789df44 100644 --- a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt +++ b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt @@ -25,11 +25,11 @@ import java.util.function.Consumer /** A target data loader contract. Added to support testing. */ abstract class TargetDataLoader { /** Load an app target icon */ - abstract fun loadAppTargetIcon( + abstract fun getOrLoadAppTargetIcon( info: DisplayResolveInfo, userHandle: UserHandle, callback: Consumer<Drawable>, - ) + ): Drawable? /** Load a shortcut icon */ abstract fun loadDirectShareIcon( diff --git a/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt b/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt index 32c040b8..9c0acb11 100644 --- a/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt +++ b/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt @@ -35,4 +35,10 @@ object TargetDataLoaderModule { @ActivityContext context: Context, @ActivityOwned lifecycle: Lifecycle, ): TargetDataLoader = DefaultTargetDataLoader(context, lifecycle, isAudioCaptureDevice = false) + + @Provides + @ActivityScoped + @Caching + fun cachingTargetDataLoader(targetDataLoader: TargetDataLoader): TargetDataLoader = + CachingTargetDataLoader(targetDataLoader) } diff --git a/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java index 30858c8e..b46d8bc3 100644 --- a/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java +++ b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java @@ -171,11 +171,12 @@ public class ResolverWrapperActivity extends ResolverActivity { } @Override - public void loadAppTargetIcon( + @Nullable + public Drawable getOrLoadAppTargetIcon( @NonNull DisplayResolveInfo info, @NonNull UserHandle userHandle, @NonNull Consumer<Drawable> callback) { - mTargetDataLoader.loadAppTargetIcon(info, userHandle, callback); + return mTargetDataLoader.getOrLoadAppTargetIcon(info, userHandle, callback); } @Override diff --git a/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt index bbe8a29e..5ac4f2b0 100644 --- a/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt +++ b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt @@ -140,7 +140,7 @@ class ChooserListAdapterTest { testSubject.onBindView(view, targetInfo, 0) - verify(mTargetDataLoader, times(1)).loadAppTargetIcon(any(), any(), any()) + verify(mTargetDataLoader, times(1)).getOrLoadAppTargetIcon(any(), any(), any()) } @Test |