summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
author Xin Li <delphij@google.com> 2023-03-13 23:11:31 -0700
committer Xin Li <delphij@google.com> 2023-03-13 23:11:31 -0700
commit7b21dc4a35cae1218308a2f04fc61d6247faa17b (patch)
tree1448e0c76772f25db02c8931f588e0d32673d1d4 /java/src
parentcc64c57aa426bf71e88dc073b8197748fd720856 (diff)
parent1606e219c8db1c233713f9dc2546225533718eca (diff)
Merge Android 13 QPR2
Bug: 273316506 Merged-In: Ia56e92ed5358ca66185f5011abd139392ee73785 Change-Id: Ib152678de052bf41ad0716401561c7e505614fe5
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java409
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java3122
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityLogger.java311
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java84
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java132
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewUi.java566
-rw-r--r--java/src/com/android/intentresolver/ChooserGridLayoutManager.java4
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java564
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java375
-rw-r--r--java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java4
-rw-r--r--java/src/com/android/intentresolver/ChooserRequestParameters.java441
-rw-r--r--java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java64
-rw-r--r--java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java123
-rw-r--r--java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt107
-rw-r--r--java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java225
-rw-r--r--java/src/com/android/intentresolver/ImagePreviewImageLoader.kt38
-rw-r--r--java/src/com/android/intentresolver/IntentForwarderActivity.java3
-rw-r--r--java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java154
-rw-r--r--java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java137
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java291
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java422
-rw-r--r--java/src/com/android/intentresolver/ResolverListController.java28
-rw-r--r--java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java303
-rw-r--r--java/src/com/android/intentresolver/ResolverViewPager.java11
-rw-r--r--java/src/com/android/intentresolver/ShortcutSelectionLogic.java193
-rw-r--r--java/src/com/android/intentresolver/SimpleIconFactory.java16
-rw-r--r--java/src/com/android/intentresolver/TargetPresentationGetter.java267
-rw-r--r--java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java114
-rw-r--r--java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java39
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java180
-rw-r--r--java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java43
-rw-r--r--java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java72
-rw-r--r--java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java504
-rw-r--r--java/src/com/android/intentresolver/chooser/TargetInfo.java322
-rw-r--r--java/src/com/android/intentresolver/grid/ChooserGridAdapter.java604
-rw-r--r--java/src/com/android/intentresolver/grid/DirectShareViewHolder.java197
-rw-r--r--java/src/com/android/intentresolver/grid/FooterViewHolder.java (renamed from java/src/com/android/intentresolver/ChooserFlags.java)21
-rw-r--r--java/src/com/android/intentresolver/grid/ItemGroupViewHolder.java76
-rw-r--r--java/src/com/android/intentresolver/grid/ItemViewHolder.java63
-rw-r--r--java/src/com/android/intentresolver/grid/SingleRowViewHolder.java73
-rw-r--r--java/src/com/android/intentresolver/grid/ViewHolderBase.java35
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java (renamed from java/src/com/android/intentresolver/AbstractResolverComparator.java)35
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java (renamed from java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java)13
-rw-r--r--java/src/com/android/intentresolver/model/ResolverComparatorModel.java (renamed from java/src/com/android/intentresolver/ResolverComparatorModel.java)3
-rw-r--r--java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java (renamed from java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java)26
-rw-r--r--java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt67
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java426
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java109
-rw-r--r--java/src/com/android/intentresolver/widget/ActionRow.kt33
-rw-r--r--java/src/com/android/intentresolver/widget/ChooserActionRow.kt81
-rw-r--r--java/src/com/android/intentresolver/widget/ImagePreviewView.kt178
-rw-r--r--java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java1280
-rw-r--r--java/src/com/android/intentresolver/widget/RoundedRectImageView.java132
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableActionRow.kt130
54 files changed, 8707 insertions, 4543 deletions
diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
index 4f6c0bf1..17dbb8f2 100644
--- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
@@ -16,27 +16,25 @@
package com.android.intentresolver;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.AppGlobals;
-import android.app.admin.DevicePolicyEventLogger;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.AsyncTask;
import android.os.Trace;
import android.os.UserHandle;
-import android.os.UserManager;
-import android.stats.devicepolicy.DevicePolicyEnums;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.PagerAdapter;
-import com.android.internal.widget.ViewPager;
import java.util.HashSet;
import java.util.List;
@@ -59,73 +57,32 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
private final Context mContext;
private int mCurrentPage;
private OnProfileSelectedListener mOnProfileSelectedListener;
- private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+
private Set<Integer> mLoadedPages;
- private final UserHandle mPersonalProfileUserHandle;
+ private final EmptyStateProvider mEmptyStateProvider;
private final UserHandle mWorkProfileUserHandle;
- private Injector mInjector;
- private boolean mIsWaitingToEnableWorkProfile;
+ private final QuietModeManager mQuietModeManager;
AbstractMultiProfilePagerAdapter(Context context, int currentPage,
- UserHandle personalProfileUserHandle,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
UserHandle workProfileUserHandle) {
mContext = Objects.requireNonNull(context);
mCurrentPage = currentPage;
mLoadedPages = new HashSet<>();
- mPersonalProfileUserHandle = personalProfileUserHandle;
mWorkProfileUserHandle = workProfileUserHandle;
- UserManager userManager = context.getSystemService(UserManager.class);
- mInjector = new Injector() {
- @Override
- public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId,
- int targetUserId) {
- return AbstractMultiProfilePagerAdapter.this
- .hasCrossProfileIntents(intents, sourceUserId, targetUserId);
- }
-
- @Override
- public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
- return userManager.isQuietModeEnabled(workProfileUserHandle);
- }
-
- @Override
- public void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle) {
- AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
- userManager.requestQuietModeEnabled(enabled, workProfileUserHandle);
- });
- mIsWaitingToEnableWorkProfile = true;
- }
- };
- }
-
- protected void markWorkProfileEnabledBroadcastReceived() {
- mIsWaitingToEnableWorkProfile = false;
- }
-
- protected boolean isWaitingToEnableWorkProfile() {
- return mIsWaitingToEnableWorkProfile;
- }
-
- /**
- * Overrides the default {@link Injector} for testing purposes.
- */
- @VisibleForTesting
- public void setInjector(Injector injector) {
- mInjector = injector;
+ mEmptyStateProvider = emptyStateProvider;
+ mQuietModeManager = quietModeManager;
}
- protected boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
- return mInjector.isQuietModeEnabled(workProfileUserHandle);
+ private boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+ return mQuietModeManager.isQuietModeEnabled(workProfileUserHandle);
}
void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
mOnProfileSelectedListener = listener;
}
- void setOnSwitchOnWorkSelectedListener(OnSwitchOnWorkSelectedListener listener) {
- mOnSwitchOnWorkSelectedListener = listener;
- }
-
Context getContext() {
return mContext;
}
@@ -191,7 +148,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
@VisibleForTesting
public UserHandle getCurrentUserHandle() {
- return getActiveListAdapter().mResolverListController.getUserHandle();
+ return getActiveListAdapter().getUserHandle();
}
@Override
@@ -216,6 +173,10 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
*/
abstract ProfileDescriptor getItem(int pageIndex);
+ protected ViewGroup getEmptyStateView(int pageIndex) {
+ return getItem(pageIndex).getEmptyStateView();
+ }
+
/**
* Returns the number of {@link ProfileDescriptor} objects.
* <p>For a normal consumer device with only one user returns <code>1</code>.
@@ -279,8 +240,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
abstract @Nullable ViewGroup getInactiveAdapterView();
- abstract String getMetricsCategory();
-
/**
* Rebuilds the tab that is currently visible to the user.
* <p>Returns {@code true} if rebuild has completed.
@@ -308,7 +267,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
private int userHandleToPageIndex(UserHandle userHandle) {
- if (userHandle.equals(getPersonalListAdapter().mResolverListController.getUserHandle())) {
+ if (userHandle.equals(getPersonalListAdapter().getUserHandle())) {
return PROFILE_PERSONAL;
} else {
return PROFILE_WORK;
@@ -316,41 +275,18 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) {
- if (shouldShowNoCrossProfileIntentsEmptyState(activeListAdapter)) {
+ if (shouldSkipRebuild(activeListAdapter)) {
activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
return false;
}
return activeListAdapter.rebuildList(doPostProcessing);
}
- private boolean shouldShowNoCrossProfileIntentsEmptyState(
- ResolverListAdapter activeListAdapter) {
- UserHandle listUserHandle = activeListAdapter.getUserHandle();
- return UserHandle.myUserId() != listUserHandle.getIdentifier()
- && allowShowNoCrossProfileIntentsEmptyState()
- && !mInjector.hasCrossProfileIntents(activeListAdapter.getIntents(),
- UserHandle.myUserId(), listUserHandle.getIdentifier());
- }
-
- boolean allowShowNoCrossProfileIntentsEmptyState() {
- return true;
+ private boolean shouldSkipRebuild(ResolverListAdapter activeListAdapter) {
+ EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);
+ return emptyState != null && emptyState.shouldSkipDataRebuild();
}
- protected abstract void showWorkProfileOffEmptyState(
- ResolverListAdapter activeListAdapter, View.OnClickListener listener);
-
- protected abstract void showNoPersonalToWorkIntentsEmptyState(
- ResolverListAdapter activeListAdapter);
-
- protected abstract void showNoPersonalAppsAvailableEmptyState(
- ResolverListAdapter activeListAdapter);
-
- protected abstract void showNoWorkAppsAvailableEmptyState(
- ResolverListAdapter activeListAdapter);
-
- protected abstract void showNoWorkToPersonalIntentsEmptyState(
- ResolverListAdapter activeListAdapter);
-
/**
* The empty state screens are shown according to their priority:
* <ol>
@@ -365,103 +301,88 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* anyway.
*/
void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) {
- if (maybeShowNoCrossProfileIntentsEmptyState(listAdapter)) {
+ final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
+
+ if (emptyState == null) {
return;
}
- if (maybeShowWorkProfileOffEmptyState(listAdapter)) {
- return;
+
+ emptyState.onEmptyStateShown();
+
+ View.OnClickListener clickListener = null;
+
+ if (emptyState.getButtonClickListener() != null) {
+ clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
+ ProfileDescriptor descriptor = getItem(
+ userHandleToPageIndex(listAdapter.getUserHandle()));
+ AbstractMultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView());
+ });
}
- maybeShowNoAppsAvailableEmptyState(listAdapter);
+
+ showEmptyState(listAdapter, emptyState, clickListener);
}
- private boolean maybeShowNoCrossProfileIntentsEmptyState(ResolverListAdapter listAdapter) {
- if (!shouldShowNoCrossProfileIntentsEmptyState(listAdapter)) {
- return false;
- }
- if (listAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) {
- DevicePolicyEventLogger.createEvent(
- DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL)
- .setStrings(getMetricsCategory())
- .write();
- showNoWorkToPersonalIntentsEmptyState(listAdapter);
- } else {
- DevicePolicyEventLogger.createEvent(
- DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK)
- .setStrings(getMetricsCategory())
- .write();
- showNoPersonalToWorkIntentsEmptyState(listAdapter);
+ /**
+ * Class to get user id of the current process
+ */
+ public static class MyUserIdProvider {
+ /**
+ * @return user id of the current process
+ */
+ public int getMyUserId() {
+ return UserHandle.myUserId();
}
- return true;
}
/**
- * Returns {@code true} if the work profile off empty state screen is shown.
+ * Utility class to check if there are cross profile intents, it is in a separate class so
+ * it could be mocked in tests
*/
- private boolean maybeShowWorkProfileOffEmptyState(ResolverListAdapter listAdapter) {
- UserHandle listUserHandle = listAdapter.getUserHandle();
- if (!listUserHandle.equals(mWorkProfileUserHandle)
- || !mInjector.isQuietModeEnabled(mWorkProfileUserHandle)
- || listAdapter.getCount() == 0) {
- return false;
- }
- DevicePolicyEventLogger
- .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED)
- .setStrings(getMetricsCategory())
- .write();
- showWorkProfileOffEmptyState(listAdapter,
- v -> {
- ProfileDescriptor descriptor = getItem(
- userHandleToPageIndex(listAdapter.getUserHandle()));
- showSpinner(descriptor.getEmptyStateView());
- if (mOnSwitchOnWorkSelectedListener != null) {
- mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
- }
- mInjector.requestQuietModeEnabled(false, mWorkProfileUserHandle);
- });
- return true;
- }
-
- private void maybeShowNoAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
- UserHandle listUserHandle = listAdapter.getUserHandle();
- if (mWorkProfileUserHandle != null
- && (UserHandle.myUserId() == listUserHandle.getIdentifier()
- || !hasAppsInOtherProfile(listAdapter))) {
- DevicePolicyEventLogger.createEvent(
- DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED)
- .setStrings(getMetricsCategory())
- .setBoolean(/*isPersonalProfile*/ listUserHandle == mPersonalProfileUserHandle)
- .write();
- if (listUserHandle == mPersonalProfileUserHandle) {
- showNoPersonalAppsAvailableEmptyState(listAdapter);
- } else {
- showNoWorkAppsAvailableEmptyState(listAdapter);
- }
- } else if (mWorkProfileUserHandle == null) {
- showConsumerUserNoAppsAvailableEmptyState(listAdapter);
+ public static class CrossProfileIntentsChecker {
+
+ private final ContentResolver mContentResolver;
+
+ public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
}
- }
- protected void showEmptyState(ResolverListAdapter activeListAdapter, String title,
- String subtitle) {
- showEmptyState(activeListAdapter, title, subtitle, /* buttonOnClick */ null);
+ /**
+ * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
+ * from {@code source} (user id) to {@code target} (user id).
+ */
+ public boolean hasCrossProfileIntents(List<Intent> intents, @UserIdInt int source,
+ @UserIdInt int target) {
+ IPackageManager packageManager = AppGlobals.getPackageManager();
+
+ return intents.stream().anyMatch(intent ->
+ null != IntentForwarderActivity.canForward(intent, source, target,
+ packageManager, mContentResolver));
+ }
}
- protected void showEmptyState(ResolverListAdapter activeListAdapter,
- String title, String subtitle, View.OnClickListener buttonOnClick) {
+ protected void showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState,
+ View.OnClickListener buttonOnClick) {
ProfileDescriptor descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
ViewGroup emptyStateView = descriptor.getEmptyStateView();
- resetViewVisibilitiesForWorkProfileEmptyState(emptyStateView);
+ resetViewVisibilitiesForEmptyState(emptyStateView);
emptyStateView.setVisibility(View.VISIBLE);
View container = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_container);
setupContainerPadding(container);
TextView titleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title);
- titleView.setText(title);
+ String title = emptyState.getTitle();
+ if (title != null) {
+ titleView.setVisibility(View.VISIBLE);
+ titleView.setText(title);
+ } else {
+ titleView.setVisibility(View.GONE);
+ }
TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle);
+ String subtitle = emptyState.getSubtitle();
if (subtitle != null) {
subtitleView.setVisibility(View.VISIBLE);
subtitleView.setText(subtitle);
@@ -469,6 +390,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
subtitleView.setVisibility(View.GONE);
}
+ View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty);
+ defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
+
Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button);
button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
button.setOnClickListener(buttonOnClick);
@@ -482,22 +406,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
*/
protected void setupContainerPadding(View container) {}
- private void showConsumerUserNoAppsAvailableEmptyState(ResolverListAdapter activeListAdapter) {
- ProfileDescriptor descriptor = getItem(
- userHandleToPageIndex(activeListAdapter.getUserHandle()));
- descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
- View emptyStateView = descriptor.getEmptyStateView();
- resetViewVisibilitiesForConsumerUserEmptyState(emptyStateView);
- emptyStateView.setVisibility(View.VISIBLE);
-
- activeListAdapter.markTabLoaded();
- }
-
- private boolean isSpinnerShowing(View emptyStateView) {
- return emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).getVisibility()
- == View.VISIBLE;
- }
-
private void showSpinner(View emptyStateView) {
emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.INVISIBLE);
emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
@@ -505,7 +413,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
}
- private void resetViewVisibilitiesForWorkProfileEmptyState(View emptyStateView) {
+ private void resetViewVisibilitiesForEmptyState(View emptyStateView) {
emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.VISIBLE);
emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.VISIBLE);
emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
@@ -513,14 +421,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
}
- private void resetViewVisibilitiesForConsumerUserEmptyState(View emptyStateView) {
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.GONE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.GONE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.GONE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE);
- emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.VISIBLE);
- }
-
protected void showListView(ResolverListAdapter activeListAdapter) {
ProfileDescriptor descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
@@ -529,33 +429,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
emptyStateView.setVisibility(View.GONE);
}
- private boolean hasCrossProfileIntents(List<Intent> intents, int source, int target) {
- IPackageManager packageManager = AppGlobals.getPackageManager();
- ContentResolver contentResolver = mContext.getContentResolver();
- for (Intent intent : intents) {
- if (IntentForwarderActivity.canForward(intent, source, target, packageManager,
- contentResolver) != null) {
- return true;
- }
- }
- return false;
- }
-
- private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) {
- if (mWorkProfileUserHandle == null) {
- return false;
- }
- List<ResolverActivity.ResolvedComponentInfo> resolversForIntent =
- adapter.getResolversForUser(UserHandle.of(UserHandle.myUserId()));
- for (ResolverActivity.ResolvedComponentInfo info : resolversForIntent) {
- ResolveInfo resolveInfo = info.getResolveInfoAt(0);
- if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
- return true;
- }
- }
- return false;
- }
-
boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) {
int count = listAdapter.getUnfilteredCount();
return (count == 0 && listAdapter.getPlaceholderCount() == 0)
@@ -563,7 +436,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
&& isQuietModeEnabled(mWorkProfileUserHandle));
}
- protected class ProfileDescriptor {
+ protected static class ProfileDescriptor {
final ViewGroup rootView;
private final ViewGroup mEmptyStateView;
ProfileDescriptor(ViewGroup rootView) {
@@ -599,6 +472,99 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
/**
+ * Returns an empty state to show for the current profile page (tab) if necessary.
+ * This could be used e.g. to show a blocker on a tab if device management policy doesn't
+ * allow to use it or there are no apps available.
+ */
+ public interface EmptyStateProvider {
+ /**
+ * When a non-null empty state is returned the corresponding profile page will show
+ * this empty state
+ * @param resolverListAdapter the current adapter
+ */
+ @Nullable
+ default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ return null;
+ }
+ }
+
+ /**
+ * Empty state provider that combines multiple providers. Providers earlier in the list have
+ * priority, that is if there is a provider that returns non-null empty state then all further
+ * providers will be ignored.
+ */
+ public static class CompositeEmptyStateProvider implements EmptyStateProvider {
+
+ private final EmptyStateProvider[] mProviders;
+
+ public CompositeEmptyStateProvider(EmptyStateProvider... providers) {
+ mProviders = providers;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ for (EmptyStateProvider provider : mProviders) {
+ EmptyState emptyState = provider.getEmptyState(resolverListAdapter);
+ if (emptyState != null) {
+ return emptyState;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Describes how the blocked empty state should look like for a profile tab
+ */
+ public interface EmptyState {
+ /**
+ * Title that will be shown on the empty state
+ */
+ @Nullable
+ default String getTitle() { return null; }
+
+ /**
+ * Subtitle that will be shown underneath the title on the empty state
+ */
+ @Nullable
+ default String getSubtitle() { return null; }
+
+ /**
+ * If non-null then a button will be shown and this listener will be called
+ * when the button is clicked
+ */
+ @Nullable
+ default ClickListener getButtonClickListener() { return null; }
+
+ /**
+ * If true then default text ('No apps can perform this action') and style for the empty
+ * state will be applied, title and subtitle will be ignored.
+ */
+ default boolean useDefaultEmptyView() { return false; }
+
+ /**
+ * Returns true if for this empty state we should skip rebuilding of the apps list
+ * for this tab.
+ */
+ default boolean shouldSkipDataRebuild() { return false; }
+
+ /**
+ * Called when empty state is shown, could be used e.g. to track analytics events
+ */
+ default void onEmptyStateShown() {}
+
+ interface ClickListener {
+ void onClick(TabControl currentTab);
+ }
+
+ interface TabControl {
+ void showSpinner();
+ }
+ }
+
+
+ /**
* Listener for when the user switches on the work profile from the work tab.
*/
interface OnSwitchOnWorkSelectedListener {
@@ -611,14 +577,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
/**
* Describes an injector to be used for cross profile functionality. Overridable for testing.
*/
- @VisibleForTesting
- public interface Injector {
- /**
- * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
- * from {@code sourceUserId} to {@code targetUserId}.
- */
- boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId, int targetUserId);
-
+ public interface QuietModeManager {
/**
* Returns whether the given profile is in quiet mode or not.
*/
@@ -628,5 +587,15 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* Enables or disables quiet mode for a managed profile.
*/
void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle);
+
+ /**
+ * Should be called when the work profile enabled broadcast received
+ */
+ void markWorkProfileEnabledBroadcastReceived();
+
+ /**
+ * Returns true if enabling of work profile is in progress
+ */
+ boolean isWaitingToEnableWorkProfile();
}
}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 14d77427..ceab62b2 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -16,29 +16,26 @@
package com.android.intentresolver;
-import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
-import android.app.SharedElementCallback;
-import android.app.prediction.AppPredictionContext;
-import android.app.prediction.AppPredictionManager;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
-import android.compat.annotation.UnsupportedAppUsage;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
@@ -50,96 +47,75 @@ import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
-import android.content.pm.ShortcutManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
-import android.database.DataSetObserver;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
-import android.metrics.LogMaker;
import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
-import android.os.Message;
import android.os.Parcelable;
import android.os.PatternMatcher;
import android.os.ResultReceiver;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
import android.provider.DeviceConfig;
-import android.provider.DocumentsContract;
-import android.provider.Downloads;
-import android.provider.OpenableColumns;
import android.provider.Settings;
import android.service.chooser.ChooserTarget;
import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.HashedStringCache;
import android.util.Log;
-import android.util.PluralsMessageFormatter;
import android.util.Size;
import android.util.Slog;
-import android.view.LayoutInflater;
+import android.util.SparseArray;
import android.view.View;
-import android.view.View.MeasureSpec;
-import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
-import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
-import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.Space;
import android.widget.TextView;
-import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
-import com.android.intentresolver.ResolverListAdapter.ViewHolder;
-import com.android.intentresolver.chooser.ChooserTargetInfo;
+import androidx.annotation.MainThread;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
-import com.android.intentresolver.chooser.NotSelectableTargetInfo;
-import com.android.intentresolver.chooser.SelectableTargetInfo;
-import com.android.intentresolver.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator;
import com.android.intentresolver.chooser.TargetInfo;
-
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.grid.DirectShareViewHolder;
+import com.android.intentresolver.model.AbstractResolverComparator;
+import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
+import com.android.intentresolver.shortcuts.AppPredictorFactory;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.content.PackageMonitor;
-import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FrameworkStatsLog;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.RecyclerView;
-import com.android.internal.widget.ResolverDrawerLayout;
-import com.android.internal.widget.ViewPager;
-
-import com.google.android.collect.Lists;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.net.URISyntaxException;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
@@ -148,25 +124,19 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.function.Supplier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
/**
* The Chooser Activity handles intent resolution specifically for sharing intents -
- * for example, those generated by @see android.content.Intent#createChooser(Intent, CharSequence).
+ * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
*
*/
public class ChooserActivity extends ResolverActivity implements
- ChooserListAdapter.ChooserListCommunicator,
- SelectableTargetInfoCommunicator {
+ ResolverListAdapter.ResolverListCommunicator {
private static final String TAG = "ChooserActivity";
- private AppPredictor mPersonalAppPredictor;
- private AppPredictor mWorkAppPredictor;
- private boolean mShouldDisplayLandscape;
-
- @UnsupportedAppUsage
- public ChooserActivity() {
- }
/**
* Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself
* in onStop when launched in a new task. If this extra is set to true, we do not finish
@@ -175,7 +145,6 @@ public class ChooserActivity extends ResolverActivity implements
public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
= "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
-
/**
* Transition name for the first image preview.
* To be used for shared element transition into this activity.
@@ -190,44 +159,31 @@ public class ChooserActivity extends ResolverActivity implements
private static final boolean DEBUG = true;
- private static final boolean USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES = true;
- // TODO(b/123088566) Share these in a better way.
- private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share";
public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
- public static final String CHOOSER_TARGET = "chooser_target";
private static final String SHORTCUT_TARGET = "shortcut_target";
- private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20;
- public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter";
- private static final String SHARED_TEXT_KEY = "shared_text";
private static final String PLURALS_COUNT = "count";
private static final String PLURALS_FILE_NAME = "file_name";
private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
- private boolean mIsAppPredictorComponentAvailable;
- private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache;
- private Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache;
+ // TODO: these data structures are for one-time use in shuttling data from where they're
+ // populated in `ShortcutToChooserTargetConverter` to where they're consumed in
+ // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`.
+ // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their
+ // intermediate data, and then these members can be removed.
+ private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
+ private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();
public static final int TARGET_TYPE_DEFAULT = 0;
public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
- public static final int SELECTION_TYPE_SERVICE = 1;
- public static final int SELECTION_TYPE_APP = 2;
- public static final int SELECTION_TYPE_STANDARD = 3;
- public static final int SELECTION_TYPE_COPY = 4;
- public static final int SELECTION_TYPE_NEARBY = 5;
- public static final int SELECTION_TYPE_EDIT = 6;
-
private static final int SCROLL_STATUS_IDLE = 0;
private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
- // statsd logger wrapper
- protected ChooserActivityLogger mChooserActivityLogger;
-
@IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
TARGET_TYPE_DEFAULT,
TARGET_TYPE_CHOOSER_TARGET,
@@ -237,294 +193,68 @@ public class ChooserActivity extends ResolverActivity implements
@Retention(RetentionPolicy.SOURCE)
public @interface ShareTargetType {}
- /**
- * The transition time between placeholders for direct share to a message
- * indicating that non are available.
- */
- private static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200;
-
- private static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f;
+ public static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f;
private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
- private int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
+ private final int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
DEFAULT_SALT_EXPIRATION_DAYS);
- private static final boolean DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP = false;
- private boolean mIsNearbyShareFirstTargetInRankedApp =
- DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP,
- DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP);
-
- private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 0;
-
private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
- @VisibleForTesting
- int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.SHARESHEET_LIST_VIEW_UPDATE_DELAY,
- DEFAULT_LIST_VIEW_UPDATE_DELAY_MS);
+ /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
+ * only assignment there, and expect it to be ready by the time we ever use it --
+ * someday if we move all the usage to a component with a narrower lifecycle (something that
+ * matches our Activity's create/destroy lifecycle, not its Java object lifecycle) then we
+ * should be able to make this assignment as "final."
+ */
+ @Nullable
+ private ChooserRequestParameters mChooserRequest;
- private Bundle mReplacementExtras;
- private IntentSender mChosenComponentSender;
- private IntentSender mRefinementIntentSender;
- private RefinementResultReceiver mRefinementResultReceiver;
- private ChooserTarget[] mCallerChooserTargets;
- private ComponentName[] mFilteredComponentNames;
+ private boolean mShouldDisplayLandscape;
+ // statsd logger wrapper
+ protected ChooserActivityLogger mChooserActivityLogger;
- private Intent mReferrerFillInIntent;
+ @Nullable
+ private RefinementResultReceiver mRefinementResultReceiver;
private long mChooserShownTime;
protected boolean mIsSuccessfullySelected;
- private long mQueriedSharingShortcutsTimeMs;
-
private int mCurrAvailableWidth = 0;
private Insets mLastAppliedInsets = null;
private int mLastNumberOfChildren = -1;
private int mMaxTargetsPerRow = 1;
- private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment";
-
private static final int MAX_LOG_RANK_POSITION = 12;
+ // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters.
private static final int MAX_EXTRA_INITIAL_INTENTS = 2;
private static final int MAX_EXTRA_CHOOSER_TARGETS = 2;
private SharedPreferences mPinnedSharedPrefs;
private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings";
- @Retention(SOURCE)
- @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT})
- private @interface ContentPreviewType {
- }
+ private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
- // Starting at 1 since 0 is considered "undefined" for some of the database transformations
- // of tron logs.
- protected static final int CONTENT_PREVIEW_IMAGE = 1;
- protected static final int CONTENT_PREVIEW_FILE = 2;
- protected static final int CONTENT_PREVIEW_TEXT = 3;
- protected MetricsLogger mMetricsLogger;
+ @Nullable
+ private ChooserContentPreviewCoordinator mPreviewCoordinator;
- private ContentPreviewCoordinator mPreviewCoord;
private int mScrollStatus = SCROLL_STATUS_IDLE;
@VisibleForTesting
protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate =
- new EnterTransitionAnimationDelegate();
-
- private boolean mRemoveSharedElements = false;
+ new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout);
private View mContentView = null;
- private class ContentPreviewCoordinator {
- private static final int IMAGE_FADE_IN_MILLIS = 150;
- private static final int IMAGE_LOAD_TIMEOUT = 1;
- private static final int IMAGE_LOAD_INTO_VIEW = 2;
-
- private final int mImageLoadTimeoutMillis =
- getResources().getInteger(R.integer.config_shortAnimTime);
-
- private final View mParentView;
- private boolean mHideParentOnFail;
- private boolean mAtLeastOneLoaded = false;
-
- class LoadUriTask {
- public final Uri mUri;
- public final int mImageResourceId;
- public final int mExtraCount;
- public final Bitmap mBmp;
-
- LoadUriTask(int imageResourceId, Uri uri, int extraCount, Bitmap bmp) {
- this.mImageResourceId = imageResourceId;
- this.mUri = uri;
- this.mExtraCount = extraCount;
- this.mBmp = bmp;
- }
- }
-
- // If at least one image loads within the timeout period, allow other
- // loads to continue. Otherwise terminate and optionally hide
- // the parent area
- private final Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case IMAGE_LOAD_TIMEOUT:
- maybeHideContentPreview();
- break;
-
- case IMAGE_LOAD_INTO_VIEW:
- if (isFinishing()) break;
-
- LoadUriTask task = (LoadUriTask) msg.obj;
- RoundedRectImageView imageView = mParentView.findViewById(
- task.mImageResourceId);
- if (task.mBmp == null) {
- imageView.setVisibility(View.GONE);
- maybeHideContentPreview();
- return;
- }
-
- mAtLeastOneLoaded = true;
- imageView.setVisibility(View.VISIBLE);
- imageView.setAlpha(0.0f);
- imageView.setImageBitmap(task.mBmp);
-
- ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f,
- 1.0f);
- fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
- fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
- fadeAnim.start();
-
- if (task.mExtraCount > 0) {
- imageView.setExtraImageCount(task.mExtraCount);
- }
-
- setupPreDrawForSharedElementTransition(imageView);
- }
- }
- };
-
- private void setupPreDrawForSharedElementTransition(View v) {
- v.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- v.getViewTreeObserver().removeOnPreDrawListener(this);
-
- if (!mRemoveSharedElements && isActivityTransitionRunning()) {
- // Disable the window animations as it interferes with the
- // transition animation.
- getWindow().setWindowAnimations(0);
- }
- mEnterTransitionAnimationDelegate.markImagePreviewReady();
- return true;
- }
- });
- }
+ private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
- ContentPreviewCoordinator(View parentView, boolean hideParentOnFail) {
- super();
-
- this.mParentView = parentView;
- this.mHideParentOnFail = hideParentOnFail;
- }
-
- private void loadUriIntoView(final int imageResourceId, final Uri uri,
- final int extraImages) {
- mHandler.sendEmptyMessageDelayed(IMAGE_LOAD_TIMEOUT, mImageLoadTimeoutMillis);
-
- AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
- int size = getResources().getDimensionPixelSize(
- R.dimen.chooser_preview_image_max_dimen);
- final Bitmap bmp = loadThumbnail(uri, new Size(size, size));
- final Message msg = Message.obtain();
- msg.what = IMAGE_LOAD_INTO_VIEW;
- msg.obj = new LoadUriTask(imageResourceId, uri, extraImages, bmp);
- mHandler.sendMessage(msg);
- });
- }
-
- private void cancelLoads() {
- mHandler.removeMessages(IMAGE_LOAD_INTO_VIEW);
- mHandler.removeMessages(IMAGE_LOAD_TIMEOUT);
- }
-
- private void maybeHideContentPreview() {
- if (!mAtLeastOneLoaded) {
- if (mHideParentOnFail) {
- Log.i(TAG, "Hiding image preview area. Timed out waiting for preview to load"
- + " within " + mImageLoadTimeoutMillis + "ms.");
- collapseParentView();
- if (shouldShowTabs()) {
- hideStickyContentPreview();
- } else if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
- mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()
- .hideContentPreview();
- }
- mHideParentOnFail = false;
- }
- mRemoveSharedElements = true;
- mEnterTransitionAnimationDelegate.markImagePreviewReady();
- }
- }
-
- private void collapseParentView() {
- // This will effectively hide the content preview row by forcing the height
- // to zero. It is faster than forcing a relayout of the listview
- final View v = mParentView;
- int widthSpec = MeasureSpec.makeMeasureSpec(v.getWidth(), MeasureSpec.EXACTLY);
- int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY);
- v.measure(widthSpec, heightSpec);
- v.getLayoutParams().height = 0;
- v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getTop());
- v.invalidate();
- }
- }
-
- private final ChooserHandler mChooserHandler = new ChooserHandler();
-
- private class ChooserHandler extends Handler {
- private static final int LIST_VIEW_UPDATE_MESSAGE = 6;
- private static final int SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS = 7;
-
- private void removeAllMessages() {
- removeMessages(LIST_VIEW_UPDATE_MESSAGE);
- removeMessages(SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS);
- }
-
- @Override
- public void handleMessage(Message msg) {
- if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null || isDestroyed()) {
- return;
- }
-
- switch (msg.what) {
- case LIST_VIEW_UPDATE_MESSAGE:
- if (DEBUG) {
- Log.d(TAG, "LIST_VIEW_UPDATE_MESSAGE; ");
- }
-
- UserHandle userHandle = (UserHandle) msg.obj;
- mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle)
- .refreshListView();
- break;
-
- case SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS:
- if (DEBUG) Log.d(TAG, "SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS");
- final ServiceResultInfo[] resultInfos = (ServiceResultInfo[]) msg.obj;
- for (ServiceResultInfo resultInfo : resultInfos) {
- if (resultInfo.resultTargets != null) {
- ChooserListAdapter adapterForUserHandle =
- mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(
- resultInfo.userHandle);
- if (adapterForUserHandle != null) {
- adapterForUserHandle.addServiceResults(
- resultInfo.originalTarget,
- resultInfo.resultTargets, msg.arg1,
- mDirectShareShortcutInfoCache);
- }
- }
- }
-
- logDirectShareTargetReceived(
- MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER);
- sendVoiceChoicesIfNeeded();
- getChooserActivityLogger().logSharesheetDirectLoadComplete();
-
- mChooserMultiProfilePagerAdapter.getActiveListAdapter()
- .completeServiceTargetLoading();
- break;
-
- default:
- super.handleMessage(msg);
- }
- }
- };
+ public ChooserActivity() {}
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -532,168 +262,59 @@ public class ChooserActivity extends ResolverActivity implements
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
getChooserActivityLogger().logSharesheetTriggered();
- // This is the only place this value is being set. Effectively final.
- mIsAppPredictorComponentAvailable = isAppPredictionServiceAvailable();
-
- mIsSuccessfullySelected = false;
- Intent intent = getIntent();
- Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT);
- if (targetParcelable instanceof Uri) {
- try {
- targetParcelable = Intent.parseUri(targetParcelable.toString(),
- Intent.URI_INTENT_SCHEME);
- } catch (URISyntaxException ex) {
- // doesn't parse as an intent; let the next test fail and error out
- }
- }
- if (!(targetParcelable instanceof Intent)) {
- Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable);
+ try {
+ mChooserRequest = new ChooserRequestParameters(
+ getIntent(), getReferrer(), getNearbySharingComponent());
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
finish();
- super.onCreate(null);
+ super_onCreate(null);
return;
}
- Intent target = (Intent) targetParcelable;
- if (target != null) {
- modifyTargetIntent(target);
- }
- Parcelable[] targetsParcelable
- = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS);
- if (targetsParcelable != null) {
- final boolean offset = target == null;
- Intent[] additionalTargets =
- new Intent[offset ? targetsParcelable.length - 1 : targetsParcelable.length];
- for (int i = 0; i < targetsParcelable.length; i++) {
- if (!(targetsParcelable[i] instanceof Intent)) {
- Log.w(TAG, "EXTRA_ALTERNATE_INTENTS array entry #" + i + " is not an Intent: "
- + targetsParcelable[i]);
- finish();
- super.onCreate(null);
- return;
- }
- final Intent additionalTarget = (Intent) targetsParcelable[i];
- if (i == 0 && target == null) {
- target = additionalTarget;
- modifyTargetIntent(target);
- } else {
- additionalTargets[offset ? i - 1 : i] = additionalTarget;
- modifyTargetIntent(additionalTarget);
- }
- }
- setAdditionalTargets(additionalTargets);
- }
-
- mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS);
-
- // Do not allow the title to be changed when sharing content
- CharSequence title = null;
- if (target != null) {
- if (!isSendAction(target)) {
- title = intent.getCharSequenceExtra(Intent.EXTRA_TITLE);
- } else {
- Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a"
- + " preview title by using EXTRA_TITLE property of the wrapped"
- + " EXTRA_INTENT.");
- }
- }
-
- int defaultTitleRes = 0;
- if (title == null) {
- defaultTitleRes = com.android.internal.R.string.chooseActivity;
- }
-
- Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS);
- Intent[] initialIntents = null;
- if (pa != null) {
- int count = Math.min(pa.length, MAX_EXTRA_INITIAL_INTENTS);
- initialIntents = new Intent[count];
- for (int i = 0; i < count; i++) {
- if (!(pa[i] instanceof Intent)) {
- Log.w(TAG, "Initial intent #" + i + " not an Intent: " + pa[i]);
- finish();
- super.onCreate(null);
- return;
- }
- final Intent in = (Intent) pa[i];
- modifyTargetIntent(in);
- initialIntents[i] = in;
- }
- }
- mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, getReferrer());
+ setAdditionalTargets(mChooserRequest.getAdditionalTargets());
- mChosenComponentSender = intent.getParcelableExtra(
- Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
- mRefinementIntentSender = intent.getParcelableExtra(
- Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
setSafeForwardingMode(true);
mPinnedSharedPrefs = getPinnedSharedPrefs(this);
- pa = intent.getParcelableArrayExtra(Intent.EXTRA_EXCLUDE_COMPONENTS);
-
-
- // Exclude out Nearby from main list if chip is present, to avoid duplication
- ComponentName nearbySharingComponent = getNearbySharingComponent();
- boolean shouldFilterNearby = !shouldNearbyShareBeFirstInRankedRow()
- && nearbySharingComponent != null;
-
- if (pa != null) {
- ComponentName[] names = new ComponentName[pa.length + (shouldFilterNearby ? 1 : 0)];
- for (int i = 0; i < pa.length; i++) {
- if (!(pa[i] instanceof ComponentName)) {
- Log.w(TAG, "Filtered component #" + i + " not a ComponentName: " + pa[i]);
- names = null;
- break;
- }
- names[i] = (ComponentName) pa[i];
- }
- if (shouldFilterNearby) {
- names[names.length - 1] = nearbySharingComponent;
- }
-
- mFilteredComponentNames = names;
- } else if (shouldFilterNearby) {
- mFilteredComponentNames = new ComponentName[1];
- mFilteredComponentNames[0] = nearbySharingComponent;
- }
-
- pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS);
- if (pa != null) {
- int count = Math.min(pa.length, MAX_EXTRA_CHOOSER_TARGETS);
- ChooserTarget[] targets = new ChooserTarget[count];
- for (int i = 0; i < count; i++) {
- if (!(pa[i] instanceof ChooserTarget)) {
- Log.w(TAG, "Chooser target #" + i + " not a ChooserTarget: " + pa[i]);
- targets = null;
- break;
- }
- targets[i] = (ChooserTarget) pa[i];
- }
- mCallerChooserTargets = targets;
- }
-
mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
mShouldDisplayLandscape =
shouldDisplayLandscape(getResources().getConfiguration().orientation);
- setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false));
- super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents,
- null, false);
+ setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
+
+ createProfileRecords(
+ new AppPredictorFactory(
+ getApplicationContext(),
+ mChooserRequest.getSharedText(),
+ mChooserRequest.getTargetIntentFilter()),
+ mChooserRequest.getTargetIntentFilter());
+
+ mPreviewCoordinator = new ChooserContentPreviewCoordinator(
+ mBackgroundThreadPoolExecutor,
+ this,
+ () -> mEnterTransitionAnimationDelegate.markImagePreviewReady(false));
+
+ super.onCreate(
+ savedInstanceState,
+ mChooserRequest.getTargetIntent(),
+ mChooserRequest.getTitle(),
+ mChooserRequest.getDefaultTitleResource(),
+ mChooserRequest.getInitialIntents(),
+ /* rList: List<ResolveInfo> = */ null,
+ /* supportsAlwaysUseOption = */ false);
mChooserShownTime = System.currentTimeMillis();
final long systemCost = mChooserShownTime - intentReceivedTime;
-
- getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)
- .setSubtype(isWorkProfile() ? MetricsEvent.MANAGED_PROFILE :
- MetricsEvent.PARENT_PROFILE)
- .addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, target.getType())
- .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost));
+ getChooserActivityLogger().logChooserActivityShown(
+ isWorkProfile(), mChooserRequest.getTargetType(), systemCost);
if (mResolverDrawerLayout != null) {
mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
// expand/shrink direct share 4 -> 8 viewgroup
- if (isSendAction(target)) {
+ if (mChooserRequest.isSendActionTarget()) {
mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll);
}
@@ -722,26 +343,16 @@ public class ChooserActivity extends ResolverActivity implements
getChooserActivityLogger().logShareStarted(
FrameworkStatsLog.SHARESHEET_STARTED,
getReferrerPackageName(),
- target.getType(),
- mCallerChooserTargets == null ? 0 : mCallerChooserTargets.length,
- initialIntents == null ? 0 : initialIntents.length,
+ mChooserRequest.getTargetType(),
+ mChooserRequest.getCallerChooserTargets().size(),
+ (mChooserRequest.getInitialIntents() == null)
+ ? 0 : mChooserRequest.getInitialIntents().length,
isWorkProfile(),
- findPreferredContentPreview(getTargetIntent(), getContentResolver()),
- target.getAction()
+ ChooserContentPreviewUi.findPreferredContentPreview(
+ getTargetIntent(), getContentResolver(), this::isImageType),
+ mChooserRequest.getTargetAction()
);
- mDirectShareShortcutInfoCache = new HashMap<>();
- setEnterSharedElementCallback(new SharedElementCallback() {
- @Override
- public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
- if (mRemoveSharedElements) {
- names.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
- sharedElements.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
- }
- super.onMapSharedElements(names, sharedElements);
- mRemoveSharedElements = false;
- }
- });
mEnterTransitionAnimationDelegate.postponeTransition();
}
@@ -750,52 +361,51 @@ public class ChooserActivity extends ResolverActivity implements
return R.style.Theme_DeviceDefault_Chooser;
}
- private AppPredictor setupAppPredictorForUser(UserHandle userHandle,
- AppPredictor.Callback appPredictorCallback) {
- AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(userHandle);
- if (appPredictor == null) {
- return null;
+ private void createProfileRecords(
+ AppPredictorFactory factory, IntentFilter targetIntentFilter) {
+ UserHandle mainUserHandle = getPersonalProfileUserHandle();
+ createProfileRecord(mainUserHandle, targetIntentFilter, factory);
+
+ UserHandle workUserHandle = getWorkProfileUserHandle();
+ if (workUserHandle != null) {
+ createProfileRecord(workUserHandle, targetIntentFilter, factory);
}
- mDirectShareAppTargetCache = new HashMap<>();
- appPredictor.registerPredictionUpdates(this.getMainExecutor(), appPredictorCallback);
- return appPredictor;
}
- private AppPredictor.Callback createAppPredictorCallback(
- ChooserListAdapter chooserListAdapter) {
- return resultList -> {
- if (isFinishing() || isDestroyed()) {
- return;
- }
- if (chooserListAdapter.getCount() == 0) {
- return;
- }
- if (resultList.isEmpty()
- && shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
- // APS may be disabled, so try querying targets ourselves.
- queryDirectShareTargets(chooserListAdapter, true);
- return;
- }
- final List<ShortcutManager.ShareShortcutInfo> shareShortcutInfos =
- new ArrayList<>();
+ private void createProfileRecord(
+ UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {
+ AppPredictor appPredictor = factory.create(userHandle);
+ ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
+ ? null
+ : createShortcutLoader(
+ getApplicationContext(),
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
+ mProfileRecords.put(
+ userHandle.getIdentifier(),
+ new ProfileRecord(appPredictor, shortcutLoader));
+ }
- List<AppTarget> shortcutResults = new ArrayList<>();
- for (AppTarget appTarget : resultList) {
- if (appTarget.getShortcutInfo() == null) {
- continue;
- }
- shortcutResults.add(appTarget);
- }
- resultList = shortcutResults;
- for (AppTarget appTarget : resultList) {
- shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo(
- appTarget.getShortcutInfo(),
- new ComponentName(
- appTarget.getPackageName(), appTarget.getClassName())));
- }
- sendShareShortcutInfoList(shareShortcutInfos, chooserListAdapter, resultList,
- chooserListAdapter.getUserHandle());
- };
+ @Nullable
+ private ProfileRecord getProfileRecord(UserHandle userHandle) {
+ return mProfileRecords.get(userHandle.getIdentifier(), null);
+ }
+
+ @VisibleForTesting
+ protected ShortcutLoader createShortcutLoader(
+ Context context,
+ AppPredictor appPredictor,
+ UserHandle userHandle,
+ IntentFilter targetIntentFilter,
+ Consumer<ShortcutLoader.Result> callback) {
+ return new ShortcutLoader(
+ context,
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ callback);
}
static SharedPreferences getPinnedSharedPrefs(Context context) {
@@ -829,6 +439,41 @@ public class ChooserActivity extends ResolverActivity implements
return mChooserMultiProfilePagerAdapter;
}
+ @Override
+ protected EmptyStateProvider createBlockerEmptyStateProvider() {
+ final boolean isSendAction = mChooserRequest.isSendActionTarget();
+
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */
+ isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL,
+ /* defaultSubtitleResource= */
+ isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation
+ : R.string.resolver_cant_access_personal_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
+
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */
+ isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK,
+ /* defaultSubtitleResource= */
+ isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation
+ : R.string.resolver_cant_access_work_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
+
+ return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(),
+ noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(), createMyUserIdProvider());
+ }
+
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
Intent[] initialIntents,
List<ResolveInfo> rList,
@@ -843,9 +488,10 @@ public class ChooserActivity extends ResolverActivity implements
return new ChooserMultiProfilePagerAdapter(
/* context */ this,
adapter,
- getPersonalProfileUserHandle(),
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ mQuietModeManager,
/* workProfileUserHandle= */ null,
- isSendAction(getTargetIntent()), mMaxTargetsPerRow);
+ mMaxTargetsPerRow);
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
@@ -871,10 +517,11 @@ public class ChooserActivity extends ResolverActivity implements
/* context */ this,
personalAdapter,
workAdapter,
+ createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()),
+ mQuietModeManager,
selectedProfile,
- getPersonalProfileUserHandle(),
getWorkProfileUserHandle(),
- isSendAction(getTargetIntent()), mMaxTargetsPerRow);
+ mMaxTargetsPerRow);
}
private int findSelectedProfile() {
@@ -891,19 +538,14 @@ public class ChooserActivity extends ResolverActivity implements
if (shouldShowStickyContentPreview()
|| mChooserMultiProfilePagerAdapter
.getCurrentRootAdapter().getSystemRowCount() != 0) {
- logActionShareWithPreview();
+ getChooserActivityLogger().logActionShareWithPreview(
+ ChooserContentPreviewUi.findPreferredContentPreview(
+ getTargetIntent(), getContentResolver(), this::isImageType));
}
return postRebuildListInternal(rebuildCompleted);
}
/**
- * Returns true if app prediction service is defined and the component exists on device.
- */
- private boolean isAppPredictionServiceAvailable() {
- return getPackageManager().getAppPredictionServicePackageName() != null;
- }
-
- /**
* Check if the profile currently used is a work profile.
* @return true if it is work profile, false if it is parent profile (or no work profile is
* set up)
@@ -949,7 +591,7 @@ public class ChooserActivity extends ResolverActivity implements
updateProfileViewButton();
}
- private void onCopyButtonClicked(View v) {
+ private void onCopyButtonClicked() {
Intent targetIntent = getTargetIntent();
if (targetIntent == null) {
finish();
@@ -987,15 +629,7 @@ public class ChooserActivity extends ResolverActivity implements
Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClipAsPackage(clipData, getReferrerPackageName());
- // Log share completion via copy
- LogMaker targetLogMaker = new LogMaker(
- MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET).setSubtype(1);
- getMetricsLogger().write(targetLogMaker);
- getChooserActivityLogger().logShareTargetSelected(
- SELECTION_TYPE_COPY,
- "",
- -1,
- false);
+ getChooserActivityLogger().logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
setResult(RESULT_OK);
finish();
@@ -1068,10 +702,59 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- private ViewGroup createContentPreviewView(ViewGroup parent) {
+ /**
+ * Create a view that will be shown in the content preview area
+ * @param parent reference to the parent container where the view should be attached to
+ * @return content preview view
+ */
+ protected ViewGroup createContentPreviewView(
+ ViewGroup parent,
+ ChooserContentPreviewUi.ContentPreviewCoordinator previewCoordinator) {
Intent targetIntent = getTargetIntent();
- int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
- return displayContentPreview(previewType, targetIntent, getLayoutInflater(), parent);
+ int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
+ targetIntent, getContentResolver(), this::isImageType);
+
+ ChooserContentPreviewUi.ActionFactory actionFactory =
+ new ChooserContentPreviewUi.ActionFactory() {
+ @Override
+ public ActionRow.Action createCopyButton() {
+ return ChooserActivity.this.createCopyAction();
+ }
+
+ @Nullable
+ @Override
+ public ActionRow.Action createEditButton() {
+ return ChooserActivity.this.createEditAction(targetIntent);
+ }
+
+ @Nullable
+ @Override
+ public ActionRow.Action createNearbyButton() {
+ return ChooserActivity.this.createNearbyAction(targetIntent);
+ }
+ };
+
+ ViewGroup layout = ChooserContentPreviewUi.displayContentPreview(
+ previewType,
+ targetIntent,
+ getResources(),
+ getLayoutInflater(),
+ actionFactory,
+ R.layout.chooser_action_row,
+ parent,
+ previewCoordinator,
+ mEnterTransitionAnimationDelegate::markImagePreviewReady,
+ getContentResolver(),
+ this::isImageType);
+
+ if (layout != null) {
+ adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
+ }
+ if (previewType != ChooserContentPreviewUi.CONTENT_PREVIEW_IMAGE) {
+ mEnterTransitionAnimationDelegate.markImagePreviewReady(false);
+ }
+
+ return layout;
}
@VisibleForTesting
@@ -1108,6 +791,19 @@ public class ChooserActivity extends ResolverActivity implements
resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
resolveIntent.setComponent(cn);
resolveIntent.setAction(Intent.ACTION_EDIT);
+ String originalAction = originalIntent.getAction();
+ if (Intent.ACTION_SEND.equals(originalAction)) {
+ if (resolveIntent.getData() == null) {
+ Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ String mimeType = getContentResolver().getType(uri);
+ resolveIntent.setDataAndType(uri, mimeType);
+ }
+ }
+ } else {
+ Log.e(TAG, originalAction + " is not supported.");
+ return null;
+ }
final ResolveInfo ri = getPackageManager().resolveActivity(
resolveIntent, PackageManager.GET_META_DATA);
if (ri == null || ri.activityInfo == null) {
@@ -1116,9 +812,15 @@ public class ChooserActivity extends ResolverActivity implements
return null;
}
- final DisplayResolveInfo dri = new DisplayResolveInfo(
- originalIntent, ri, getString(com.android.internal.R.string.screenshot_edit), "", resolveIntent, null);
- dri.setDisplayIcon(getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ri,
+ getString(com.android.internal.R.string.screenshot_edit),
+ "",
+ resolveIntent,
+ null);
+ dri.getDisplayIconHolder().setDisplayIcon(
+ getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
return dri;
}
@@ -1160,70 +862,55 @@ public class ChooserActivity extends ResolverActivity implements
icon = ri.loadIcon(getPackageManager());
}
- final DisplayResolveInfo dri = new DisplayResolveInfo(
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
originalIntent, ri, name, "", resolveIntent, null);
- dri.setDisplayIcon(icon);
+ dri.getDisplayIconHolder().setDisplayIcon(icon);
return dri;
}
- private Button createActionButton(Drawable icon, CharSequence title, View.OnClickListener r) {
- Button b = (Button) LayoutInflater.from(this).inflate(R.layout.chooser_action_button, null);
- if (icon != null) {
- final int size = getResources()
- .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size);
- icon.setBounds(0, 0, size, size);
- b.setCompoundDrawablesRelative(icon, null, null, null);
- }
- b.setText(title);
- b.setOnClickListener(r);
- return b;
- }
-
- private Button createCopyButton() {
- final Button b = createActionButton(
+ private ActionRow.Action createCopyAction() {
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_copy_button,
+ getString(com.android.internal.R.string.copy),
getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
- getString(com.android.internal.R.string.copy), this::onCopyButtonClicked);
- b.setId(com.android.internal.R.id.chooser_copy_button);
- return b;
+ this::onCopyButtonClicked);
}
- private @Nullable Button createNearbyButton(Intent originalIntent) {
+ @Nullable
+ private ActionRow.Action createNearbyAction(Intent originalIntent) {
final TargetInfo ti = getNearbySharingTarget(originalIntent);
- if (ti == null) return null;
+ if (ti == null) {
+ return null;
+ }
- final Button b = createActionButton(
- ti.getDisplayIcon(this),
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_nearby_button,
ti.getDisplayLabel(),
- (View unused) -> {
- // Log share completion via nearby
- getChooserActivityLogger().logShareTargetSelected(
- SELECTION_TYPE_NEARBY,
- "",
- -1,
- false);
+ ti.getDisplayIconHolder().getDisplayIcon(),
+ () -> {
+ getChooserActivityLogger().logActionSelected(
+ ChooserActivityLogger.SELECTION_TYPE_NEARBY);
// Action bar is user-independent, always start as primary
safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
finish();
- }
- );
- b.setId(com.android.internal.R.id.chooser_nearby_button);
- return b;
+ });
}
- private @Nullable Button createEditButton(Intent originalIntent) {
+ @Nullable
+ private ActionRow.Action createEditAction(Intent originalIntent) {
final TargetInfo ti = getEditSharingTarget(originalIntent);
- if (ti == null) return null;
+ if (ti == null) {
+ return null;
+ }
- final Button b = createActionButton(
- ti.getDisplayIcon(this),
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_edit_button,
ti.getDisplayLabel(),
- (View unused) -> {
+ ti.getDisplayIconHolder().getDisplayIcon(),
+ () -> {
// Log share completion via edit
- getChooserActivityLogger().logShareTargetSelected(
- SELECTION_TYPE_EDIT,
- "",
- -1,
- false);
+ getChooserActivityLogger().logActionSelected(
+ ChooserActivityLogger.SELECTION_TYPE_EDIT);
View firstImgView = getFirstVisibleImgPreviewView();
// Action bar is user-independent, always start as primary
if (firstImgView == null) {
@@ -1238,8 +925,6 @@ public class ChooserActivity extends ResolverActivity implements
}
}
);
- b.setId(com.android.internal.R.id.chooser_edit_button);
- return b;
}
@Nullable
@@ -1248,165 +933,6 @@ public class ChooserActivity extends ResolverActivity implements
return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null;
}
- private void addActionButton(ViewGroup parent, Button b) {
- if (b == null) return;
- final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT
- );
- final int gap = getResources().getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2;
- lp.setMarginsRelative(gap, 0, gap, 0);
- parent.addView(b, lp);
- }
-
- private ViewGroup displayContentPreview(@ContentPreviewType int previewType,
- Intent targetIntent, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = null;
-
- switch (previewType) {
- case CONTENT_PREVIEW_TEXT:
- layout = displayTextContentPreview(targetIntent, layoutInflater, parent);
- break;
- case CONTENT_PREVIEW_IMAGE:
- layout = displayImageContentPreview(targetIntent, layoutInflater, parent);
- break;
- case CONTENT_PREVIEW_FILE:
- layout = displayFileContentPreview(targetIntent, layoutInflater, parent);
- break;
- default:
- Log.e(TAG, "Unexpected content preview type: " + previewType);
- }
-
- if (layout != null) {
- adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
- }
- if (previewType != CONTENT_PREVIEW_IMAGE) {
- mEnterTransitionAnimationDelegate.markImagePreviewReady();
- }
-
- return layout;
- }
-
- private ViewGroup displayTextContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
- ViewGroup parent) {
- ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
- R.layout.chooser_grid_preview_text, parent, false);
-
- final ViewGroup actionRow =
- (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
- addActionButton(actionRow, createCopyButton());
- if (shouldNearbyShareBeIncludedAsActionButton()) {
- addActionButton(actionRow, createNearbyButton(targetIntent));
- }
-
- CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
- if (sharingText == null) {
- contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_text_layout).setVisibility(
- View.GONE);
- } else {
- TextView textView = contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_text);
- textView.setText(sharingText);
- }
-
- String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
- if (TextUtils.isEmpty(previewTitle)) {
- contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_title_layout).setVisibility(
- View.GONE);
- } else {
- TextView previewTitleView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_title);
- previewTitleView.setText(previewTitle);
-
- ClipData previewData = targetIntent.getClipData();
- Uri previewThumbnail = null;
- if (previewData != null) {
- if (previewData.getItemCount() > 0) {
- ClipData.Item previewDataItem = previewData.getItemAt(0);
- previewThumbnail = previewDataItem.getUri();
- }
- }
-
- ImageView previewThumbnailView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_thumbnail);
- if (previewThumbnail == null) {
- previewThumbnailView.setVisibility(View.GONE);
- } else {
- mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false);
- mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_thumbnail, previewThumbnail, 0);
- }
- }
-
- return contentPreviewLayout;
- }
-
- private ViewGroup displayImageContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
- ViewGroup parent) {
- ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
- R.layout.chooser_grid_preview_image, parent, false);
- ViewGroup imagePreview = contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_image_area);
-
- final ViewGroup actionRow =
- (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
- //TODO: addActionButton(actionRow, createCopyButton());
- if (shouldNearbyShareBeIncludedAsActionButton()) {
- addActionButton(actionRow, createNearbyButton(targetIntent));
- }
- addActionButton(actionRow, createEditButton(targetIntent));
-
- mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false);
-
- String action = targetIntent.getAction();
- if (Intent.ACTION_SEND.equals(action)) {
- Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
- imagePreview.findViewById(com.android.internal.R.id.content_preview_image_1_large)
- .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
- mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_1_large, uri, 0);
- } else {
- ContentResolver resolver = getContentResolver();
-
- List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- List<Uri> imageUris = new ArrayList<>();
- for (Uri uri : uris) {
- if (isImageType(resolver.getType(uri))) {
- imageUris.add(uri);
- }
- }
-
- if (imageUris.size() == 0) {
- Log.i(TAG, "Attempted to display image preview area with zero"
- + " available images detected in EXTRA_STREAM list");
- imagePreview.setVisibility(View.GONE);
- return contentPreviewLayout;
- }
-
- imagePreview.findViewById(com.android.internal.R.id.content_preview_image_1_large)
- .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
- mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_1_large, imageUris.get(0), 0);
-
- if (imageUris.size() == 2) {
- mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_2_large,
- imageUris.get(1), 0);
- } else if (imageUris.size() > 2) {
- mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_2_small,
- imageUris.get(1), 0);
- mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_3_small,
- imageUris.get(2), imageUris.size() - 3);
- }
- }
-
- return contentPreviewLayout;
- }
-
- private static class FileInfo {
- public final String name;
- public final boolean hasThumbnail;
-
- FileInfo(String name, boolean hasThumbnail) {
- this.name = name;
- this.hasThumbnail = hasThumbnail;
- }
- }
-
/**
* Wrapping the ContentResolver call to expose for easier mocking,
* and to avoid mocking Android core classes.
@@ -1416,175 +942,11 @@ public class ChooserActivity extends ResolverActivity implements
return resolver.query(uri, null, null, null, null);
}
- private FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
- String fileName = null;
- boolean hasThumbnail = false;
-
- try (Cursor cursor = queryResolver(resolver, uri)) {
- if (cursor != null && cursor.getCount() > 0) {
- int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
- int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
- int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
-
- cursor.moveToFirst();
- if (nameIndex != -1) {
- fileName = cursor.getString(nameIndex);
- } else if (titleIndex != -1) {
- fileName = cursor.getString(titleIndex);
- }
-
- if (flagsIndex != -1) {
- hasThumbnail = (cursor.getInt(flagsIndex)
- & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
- }
- }
- } catch (SecurityException | NullPointerException e) {
- logContentPreviewWarning(uri);
- }
-
- if (TextUtils.isEmpty(fileName)) {
- fileName = uri.getPath();
- int index = fileName.lastIndexOf('/');
- if (index != -1) {
- fileName = fileName.substring(index + 1);
- }
- }
-
- return new FileInfo(fileName, hasThumbnail);
- }
-
- private void logContentPreviewWarning(Uri uri) {
- // The ContentResolver already logs the exception. Log something more informative.
- Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
- + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
- + "and set your Intent's clipData and flags in accordance with that method's "
- + "documentation");
- }
-
- private ViewGroup displayFileContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
- ViewGroup parent) {
-
- ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
- R.layout.chooser_grid_preview_file, parent, false);
-
- final ViewGroup actionRow =
- (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
- //TODO(b/120417119): addActionButton(actionRow, createCopyButton());
- if (shouldNearbyShareBeIncludedAsActionButton()) {
- addActionButton(actionRow, createNearbyButton(targetIntent));
- }
-
- String action = targetIntent.getAction();
- if (Intent.ACTION_SEND.equals(action)) {
- Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
- loadFileUriIntoView(uri, contentPreviewLayout);
- } else {
- List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- int uriCount = uris.size();
-
- if (uriCount == 0) {
- contentPreviewLayout.setVisibility(View.GONE);
- Log.i(TAG,
- "Appears to be no uris available in EXTRA_STREAM, removing "
- + "preview area");
- return contentPreviewLayout;
- } else if (uriCount == 1) {
- loadFileUriIntoView(uris.get(0), contentPreviewLayout);
- } else {
- FileInfo fileInfo = extractFileInfo(uris.get(0), getContentResolver());
- int remUriCount = uriCount - 1;
- Map<String, Object> arguments = new HashMap<>();
- arguments.put(PLURALS_COUNT, remUriCount);
- arguments.put(PLURALS_FILE_NAME, fileInfo.name);
- String fileName = PluralsMessageFormatter.format(
- getResources(),
- arguments,
- R.string.file_count);
-
- TextView fileNameView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_filename);
- fileNameView.setText(fileName);
-
- View thumbnailView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_file_thumbnail);
- thumbnailView.setVisibility(View.GONE);
-
- ImageView fileIconView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_file_icon);
- fileIconView.setVisibility(View.VISIBLE);
- fileIconView.setImageResource(R.drawable.ic_file_copy);
- }
- }
-
- return contentPreviewLayout;
- }
-
- private void loadFileUriIntoView(final Uri uri, final View parent) {
- FileInfo fileInfo = extractFileInfo(uri, getContentResolver());
-
- TextView fileNameView = parent.findViewById(com.android.internal.R.id.content_preview_filename);
- fileNameView.setText(fileInfo.name);
-
- if (fileInfo.hasThumbnail) {
- mPreviewCoord = new ContentPreviewCoordinator(parent, false);
- mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_file_thumbnail, uri, 0);
- } else {
- View thumbnailView = parent.findViewById(com.android.internal.R.id.content_preview_file_thumbnail);
- thumbnailView.setVisibility(View.GONE);
-
- ImageView fileIconView = parent.findViewById(com.android.internal.R.id.content_preview_file_icon);
- fileIconView.setVisibility(View.VISIBLE);
- fileIconView.setImageResource(R.drawable.chooser_file_generic);
- }
- }
-
@VisibleForTesting
protected boolean isImageType(String mimeType) {
return mimeType != null && mimeType.startsWith("image/");
}
- @ContentPreviewType
- private int findPreferredContentPreview(Uri uri, ContentResolver resolver) {
- if (uri == null) {
- return CONTENT_PREVIEW_TEXT;
- }
-
- String mimeType = resolver.getType(uri);
- return isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
- }
-
- /**
- * In {@link android.content.Intent#getType}, the app may specify a very general
- * mime-type that broadly covers all data being shared, such as {@literal *}/*
- * when sending an image and text. We therefore should inspect each item for the
- * the preferred type, in order of IMAGE, FILE, TEXT.
- */
- @ContentPreviewType
- private int findPreferredContentPreview(Intent targetIntent, ContentResolver resolver) {
- String action = targetIntent.getAction();
- if (Intent.ACTION_SEND.equals(action)) {
- Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
- return findPreferredContentPreview(uri, resolver);
- } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
- List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- if (uris == null || uris.isEmpty()) {
- return CONTENT_PREVIEW_TEXT;
- }
-
- for (Uri uri : uris) {
- // Defaulting to file preview when there are mixed image/file types is
- // preferable, as it shows the user the correct number of items being shared
- if (findPreferredContentPreview(uri, resolver) == CONTENT_PREVIEW_FILE) {
- return CONTENT_PREVIEW_FILE;
- }
- }
-
- return CONTENT_PREVIEW_IMAGE;
- }
-
- return CONTENT_PREVIEW_TEXT;
- }
-
private int getNumSheetExpansions() {
return getPreferences(Context.MODE_PRIVATE).getInt(PREF_NUM_SHEET_EXPANSIONS, 0);
}
@@ -1614,23 +976,29 @@ public class ChooserActivity extends ResolverActivity implements
mRefinementResultReceiver.destroy();
mRefinementResultReceiver = null;
}
- mChooserHandler.removeAllMessages();
- if (mPreviewCoord != null) mPreviewCoord.cancelLoads();
+ mBackgroundThreadPoolExecutor.shutdownNow();
- mChooserMultiProfilePagerAdapter.getActiveListAdapter().destroyAppPredictor();
- if (mChooserMultiProfilePagerAdapter.getInactiveListAdapter() != null) {
- mChooserMultiProfilePagerAdapter.getInactiveListAdapter().destroyAppPredictor();
+ destroyProfileRecords();
+ }
+
+ private void destroyProfileRecords() {
+ for (int i = 0; i < mProfileRecords.size(); ++i) {
+ mProfileRecords.valueAt(i).destroy();
}
- mPersonalAppPredictor = null;
- mWorkAppPredictor = null;
+ mProfileRecords.clear();
}
@Override // ResolverListCommunicator
public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ if (mChooserRequest == null) {
+ return defIntent;
+ }
+
Intent result = defIntent;
- if (mReplacementExtras != null) {
- final Bundle replExtras = mReplacementExtras.getBundle(aInfo.packageName);
+ if (mChooserRequest.getReplacementExtras() != null) {
+ final Bundle replExtras =
+ mChooserRequest.getReplacementExtras().getBundle(aInfo.packageName);
if (replExtras != null) {
result = new Intent(defIntent);
result.putExtras(replExtras);
@@ -1651,12 +1019,13 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void onActivityStarted(TargetInfo cti) {
- if (mChosenComponentSender != null) {
+ if (mChooserRequest.getChosenComponentSender() != null) {
final ComponentName target = cti.getResolvedComponentName();
if (target != null) {
final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
try {
- mChosenComponentSender.sendIntent(this, Activity.RESULT_OK, fillIn, null, null);
+ mChooserRequest.getChosenComponentSender().sendIntent(
+ this, Activity.RESULT_OK, fillIn, null, null);
} catch (IntentSender.SendIntentException e) {
Slog.e(TAG, "Unable to launch supplied IntentSender to report "
+ "the chosen component: " + e);
@@ -1667,12 +1036,13 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
- if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) {
+ if (mChooserRequest.getCallerChooserTargets().size() > 0) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
/* origTarget */ null,
- Lists.newArrayList(mCallerChooserTargets),
+ new ArrayList<>(mChooserRequest.getCallerChooserTargets()),
TARGET_TYPE_DEFAULT,
- /* directShareShortcutInfoCache */ null);
+ /* directShareShortcutInfoCache */ Collections.emptyMap(),
+ /* directShareAppTargetCache */ Collections.emptyMap());
}
}
@@ -1701,57 +1071,34 @@ public class ChooserActivity extends ResolverActivity implements
private void showTargetDetails(TargetInfo targetInfo) {
if (targetInfo == null) return;
- ArrayList<DisplayResolveInfo> targetList;
- ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment();
- Bundle bundle = new Bundle();
-
- if (targetInfo instanceof SelectableTargetInfo) {
- SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo;
- if (selectableTargetInfo.getDisplayResolveInfo() == null
- || selectableTargetInfo.getChooserTarget() == null) {
- Log.e(TAG, "displayResolveInfo or chooserTarget in selectableTargetInfo are null");
- return;
- }
- targetList = new ArrayList<>();
- targetList.add(selectableTargetInfo.getDisplayResolveInfo());
- bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY,
- selectableTargetInfo.getChooserTarget().getIntentExtras().getString(
- Intent.EXTRA_SHORTCUT_ID));
- bundle.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY,
- selectableTargetInfo.isPinned());
- bundle.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY,
- getTargetIntentFilter());
- if (selectableTargetInfo.getDisplayLabel() != null) {
- bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY,
- selectableTargetInfo.getDisplayLabel().toString());
- }
- } else if (targetInfo instanceof MultiDisplayResolveInfo) {
- // For multiple targets, include info on all targets
- MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
- targetList = mti.getTargets();
- } else {
- targetList = new ArrayList<DisplayResolveInfo>();
- targetList.add((DisplayResolveInfo) targetInfo);
+ List<DisplayResolveInfo> targetList = targetInfo.getAllDisplayTargets();
+ if (targetList.isEmpty()) {
+ Log.e(TAG, "No displayable data to show target details");
+ return;
}
- bundle.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
- bundle.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY,
- targetList);
- fragment.setArguments(bundle);
- fragment.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
- }
+ // TODO: implement these type-conditioned behaviors polymorphically, and consider moving
+ // the logic into `ChooserTargetActionsDialogFragment.show()`.
+ boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned();
+ IntentFilter intentFilter = targetInfo.isSelectableTargetInfo()
+ ? mChooserRequest.getTargetIntentFilter() : null;
+ String shortcutTitle = targetInfo.isSelectableTargetInfo()
+ ? targetInfo.getDisplayLabel().toString() : null;
+ String shortcutIdKey = targetInfo.getDirectShareShortcutId();
- private void modifyTargetIntent(Intent in) {
- if (isSendAction(in)) {
- in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
- Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
- }
+ ChooserTargetActionsDialogFragment.show(
+ getSupportFragmentManager(),
+ targetList,
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle(),
+ shortcutIdKey,
+ shortcutTitle,
+ isShortcutPinned,
+ intentFilter);
}
@Override
protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
- if (mRefinementIntentSender != null) {
+ if (mChooserRequest.getRefinementIntentSender() != null) {
final Intent fillIn = new Intent();
final List<Intent> sourceIntents = target.getAllSourceIntents();
if (!sourceIntents.isEmpty()) {
@@ -1770,7 +1117,8 @@ public class ChooserActivity extends ResolverActivity implements
fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER,
mRefinementResultReceiver);
try {
- mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null);
+ mChooserRequest.getRefinementIntentSender().sendIntent(
+ this, 0, fillIn, null, null);
return false;
} catch (SendIntentException e) {
Log.e(TAG, "Refinement IntentSender failed to send", e);
@@ -1787,25 +1135,20 @@ public class ChooserActivity extends ResolverActivity implements
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
TargetInfo targetInfo = currentListAdapter
.targetInfoForPosition(which, filtered);
- if (targetInfo != null && targetInfo instanceof NotSelectableTargetInfo) {
+ if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) {
return;
}
final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
- if (targetInfo instanceof MultiDisplayResolveInfo) {
+ if (targetInfo.isMultiDisplayResolveInfo()) {
MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
if (!mti.hasSelected()) {
- ChooserStackedAppDialogFragment f = new ChooserStackedAppDialogFragment();
- Bundle b = new Bundle();
- b.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
+ ChooserStackedAppDialogFragment.show(
+ getSupportFragmentManager(),
+ mti,
+ which,
mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
- b.putObject(ChooserStackedAppDialogFragment.MULTI_DRI_KEY,
- mti);
- b.putInt(ChooserStackedAppDialogFragment.WHICH_KEY, which);
- f.setArguments(b);
-
- f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
return;
}
}
@@ -1813,103 +1156,65 @@ public class ChooserActivity extends ResolverActivity implements
super.startSelected(which, always, filtered);
if (currentListAdapter.getCount() > 0) {
- // Log the index of which type of target the user picked.
- // Lower values mean the ranking was better.
- int cat = 0;
- int value = which;
- int directTargetAlsoRanked = -1;
- int numCallerProvided = 0;
- HashedStringCache.HashResult directTargetHashed = null;
switch (currentListAdapter.getPositionTargetType(which)) {
case ChooserListAdapter.TARGET_SERVICE:
- cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET;
- // Log the package name + target name to answer the question if most users
- // share to mostly the same person or to a bunch of different people.
- ChooserTarget target = currentListAdapter.getChooserTargetForValue(value);
- directTargetHashed = HashedStringCache.getInstance().hashString(
- this,
- TAG,
- target.getComponentName().getPackageName()
- + target.getTitle().toString(),
- mMaxHashSaltDays);
- SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo;
- directTargetAlsoRanked = getRankedPosition(selectableTargetInfo);
-
- if (mCallerChooserTargets != null) {
- numCallerProvided = mCallerChooserTargets.length;
- }
getChooserActivityLogger().logShareTargetSelected(
- SELECTION_TYPE_SERVICE,
+ ChooserActivityLogger.SELECTION_TYPE_SERVICE,
targetInfo.getResolveInfo().activityInfo.processName,
- value,
- selectableTargetInfo.isPinned()
+ which,
+ /* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
+ mChooserRequest.getCallerChooserTargets().size(),
+ targetInfo.getHashedTargetIdForMetrics(this),
+ targetInfo.isPinned(),
+ mIsSuccessfullySelected,
+ selectionCost
);
- break;
+ return;
case ChooserListAdapter.TARGET_CALLER:
case ChooserListAdapter.TARGET_STANDARD:
- cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET;
- value -= currentListAdapter.getSurfacedTargetInfo().size();
- numCallerProvided = currentListAdapter.getCallerTargetCount();
getChooserActivityLogger().logShareTargetSelected(
- SELECTION_TYPE_APP,
+ ChooserActivityLogger.SELECTION_TYPE_APP,
targetInfo.getResolveInfo().activityInfo.processName,
- value,
- targetInfo.isPinned()
+ (which - currentListAdapter.getSurfacedTargetInfo().size()),
+ /* directTargetAlsoRanked= */ -1,
+ currentListAdapter.getCallerTargetCount(),
+ /* directTargetHashed= */ null,
+ targetInfo.isPinned(),
+ mIsSuccessfullySelected,
+ selectionCost
);
- break;
+ return;
case ChooserListAdapter.TARGET_STANDARD_AZ:
- // A-Z targets are unranked standard targets; we use -1 to mark that they
- // are from the alphabetical pool.
- value = -1;
- cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET;
+ // A-Z targets are unranked standard targets; we use a value of -1 to mark that
+ // they are from the alphabetical pool.
+ // TODO: why do we log a different selection type if the -1 value already
+ // designates the same condition?
getChooserActivityLogger().logShareTargetSelected(
- SELECTION_TYPE_STANDARD,
+ ChooserActivityLogger.SELECTION_TYPE_STANDARD,
targetInfo.getResolveInfo().activityInfo.processName,
- value,
- false
+ /* value= */ -1,
+ /* directTargetAlsoRanked= */ -1,
+ /* numCallerProvided= */ 0,
+ /* directTargetHashed= */ null,
+ /* isPinned= */ false,
+ mIsSuccessfullySelected,
+ selectionCost
);
- break;
- }
-
- if (cat != 0) {
- LogMaker targetLogMaker = new LogMaker(cat).setSubtype(value);
- if (directTargetHashed != null) {
- targetLogMaker.addTaggedData(
- MetricsEvent.FIELD_HASHED_TARGET_NAME, directTargetHashed.hashedString);
- targetLogMaker.addTaggedData(
- MetricsEvent.FIELD_HASHED_TARGET_SALT_GEN,
- directTargetHashed.saltGeneration);
- targetLogMaker.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION,
- directTargetAlsoRanked);
- }
- targetLogMaker.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED,
- numCallerProvided);
- getMetricsLogger().write(targetLogMaker);
- }
-
- if (mIsSuccessfullySelected) {
- if (DEBUG) {
- Log.d(TAG, "User Selection Time Cost is " + selectionCost);
- Log.d(TAG, "position of selected app/service/caller is " +
- Integer.toString(value));
- }
- MetricsLogger.histogram(null, "user_selection_cost_for_smart_sharing",
- (int) selectionCost);
- MetricsLogger.histogram(null, "app_position_for_smart_sharing", value);
+ return;
}
}
}
- private int getRankedPosition(SelectableTargetInfo targetInfo) {
+ private int getRankedPosition(TargetInfo targetInfo) {
String targetPackageName =
- targetInfo.getChooserTarget().getComponentName().getPackageName();
+ targetInfo.getChooserTargetComponentName().getPackageName();
ChooserListAdapter currentListAdapter =
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
- int maxRankedResults = Math.min(currentListAdapter.mDisplayList.size(),
- MAX_LOG_RANK_POSITION);
+ int maxRankedResults = Math.min(
+ currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION);
for (int i = 0; i < maxRankedResults; i++) {
- if (currentListAdapter.mDisplayList.get(i)
+ if (currentListAdapter.getDisplayResolveInfo(i)
.getResolveInfo().activityInfo.packageName.equals(targetPackageName)) {
return i;
}
@@ -1933,8 +1238,11 @@ public class ChooserActivity extends ResolverActivity implements
}
private IntentFilter getTargetIntentFilter() {
+ return getTargetIntentFilter(getTargetIntent());
+ }
+
+ private IntentFilter getTargetIntentFilter(final Intent intent) {
try {
- final Intent intent = getTargetIntent();
String dataString = intent.getDataString();
if (intent.getType() == null) {
if (!TextUtils.isEmpty(dataString)) {
@@ -1968,218 +1276,18 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- @VisibleForTesting
- protected void queryDirectShareTargets(
- ChooserListAdapter adapter, boolean skipAppPredictionService) {
- mQueriedSharingShortcutsTimeMs = System.currentTimeMillis();
- UserHandle userHandle = adapter.getUserHandle();
- if (!skipAppPredictionService) {
- AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(userHandle);
- if (appPredictor != null) {
- appPredictor.requestPredictionUpdate();
- return;
- }
- }
- // Default to just querying ShortcutManager if AppPredictor not present.
- final IntentFilter filter = getTargetIntentFilter();
- if (filter == null) {
+ private void logDirectShareTargetReceived(UserHandle forUser) {
+ ProfileRecord profileRecord = getProfileRecord(forUser);
+ if (profileRecord == null) {
return;
}
-
- AsyncTask.execute(() -> {
- Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */);
- ShortcutManager sm = (ShortcutManager) selectedProfileContext
- .getSystemService(Context.SHORTCUT_SERVICE);
- List<ShortcutManager.ShareShortcutInfo> resultList = sm.getShareTargets(filter);
- sendShareShortcutInfoList(resultList, adapter, null, userHandle);
- });
- }
-
- /**
- * Returns {@code false} if {@code userHandle} is the work profile and it's either
- * in quiet mode or not running.
- */
- private boolean shouldQueryShortcutManager(UserHandle userHandle) {
- if (!shouldShowTabs()) {
- return true;
- }
- if (!getWorkProfileUserHandle().equals(userHandle)) {
- return true;
- }
- if (!isUserRunning(userHandle)) {
- return false;
- }
- if (!isUserUnlocked(userHandle)) {
- return false;
- }
- if (isQuietModeEnabled(userHandle)) {
- return false;
- }
- return true;
- }
-
- private void sendShareShortcutInfoList(
- List<ShortcutManager.ShareShortcutInfo> resultList,
- ChooserListAdapter chooserListAdapter,
- @Nullable List<AppTarget> appTargets, UserHandle userHandle) {
- if (appTargets != null && appTargets.size() != resultList.size()) {
- throw new RuntimeException("resultList and appTargets must have the same size."
- + " resultList.size()=" + resultList.size()
- + " appTargets.size()=" + appTargets.size());
- }
- Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */);
- for (int i = resultList.size() - 1; i >= 0; i--) {
- final String packageName = resultList.get(i).getTargetComponent().getPackageName();
- if (!isPackageEnabled(selectedProfileContext, packageName)) {
- resultList.remove(i);
- if (appTargets != null) {
- appTargets.remove(i);
- }
- }
- }
-
- // If |appTargets| is not null, results are from AppPredictionService and already sorted.
- final int shortcutType = (appTargets == null ? TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER :
- TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
-
- // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path
- // for direct share targets. After ShareSheet is refactored we should use the
- // ShareShortcutInfos directly.
- List<ServiceResultInfo> resultRecords = new ArrayList<>();
- for (int i = 0; i < chooserListAdapter.getDisplayResolveInfoCount(); i++) {
- DisplayResolveInfo displayResolveInfo = chooserListAdapter.getDisplayResolveInfo(i);
- List<ShortcutManager.ShareShortcutInfo> matchingShortcuts =
- filterShortcutsByTargetComponentName(
- resultList, displayResolveInfo.getResolvedComponentName());
- if (matchingShortcuts.isEmpty()) {
- continue;
- }
- List<ChooserTarget> chooserTargets = convertToChooserTarget(
- matchingShortcuts, resultList, appTargets, shortcutType);
-
- ServiceResultInfo resultRecord = new ServiceResultInfo(
- displayResolveInfo, chooserTargets, userHandle);
- resultRecords.add(resultRecord);
- }
-
- sendShortcutManagerShareTargetResults(
- shortcutType, resultRecords.toArray(new ServiceResultInfo[0]));
- }
-
- private List<ShortcutManager.ShareShortcutInfo> filterShortcutsByTargetComponentName(
- List<ShortcutManager.ShareShortcutInfo> allShortcuts, ComponentName requiredTarget) {
- List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = new ArrayList<>();
- for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) {
- if (requiredTarget.equals(shortcut.getTargetComponent())) {
- matchingShortcuts.add(shortcut);
- }
- }
- return matchingShortcuts;
- }
-
- private void sendShortcutManagerShareTargetResults(
- int shortcutType, ServiceResultInfo[] results) {
- final Message msg = Message.obtain();
- msg.what = ChooserHandler.SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS;
- msg.obj = results;
- msg.arg1 = shortcutType;
- mChooserHandler.sendMessage(msg);
- }
-
- private boolean isPackageEnabled(Context context, String packageName) {
- if (TextUtils.isEmpty(packageName)) {
- return false;
- }
- ApplicationInfo appInfo;
- try {
- appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
- } catch (NameNotFoundException e) {
- return false;
- }
-
- if (appInfo != null && appInfo.enabled
- && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) {
- return true;
- }
- return false;
- }
-
- /**
- * Converts a list of ShareShortcutInfos to ChooserTargets.
- * @param matchingShortcuts List of shortcuts, all from the same package, that match the current
- * share intent filter.
- * @param allShortcuts List of all the shortcuts from all the packages on the device that are
- * returned for the current sharing action.
- * @param allAppTargets List of AppTargets. Null if the results are not from prediction service.
- * @param shortcutType One of the values TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER or
- * TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
- * @return A list of ChooserTargets sorted by score in descending order.
- */
- @VisibleForTesting
- @NonNull
- public List<ChooserTarget> convertToChooserTarget(
- @NonNull List<ShortcutManager.ShareShortcutInfo> matchingShortcuts,
- @NonNull List<ShortcutManager.ShareShortcutInfo> allShortcuts,
- @Nullable List<AppTarget> allAppTargets, @ShareTargetType int shortcutType) {
- // A set of distinct scores for the matched shortcuts. We use index of a rank in the sorted
- // list instead of the actual rank value when converting a rank to a score.
- List<Integer> scoreList = new ArrayList<>();
- if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
- for (int i = 0; i < matchingShortcuts.size(); i++) {
- int shortcutRank = matchingShortcuts.get(i).getShortcutInfo().getRank();
- if (!scoreList.contains(shortcutRank)) {
- scoreList.add(shortcutRank);
- }
- }
- Collections.sort(scoreList);
- }
-
- List<ChooserTarget> chooserTargetList = new ArrayList<>(matchingShortcuts.size());
- for (int i = 0; i < matchingShortcuts.size(); i++) {
- ShortcutInfo shortcutInfo = matchingShortcuts.get(i).getShortcutInfo();
- int indexInAllShortcuts = allShortcuts.indexOf(matchingShortcuts.get(i));
-
- float score;
- if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
- // Incoming results are ordered. Create a score based on index in the original list.
- score = Math.max(1.0f - (0.01f * indexInAllShortcuts), 0.0f);
- } else {
- // Create a score based on the rank of the shortcut.
- int rankIndex = scoreList.indexOf(shortcutInfo.getRank());
- score = Math.max(1.0f - (0.01f * rankIndex), 0.0f);
- }
-
- Bundle extras = new Bundle();
- extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId());
-
- ChooserTarget chooserTarget = new ChooserTarget(
- shortcutInfo.getLabel(),
- null, // Icon will be loaded later if this target is selected to be shown.
- score, matchingShortcuts.get(i).getTargetComponent().clone(), extras);
-
- chooserTargetList.add(chooserTarget);
- if (mDirectShareAppTargetCache != null && allAppTargets != null) {
- mDirectShareAppTargetCache.put(chooserTarget,
- allAppTargets.get(indexInAllShortcuts));
- }
- if (mDirectShareShortcutInfoCache != null) {
- mDirectShareShortcutInfoCache.put(chooserTarget, shortcutInfo);
- }
- }
- // Sort ChooserTargets by score in descending order
- Comparator<ChooserTarget> byScore =
- (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore());
- Collections.sort(chooserTargetList, byScore);
- return chooserTargetList;
- }
-
- private void logDirectShareTargetReceived(int logCategory) {
- final int apiLatency = (int) (System.currentTimeMillis() - mQueriedSharingShortcutsTimeMs);
- getMetricsLogger().write(new LogMaker(logCategory).setSubtype(apiLatency));
+ getChooserActivityLogger().logDirectShareTargetReceived(
+ MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
+ (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime));
}
void updateModelAndChooserCounts(TargetInfo info) {
- if (info != null && info instanceof MultiDisplayResolveInfo) {
+ if (info != null && info.isMultiDisplayResolveInfo()) {
info = ((MultiDisplayResolveInfo) info).getSelectedTarget();
}
if (info != null) {
@@ -2200,31 +1308,35 @@ public class ChooserActivity extends ResolverActivity implements
Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
}
} else if (DEBUG) {
- Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo");
+ Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");
}
}
mIsSuccessfullySelected = true;
}
private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) {
- AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled(
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
- if (directShareAppPredictor == null) {
+ // Send DS target impression info to AppPredictor, only when user chooses app share.
+ if (targetInfo.isChooserTargetInfo()) {
return;
}
- // Send DS target impression info to AppPredictor, only when user chooses app share.
- if (targetInfo instanceof ChooserTargetInfo) {
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
return;
}
- List<ChooserTargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
+ List<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
List<AppTargetId> targetIds = new ArrayList<>();
- for (ChooserTargetInfo chooserTargetInfo : surfacedTargetInfo) {
- ChooserTarget chooserTarget = chooserTargetInfo.getChooserTarget();
- ComponentName componentName = chooserTarget.getComponentName();
- if (mDirectShareShortcutInfoCache.containsKey(chooserTarget)) {
- String shortcutId = mDirectShareShortcutInfoCache.get(chooserTarget).getId();
+ for (TargetInfo chooserTargetInfo : surfacedTargetInfo) {
+ ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo();
+ if (shortcutInfo != null) {
+ ComponentName componentName =
+ chooserTargetInfo.getChooserTargetComponentName();
targetIds.add(new AppTargetId(
- String.format("%s/%s/%s", shortcutId, componentName.flattenToString(),
+ String.format(
+ "%s/%s/%s",
+ shortcutInfo.getId(),
+ componentName.flattenToString(),
SHORTCUT_TARGET)));
}
}
@@ -2232,21 +1344,18 @@ public class ChooserActivity extends ResolverActivity implements
}
private void sendClickToAppPredictor(TargetInfo targetInfo) {
- AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled(
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
- if (directShareAppPredictor == null) {
+ if (!targetInfo.isChooserTargetInfo()) {
return;
}
- if (!(targetInfo instanceof ChooserTargetInfo)) {
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
return;
}
- ChooserTarget chooserTarget = ((ChooserTargetInfo) targetInfo).getChooserTarget();
- AppTarget appTarget = null;
- if (mDirectShareAppTargetCache != null) {
- appTarget = mDirectShareAppTargetCache.get(chooserTarget);
- }
- // This is a direct share click that was provided by the APS
+ AppTarget appTarget = targetInfo.getDirectShareAppTarget();
if (appTarget != null) {
+ // This is a direct share click that was provided by the APS
directShareAppPredictor.notifyAppTargetEvent(
new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH)
.setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE)
@@ -2255,70 +1364,9 @@ public class ChooserActivity extends ResolverActivity implements
}
@Nullable
- private AppPredictor createAppPredictor(UserHandle userHandle) {
- if (!mIsAppPredictorComponentAvailable) {
- return null;
- }
-
- if (getPersonalProfileUserHandle().equals(userHandle)) {
- if (mPersonalAppPredictor != null) {
- return mPersonalAppPredictor;
- }
- } else {
- if (mWorkAppPredictor != null) {
- return mWorkAppPredictor;
- }
- }
-
- // TODO(b/148230574): Currently AppPredictor fetches only the same-profile app targets.
- // Make AppPredictor work cross-profile.
- Context contextAsUser = createContextAsUser(userHandle, 0 /* flags */);
- final IntentFilter filter = getTargetIntentFilter();
- Bundle extras = new Bundle();
- extras.putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, filter);
- populateTextContent(extras);
- AppPredictionContext appPredictionContext = new AppPredictionContext.Builder(contextAsUser)
- .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
- .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
- .setExtras(extras)
- .build();
- AppPredictionManager appPredictionManager =
- contextAsUser
- .getSystemService(AppPredictionManager.class);
- AppPredictor appPredictionSession = appPredictionManager.createAppPredictionSession(
- appPredictionContext);
- if (getPersonalProfileUserHandle().equals(userHandle)) {
- mPersonalAppPredictor = appPredictionSession;
- } else {
- mWorkAppPredictor = appPredictionSession;
- }
- return appPredictionSession;
- }
-
- private void populateTextContent(Bundle extras) {
- final Intent intent = getTargetIntent();
- String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
- extras.putString(SHARED_TEXT_KEY, sharedText);
- }
-
- /**
- * This will return an app predictor if it is enabled for direct share sorting
- * and if one exists. Otherwise, it returns null.
- * @param userHandle
- */
- @Nullable
- private AppPredictor getAppPredictorForDirectShareIfEnabled(UserHandle userHandle) {
- return ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS
- && !ActivityManager.isLowRamDeviceStatic() ? createAppPredictor(userHandle) : null;
- }
-
- /**
- * This will return an app predictor if it is enabled for share activity sorting
- * and if one exists. Otherwise, it returns null.
- */
- @Nullable
- private AppPredictor getAppPredictorForShareActivitiesIfEnabled(UserHandle userHandle) {
- return USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES ? createAppPredictor(userHandle) : null;
+ private AppPredictor getAppPredictor(UserHandle userHandle) {
+ ProfileRecord record = getProfileRecord(userHandle);
+ return (record == null) ? null : record.appPredictor;
}
void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) {
@@ -2377,16 +1425,9 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- protected MetricsLogger getMetricsLogger() {
- if (mMetricsLogger == null) {
- mMetricsLogger = new MetricsLogger();
- }
- return mMetricsLogger;
- }
-
protected ChooserActivityLogger getChooserActivityLogger() {
if (mChooserActivityLogger == null) {
- mChooserActivityLogger = new ChooserActivityLoggerImpl();
+ mChooserActivityLogger = new ChooserActivityLogger();
}
return mChooserActivityLogger;
}
@@ -2405,56 +1446,139 @@ public class ChooserActivity extends ResolverActivity implements
@Override
boolean isComponentFiltered(ComponentName name) {
- if (mFilteredComponentNames == null) {
- return false;
- }
- for (ComponentName filteredComponentName : mFilteredComponentNames) {
- if (name.equals(filteredComponentName)) {
- return true;
- }
- }
- return false;
+ return mChooserRequest.getFilteredComponentNames().contains(name);
}
@Override
public boolean isComponentPinned(ComponentName name) {
return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
}
-
- @Override
- public boolean isFixedAtTop(ComponentName name) {
- return name != null && name.equals(getNearbySharingComponent())
- && shouldNearbyShareBeFirstInRankedRow();
- }
}
@VisibleForTesting
- public ChooserGridAdapter createChooserGridAdapter(Context context,
- List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
- boolean filterLastUsed, UserHandle userHandle) {
- ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents,
- initialIntents, rList, filterLastUsed,
- createListController(userHandle));
- AppPredictor.Callback appPredictorCallback = createAppPredictorCallback(chooserListAdapter);
- AppPredictor appPredictor = setupAppPredictorForUser(userHandle, appPredictorCallback);
- chooserListAdapter.setAppPredictor(appPredictor);
- chooserListAdapter.setAppPredictorCallback(appPredictorCallback);
- return new ChooserGridAdapter(chooserListAdapter);
+ public ChooserGridAdapter createChooserGridAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ UserHandle userHandle) {
+ ChooserListAdapter chooserListAdapter = createChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ getTargetIntent(),
+ mChooserRequest,
+ mMaxTargetsPerRow);
+
+ return new ChooserGridAdapter(
+ context,
+ new ChooserGridAdapter.ChooserActivityDelegate() {
+ @Override
+ public boolean shouldShowTabs() {
+ return ChooserActivity.this.shouldShowTabs();
+ }
+
+ @Override
+ public View buildContentPreview(ViewGroup parent) {
+ return createContentPreviewView(parent, mPreviewCoordinator);
+ }
+
+ @Override
+ public void onTargetSelected(int itemIndex) {
+ startSelected(itemIndex, false, true);
+ }
+
+ @Override
+ public void onTargetLongPressed(int selectedPosition) {
+ final TargetInfo longPressedTargetInfo =
+ mChooserMultiProfilePagerAdapter
+ .getActiveListAdapter()
+ .targetInfoForPosition(
+ selectedPosition, /* filtered= */ true);
+ // ItemViewHolder contents should always be "display resolve info"
+ // targets, but check just to make sure.
+ if (longPressedTargetInfo.isDisplayResolveInfo()) {
+ showTargetDetails(longPressedTargetInfo);
+ }
+ }
+
+ @Override
+ public void updateProfileViewButton(View newButtonFromProfileRow) {
+ mProfileView = newButtonFromProfileRow;
+ mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
+ ChooserActivity.this.updateProfileViewButton();
+ }
+
+ @Override
+ public int getValidTargetCount() {
+ return mChooserMultiProfilePagerAdapter
+ .getActiveListAdapter()
+ .getSelectableServiceTargetCount();
+ }
+
+ @Override
+ public void updateDirectShareExpansion(DirectShareViewHolder directShareGroup) {
+ RecyclerView activeAdapterView =
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ if (mResolverDrawerLayout.isCollapsed()) {
+ directShareGroup.collapse(activeAdapterView);
+ } else {
+ directShareGroup.expand(activeAdapterView);
+ }
+ }
+
+ @Override
+ public void handleScrollToExpandDirectShare(
+ DirectShareViewHolder directShareGroup, int y, int oldy) {
+ directShareGroup.handleScroll(
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView(),
+ y,
+ oldy,
+ mMaxTargetsPerRow);
+ }
+ },
+ chooserListAdapter,
+ shouldShowContentPreview(),
+ mMaxTargetsPerRow,
+ getNumSheetExpansions());
}
@VisibleForTesting
- public ChooserListAdapter createChooserListAdapter(Context context,
- List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
- boolean filterLastUsed, ResolverListController resolverListController) {
- return new ChooserListAdapter(context, payloadIntents, initialIntents, rList,
- filterLastUsed, resolverListController, this,
- this, context.getPackageManager(),
- getChooserActivityLogger());
+ public ChooserListAdapter createChooserListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ ChooserRequestParameters chooserRequest,
+ int maxTargetsPerRow) {
+ return new ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ this,
+ context.getPackageManager(),
+ getChooserActivityLogger(),
+ chooserRequest,
+ maxTargetsPerRow);
}
@VisibleForTesting
protected ResolverListController createListController(UserHandle userHandle) {
- AppPredictor appPredictor = getAppPredictorForShareActivitiesIfEnabled(userHandle);
+ AppPredictor appPredictor = getAppPredictor(userHandle);
AbstractResolverComparator resolverComparator;
if (appPredictor != null) {
resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
@@ -2484,28 +1608,11 @@ public class ChooserActivity extends ResolverActivity implements
try {
return getContentResolver().loadThumbnail(uri, size, null);
} catch (IOException | NullPointerException | SecurityException ex) {
- logContentPreviewWarning(uri);
+ getChooserActivityLogger().logContentPreviewWarning(uri);
}
return null;
}
- static final class PlaceHolderTargetInfo extends NotSelectableTargetInfo {
- public Drawable getDisplayIcon(Context context) {
- AnimatedVectorDrawable avd = (AnimatedVectorDrawable)
- context.getDrawable(R.drawable.chooser_direct_share_icon_placeholder);
- avd.start(); // Start animation after generation
- return avd;
- }
- }
-
- protected static final class EmptyTargetInfo extends NotSelectableTargetInfo {
- public EmptyTargetInfo() {}
-
- public Drawable getDisplayIcon(Context context) {
- return null;
- }
- }
-
private void handleScroll(View view, int x, int y, int oldx, int oldy) {
if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
mChooserMultiProfilePagerAdapter.getCurrentRootAdapter().handleScroll(view, y, oldy);
@@ -2532,8 +1639,8 @@ public class ChooserActivity extends ResolverActivity implements
}
final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
- boolean isLayoutUpdated = gridAdapter.consumeLayoutRequest()
- || gridAdapter.calculateChooserTargetWidth(availableWidth)
+ boolean isLayoutUpdated =
+ gridAdapter.calculateChooserTargetWidth(availableWidth)
|| recyclerView.getAdapter() == null
|| availableWidth != mCurrAvailableWidth;
@@ -2639,7 +1746,7 @@ public class ChooserActivity extends ResolverActivity implements
boolean isExpandable = getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode();
- if (directShareHeight != 0 && isSendAction(getTargetIntent())
+ if (directShareHeight != 0 && shouldShowContentPreview()
&& isExpandable) {
// make sure to leave room for direct share 4->8 expansion
int requiredExpansionHeight =
@@ -2688,15 +1795,7 @@ public class ChooserActivity extends ResolverActivity implements
private ViewGroup getActiveEmptyStateView() {
int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage();
- return mChooserMultiProfilePagerAdapter.getItem(currentPage).getEmptyStateView();
- }
-
- static class BaseChooserTargetComparator implements Comparator<ChooserTarget> {
- @Override
- public int compare(ChooserTarget lhs, ChooserTarget rhs) {
- // Descending order
- return (int) Math.signum(rhs.getScore() - lhs.getScore());
- }
+ return mChooserMultiProfilePagerAdapter.getEmptyStateView(currentPage);
}
@Override // ResolverListCommunicator
@@ -2705,29 +1804,6 @@ public class ChooserActivity extends ResolverActivity implements
super.onHandlePackagesChanged(listAdapter);
}
- @Override // SelectableTargetInfoCommunicator
- public ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info) {
- return mChooserMultiProfilePagerAdapter.getActiveListAdapter().makePresentationGetter(info);
- }
-
- @Override // SelectableTargetInfoCommunicator
- public Intent getReferrerFillInIntent() {
- return mReferrerFillInIntent;
- }
-
- @Override // ChooserListCommunicator
- public int getMaxRankedTargets() {
- return mMaxTargetsPerRow;
- }
-
- @Override // ChooserListCommunicator
- public void sendListViewUpdateMessage(UserHandle userHandle) {
- Message msg = Message.obtain();
- msg.what = ChooserHandler.LIST_VIEW_UPDATE_MESSAGE;
- msg.obj = userHandle;
- mChooserHandler.sendMessageDelayed(msg, mListViewUpdateDelayMs);
- }
-
@Override
public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
setupScrollListener();
@@ -2742,8 +1818,7 @@ public class ChooserActivity extends ResolverActivity implements
.setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
}
- if (chooserListAdapter.mDisplayList == null
- || chooserListAdapter.mDisplayList.isEmpty()) {
+ if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
chooserListAdapter.notifyDataSetChanged();
} else {
chooserListAdapter.updateAlphabeticalList();
@@ -2757,41 +1832,45 @@ public class ChooserActivity extends ResolverActivity implements
}
private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) {
- // don't support direct share on low ram devices
- if (ActivityManager.isLowRamDeviceStatic()) {
+ UserHandle userHandle = chooserListAdapter.getUserHandle();
+ ProfileRecord record = getProfileRecord(userHandle);
+ if (record == null) {
return;
}
-
- // no need to query direct share for work profile when its locked or disabled
- if (!shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
+ if (record.shortcutLoader == null) {
return;
}
+ record.loadingStartTime = SystemClock.elapsedRealtime();
+ record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos());
+ }
- if (ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) {
- if (DEBUG) {
- Log.d(TAG, "querying direct share targets from ShortcutManager");
+ @MainThread
+ private void onShortcutsLoaded(
+ UserHandle userHandle, ShortcutLoader.Result shortcutsResult) {
+ if (DEBUG) {
+ Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
+ }
+ mDirectShareShortcutInfoCache.putAll(shortcutsResult.directShareShortcutInfoCache);
+ mDirectShareAppTargetCache.putAll(shortcutsResult.directShareAppTargetCache);
+ ChooserListAdapter adapter =
+ mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
+ if (adapter != null) {
+ for (ShortcutLoader.ShortcutResultInfo resultInfo : shortcutsResult.shortcutsByApp) {
+ adapter.addServiceResults(
+ resultInfo.appTarget,
+ resultInfo.shortcuts,
+ shortcutsResult.isFromAppPredictor
+ ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+ : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
+ mDirectShareShortcutInfoCache,
+ mDirectShareAppTargetCache);
}
-
- queryDirectShareTargets(chooserListAdapter, false);
+ adapter.completeServiceTargetLoading();
}
- }
-
- @VisibleForTesting
- protected boolean isUserRunning(UserHandle userHandle) {
- UserManager userManager = getSystemService(UserManager.class);
- return userManager.isUserRunning(userHandle);
- }
- @VisibleForTesting
- protected boolean isUserUnlocked(UserHandle userHandle) {
- UserManager userManager = getSystemService(UserManager.class);
- return userManager.isUserUnlocked(userHandle);
- }
-
- @VisibleForTesting
- protected boolean isQuietModeEnabled(UserHandle userHandle) {
- UserManager userManager = getSystemService(UserManager.class);
- return userManager.isQuietModeEnabled(userHandle);
+ logDirectShareTargetReceived(userHandle);
+ sendVoiceChoicesIfNeeded();
+ getChooserActivityLogger().logSharesheetDirectLoadComplete();
}
private void setupScrollListener() {
@@ -2855,24 +1934,6 @@ public class ChooserActivity extends ResolverActivity implements
});
}
- @Override // ChooserListCommunicator
- public boolean isSendAction(Intent targetIntent) {
- if (targetIntent == null) {
- return false;
- }
-
- String action = targetIntent.getAction();
- if (action == null) {
- return false;
- }
-
- if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
- return true;
- }
-
- return false;
- }
-
/**
* The sticky content preview is shown only when we have a tabbed view. It's shown above
* the tabs so it is not part of the scrollable list. If we are not in tabbed view,
@@ -2887,7 +1948,14 @@ public class ChooserActivity extends ResolverActivity implements
return shouldShowTabs()
&& mMultiProfilePagerAdapter.getListAdapterForUserHandle(
UserHandle.of(UserHandle.myUserId())).getCount() > 0
- && isSendAction(getTargetIntent());
+ && shouldShowContentPreview();
+ }
+
+ /**
+ * @return true if we want to show the content preview area
+ */
+ protected boolean shouldShowContentPreview() {
+ return (mChooserRequest != null) && mChooserRequest.isSendActionTarget();
}
private void updateStickyContentPreview() {
@@ -2898,7 +1966,8 @@ public class ChooserActivity extends ResolverActivity implements
// then always preload it to avoid subsequent resizing of the share sheet.
ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
if (contentPreviewContainer.getChildCount() == 0) {
- ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
+ ViewGroup contentPreviewView =
+ createContentPreviewView(contentPreviewContainer, mPreviewCoordinator);
contentPreviewContainer.addView(contentPreviewView);
}
}
@@ -2930,21 +1999,16 @@ public class ChooserActivity extends ResolverActivity implements
contentPreviewContainer.setVisibility(View.GONE);
}
- private void logActionShareWithPreview() {
- Intent targetIntent = getTargetIntent();
- int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
- getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)
- .setSubtype(previewType));
- }
-
private void startFinishAnimation() {
View rootView = findRootView();
- rootView.startAnimation(new FinishAnimation(this, rootView));
+ if (rootView != null) {
+ rootView.startAnimation(new FinishAnimation(this, rootView));
+ }
}
private boolean maybeCancelFinishAnimation() {
View rootView = findRootView();
- Animation animation = rootView.getAnimation();
+ Animation animation = (rootView == null) ? null : rootView.getAnimation();
if (animation instanceof FinishAnimation) {
boolean hasEnded = animation.hasEnded();
animation.cancel();
@@ -2961,69 +2025,6 @@ public class ChooserActivity extends ResolverActivity implements
return mContentView;
}
- abstract static class ViewHolderBase extends RecyclerView.ViewHolder {
- private int mViewType;
-
- ViewHolderBase(View itemView, int viewType) {
- super(itemView);
- this.mViewType = viewType;
- }
-
- int getViewType() {
- return mViewType;
- }
- }
-
- /**
- * Used to bind types of individual item including
- * {@link ChooserGridAdapter#VIEW_TYPE_NORMAL},
- * {@link ChooserGridAdapter#VIEW_TYPE_CONTENT_PREVIEW},
- * {@link ChooserGridAdapter#VIEW_TYPE_PROFILE},
- * and {@link ChooserGridAdapter#VIEW_TYPE_AZ_LABEL}.
- */
- final class ItemViewHolder extends ViewHolderBase {
- ResolverListAdapter.ViewHolder mWrappedViewHolder;
- int mListPosition = ChooserListAdapter.NO_POSITION;
-
- ItemViewHolder(View itemView, boolean isClickable, int viewType) {
- super(itemView, viewType);
- mWrappedViewHolder = new ResolverListAdapter.ViewHolder(itemView);
- if (isClickable) {
- itemView.setOnClickListener(v -> startSelected(mListPosition,
- false/* always */, true/* filterd */));
-
- itemView.setOnLongClickListener(v -> {
- final TargetInfo ti = mChooserMultiProfilePagerAdapter.getActiveListAdapter()
- .targetInfoForPosition(mListPosition, /* filtered */ true);
-
- // This should always be the case for ItemViewHolder, check for validity
- if (ti instanceof DisplayResolveInfo && shouldShowTargetDetails(ti)) {
- showTargetDetails((DisplayResolveInfo) ti);
- }
- return true;
- });
- }
- }
- }
-
- private boolean shouldShowTargetDetails(TargetInfo ti) {
- ComponentName nearbyShare = getNearbySharingComponent();
- // Suppress target details for nearby share to hide pin/unpin action
- boolean isNearbyShare = nearbyShare != null && nearbyShare.equals(
- ti.getResolvedComponentName()) && shouldNearbyShareBeFirstInRankedRow();
- return ti instanceof SelectableTargetInfo
- || (ti instanceof DisplayResolveInfo && !isNearbyShare);
- }
-
- /**
- * Add a footer to the list, to support scrolling behavior below the navbar.
- */
- static final class FooterViewHolder extends ViewHolderBase {
- FooterViewHolder(View itemView, int viewType) {
- super(itemView, viewType);
- }
- }
-
/**
* Intentionally override the {@link ResolverActivity} implementation as we only need that
* implementation for the intent resolver case.
@@ -3100,763 +2101,6 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- /**
- * Adapter for all types of items and targets in ShareSheet.
- * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
- * row level by this adapter but not on the item level. Individual targets within the row are
- * handled by {@link ChooserListAdapter}
- */
- @VisibleForTesting
- public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
- private ChooserListAdapter mChooserListAdapter;
- private final LayoutInflater mLayoutInflater;
-
- private DirectShareViewHolder mDirectShareViewHolder;
- private int mChooserTargetWidth = 0;
- private boolean mShowAzLabelIfPoss;
- private boolean mLayoutRequested = false;
-
- private int mFooterHeight = 0;
-
- private static final int VIEW_TYPE_DIRECT_SHARE = 0;
- private static final int VIEW_TYPE_NORMAL = 1;
- private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
- private static final int VIEW_TYPE_PROFILE = 3;
- private static final int VIEW_TYPE_AZ_LABEL = 4;
- private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
- private static final int VIEW_TYPE_FOOTER = 6;
-
- private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
-
- ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
- super();
- mChooserListAdapter = wrappedAdapter;
- mLayoutInflater = LayoutInflater.from(ChooserActivity.this);
-
- mShowAzLabelIfPoss = getNumSheetExpansions() < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL;
-
- wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
- @Override
- public void onChanged() {
- super.onChanged();
- notifyDataSetChanged();
- }
-
- @Override
- public void onInvalidated() {
- super.onInvalidated();
- notifyDataSetChanged();
- }
- });
- }
-
- public void setFooterHeight(int height) {
- mFooterHeight = height;
- }
-
- /**
- * Calculate the chooser target width to maximize space per item
- *
- * @param width The new row width to use for recalculation
- * @return true if the view width has changed
- */
- public boolean calculateChooserTargetWidth(int width) {
- if (width == 0) {
- return false;
- }
-
- // Limit width to the maximum width of the chooser activity
- int maxWidth = getResources().getDimensionPixelSize(R.dimen.chooser_width);
- width = Math.min(maxWidth, width);
-
- int newWidth = width / mMaxTargetsPerRow;
- if (newWidth != mChooserTargetWidth) {
- mChooserTargetWidth = newWidth;
- return true;
- }
-
- return false;
- }
-
- /**
- * Hides the list item content preview.
- * <p>Not to be confused with the sticky content preview which is above the
- * personal and work tabs.
- */
- public void hideContentPreview() {
- mLayoutRequested = true;
- notifyDataSetChanged();
- }
-
- public boolean consumeLayoutRequest() {
- boolean oldValue = mLayoutRequested;
- mLayoutRequested = false;
- return oldValue;
- }
-
- public int getRowCount() {
- return (int) (
- getSystemRowCount()
- + getProfileRowCount()
- + getServiceTargetRowCount()
- + getCallerAndRankedTargetRowCount()
- + getAzLabelRowCount()
- + Math.ceil(
- (float) mChooserListAdapter.getAlphaTargetCount()
- / mMaxTargetsPerRow)
- );
- }
-
- /**
- * Whether the "system" row of targets is displayed.
- * This area includes the content preview (if present) and action row.
- */
- public int getSystemRowCount() {
- // For the tabbed case we show the sticky content preview above the tabs,
- // please refer to shouldShowStickyContentPreview
- if (shouldShowTabs()) {
- return 0;
- }
-
- if (!isSendAction(getTargetIntent())) {
- return 0;
- }
-
- if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
- return 0;
- }
-
- return 1;
- }
-
- public int getProfileRowCount() {
- if (shouldShowTabs()) {
- return 0;
- }
- return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
- }
-
- public int getFooterRowCount() {
- return 1;
- }
-
- public int getCallerAndRankedTargetRowCount() {
- return (int) Math.ceil(
- ((float) mChooserListAdapter.getCallerTargetCount()
- + mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow);
- }
-
- // There can be at most one row in the listview, that is internally
- // a ViewGroup with 2 rows
- public int getServiceTargetRowCount() {
- if (isSendAction(getTargetIntent())
- && !ActivityManager.isLowRamDeviceStatic()) {
- return 1;
- }
- return 0;
- }
-
- public int getAzLabelRowCount() {
- // Only show a label if the a-z list is showing
- return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0;
- }
-
- @Override
- public int getItemCount() {
- return (int) (
- getSystemRowCount()
- + getProfileRowCount()
- + getServiceTargetRowCount()
- + getCallerAndRankedTargetRowCount()
- + getAzLabelRowCount()
- + mChooserListAdapter.getAlphaTargetCount()
- + getFooterRowCount()
- );
- }
-
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- switch (viewType) {
- case VIEW_TYPE_CONTENT_PREVIEW:
- return new ItemViewHolder(createContentPreviewView(parent), false, viewType);
- case VIEW_TYPE_PROFILE:
- return new ItemViewHolder(createProfileView(parent), false, viewType);
- case VIEW_TYPE_AZ_LABEL:
- return new ItemViewHolder(createAzLabelView(parent), false, viewType);
- case VIEW_TYPE_NORMAL:
- return new ItemViewHolder(
- mChooserListAdapter.createView(parent), true, viewType);
- case VIEW_TYPE_DIRECT_SHARE:
- case VIEW_TYPE_CALLER_AND_RANK:
- return createItemGroupViewHolder(viewType, parent);
- case VIEW_TYPE_FOOTER:
- Space sp = new Space(parent.getContext());
- sp.setLayoutParams(new RecyclerView.LayoutParams(
- LayoutParams.MATCH_PARENT, mFooterHeight));
- return new FooterViewHolder(sp, viewType);
- default:
- // Since we catch all possible viewTypes above, no chance this is being called.
- return null;
- }
- }
-
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
- int viewType = ((ViewHolderBase) holder).getViewType();
- switch (viewType) {
- case VIEW_TYPE_DIRECT_SHARE:
- case VIEW_TYPE_CALLER_AND_RANK:
- bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder);
- break;
- case VIEW_TYPE_NORMAL:
- bindItemViewHolder(position, (ItemViewHolder) holder);
- break;
- default:
- }
- }
-
- @Override
- public int getItemViewType(int position) {
- int count;
-
- int countSum = (count = getSystemRowCount());
- if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW;
-
- countSum += (count = getProfileRowCount());
- if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE;
-
- countSum += (count = getServiceTargetRowCount());
- if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
-
- countSum += (count = getCallerAndRankedTargetRowCount());
- if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK;
-
- countSum += (count = getAzLabelRowCount());
- if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL;
-
- if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER;
-
- return VIEW_TYPE_NORMAL;
- }
-
- public int getTargetType(int position) {
- return mChooserListAdapter.getPositionTargetType(getListPosition(position));
- }
-
- private View createProfileView(ViewGroup parent) {
- View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false);
- mProfileView = profileRow.findViewById(com.android.internal.R.id.profile_button);
- mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
- updateProfileViewButton();
- return profileRow;
- }
-
- private View createAzLabelView(ViewGroup parent) {
- return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
- }
-
- private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) {
- final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth,
- MeasureSpec.EXACTLY);
- int columnCount = holder.getColumnCount();
-
- final boolean isDirectShare = holder instanceof DirectShareViewHolder;
-
- for (int i = 0; i < columnCount; i++) {
- final View v = mChooserListAdapter.createView(holder.getRowByIndex(i));
- final int column = i;
- v.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- startSelected(holder.getItemIndex(column), false, true);
- }
- });
-
- // Show menu for both direct share and app share targets after long click.
- v.setOnLongClickListener(v1 -> {
- TargetInfo ti = mChooserListAdapter.targetInfoForPosition(
- holder.getItemIndex(column), true);
- if (shouldShowTargetDetails(ti)) {
- showTargetDetails(ti);
- }
- return true;
- });
-
- holder.addView(i, v);
-
- // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
- // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be
- // done before measuring.
- if (isDirectShare) {
- final ViewHolder vh = (ViewHolder) v.getTag();
- vh.text.setLines(2);
- vh.text.setHorizontallyScrolling(false);
- vh.text2.setVisibility(View.GONE);
- }
-
- // Force height to be a given so we don't have visual disruption during scaling.
- v.measure(exactSpec, spec);
- setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight());
- }
-
- final ViewGroup viewGroup = holder.getViewGroup();
-
- // Pre-measure and fix height so we can scale later.
- holder.measure();
- setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight());
-
- if (isDirectShare) {
- DirectShareViewHolder dsvh = (DirectShareViewHolder) holder;
- setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
- setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
- }
-
- viewGroup.setTag(holder);
- return holder;
- }
-
- private void setViewBounds(View view, int widthPx, int heightPx) {
- LayoutParams lp = view.getLayoutParams();
- if (lp == null) {
- lp = new LayoutParams(widthPx, heightPx);
- view.setLayoutParams(lp);
- } else {
- lp.height = heightPx;
- lp.width = widthPx;
- }
- }
-
- ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) {
- if (viewType == VIEW_TYPE_DIRECT_SHARE) {
- ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate(
- R.layout.chooser_row_direct_share, parent, false);
- ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row,
- parentGroup, false);
- ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row,
- parentGroup, false);
- parentGroup.addView(row1);
- parentGroup.addView(row2);
-
- mDirectShareViewHolder = new DirectShareViewHolder(parentGroup,
- Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType,
- mChooserMultiProfilePagerAdapter::getActiveListAdapter);
- loadViewsIntoGroup(mDirectShareViewHolder);
-
- return mDirectShareViewHolder;
- } else {
- ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, parent,
- false);
- ItemGroupViewHolder holder =
- new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType);
- loadViewsIntoGroup(holder);
-
- return holder;
- }
- }
-
- /**
- * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from
- * showing on top of the AZ list if the AZ label is visible. All other types are placed into
- * their own row as determined by their target type, and dividers are added in the list to
- * separate each type.
- */
- int getRowType(int rowPosition) {
- // Merge caller and ranked standard into a single row
- int positionType = mChooserListAdapter.getPositionTargetType(rowPosition);
- if (positionType == ChooserListAdapter.TARGET_CALLER) {
- return ChooserListAdapter.TARGET_STANDARD;
- }
-
- // If an the A-Z label is shown, prevent a separator from appearing by making the A-Z
- // row type the same as the suggestion row type
- if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) {
- return ChooserListAdapter.TARGET_STANDARD;
- }
-
- return positionType;
- }
-
- void bindItemViewHolder(int position, ItemViewHolder holder) {
- View v = holder.itemView;
- int listPosition = getListPosition(position);
- holder.mListPosition = listPosition;
- mChooserListAdapter.bindView(listPosition, v);
- }
-
- void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) {
- final ViewGroup viewGroup = (ViewGroup) holder.itemView;
- int start = getListPosition(position);
- int startType = getRowType(start);
-
- int columnCount = holder.getColumnCount();
- int end = start + columnCount - 1;
- while (getRowType(end) != startType && end >= start) {
- end--;
- }
-
- if (end == start && mChooserListAdapter.getItem(start) instanceof EmptyTargetInfo) {
- final TextView textView = viewGroup.findViewById(com.android.internal.R.id.chooser_row_text_option);
-
- if (textView.getVisibility() != View.VISIBLE) {
- textView.setAlpha(0.0f);
- textView.setVisibility(View.VISIBLE);
- textView.setText(R.string.chooser_no_direct_share_targets);
-
- ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f);
- fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
-
- float translationInPx = getResources().getDimensionPixelSize(
- R.dimen.chooser_row_text_option_translate);
- textView.setTranslationY(translationInPx);
- ValueAnimator translateAnim = ObjectAnimator.ofFloat(textView, "translationY",
- 0.0f);
- translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
-
- AnimatorSet animSet = new AnimatorSet();
- animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
- animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
- animSet.playTogether(fadeAnim, translateAnim);
- animSet.start();
- }
- }
-
- for (int i = 0; i < columnCount; i++) {
- final View v = holder.getView(i);
-
- if (start + i <= end) {
- holder.setViewVisibility(i, View.VISIBLE);
- holder.setItemIndex(i, start + i);
- mChooserListAdapter.bindView(holder.getItemIndex(i), v);
- } else {
- holder.setViewVisibility(i, View.INVISIBLE);
- }
- }
- }
-
- int getListPosition(int position) {
- position -= getSystemRowCount() + getProfileRowCount();
-
- final int serviceCount = mChooserListAdapter.getServiceTargetCount();
- final int serviceRows = (int) Math.ceil((float) serviceCount / getMaxRankedTargets());
- if (position < serviceRows) {
- return position * mMaxTargetsPerRow;
- }
-
- position -= serviceRows;
-
- final int callerAndRankedCount = mChooserListAdapter.getCallerTargetCount()
- + mChooserListAdapter.getRankedTargetCount();
- final int callerAndRankedRows = getCallerAndRankedTargetRowCount();
- if (position < callerAndRankedRows) {
- return serviceCount + position * mMaxTargetsPerRow;
- }
-
- position -= getAzLabelRowCount() + callerAndRankedRows;
-
- return callerAndRankedCount + serviceCount + position;
- }
-
- public void handleScroll(View v, int y, int oldy) {
- boolean canExpandDirectShare = canExpandDirectShare();
- if (mDirectShareViewHolder != null && canExpandDirectShare) {
- mDirectShareViewHolder.handleScroll(
- mChooserMultiProfilePagerAdapter.getActiveAdapterView(), y, oldy,
- mMaxTargetsPerRow);
- }
- }
-
- /**
- * Only expand direct share area if there is a minimum number of targets.
- */
- private boolean canExpandDirectShare() {
- // Do not enable until we have confirmed more apps are using sharing shortcuts
- // Check git history for enablement logic
- return false;
- }
-
- public ChooserListAdapter getListAdapter() {
- return mChooserListAdapter;
- }
-
- boolean shouldCellSpan(int position) {
- return getItemViewType(position) == VIEW_TYPE_NORMAL;
- }
-
- void updateDirectShareExpansion() {
- if (mDirectShareViewHolder == null || !canExpandDirectShare()) {
- return;
- }
- RecyclerView activeAdapterView =
- mChooserMultiProfilePagerAdapter.getActiveAdapterView();
- if (mResolverDrawerLayout.isCollapsed()) {
- mDirectShareViewHolder.collapse(activeAdapterView);
- } else {
- mDirectShareViewHolder.expand(activeAdapterView);
- }
- }
- }
-
- /**
- * Used to bind types for group of items including:
- * {@link ChooserGridAdapter#VIEW_TYPE_DIRECT_SHARE},
- * and {@link ChooserGridAdapter#VIEW_TYPE_CALLER_AND_RANK}.
- */
- abstract static class ItemGroupViewHolder extends ViewHolderBase {
- protected int mMeasuredRowHeight;
- private int[] mItemIndices;
- protected final View[] mCells;
- private final int mColumnCount;
-
- ItemGroupViewHolder(int cellCount, View itemView, int viewType) {
- super(itemView, viewType);
- this.mCells = new View[cellCount];
- this.mItemIndices = new int[cellCount];
- this.mColumnCount = cellCount;
- }
-
- abstract ViewGroup addView(int index, View v);
-
- abstract ViewGroup getViewGroup();
-
- abstract ViewGroup getRowByIndex(int index);
-
- abstract ViewGroup getRow(int rowNumber);
-
- abstract void setViewVisibility(int i, int visibility);
-
- public int getColumnCount() {
- return mColumnCount;
- }
-
- public void measure() {
- final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- getViewGroup().measure(spec, spec);
- mMeasuredRowHeight = getViewGroup().getMeasuredHeight();
- }
-
- public int getMeasuredRowHeight() {
- return mMeasuredRowHeight;
- }
-
- public void setItemIndex(int itemIndex, int listIndex) {
- mItemIndices[itemIndex] = listIndex;
- }
-
- public int getItemIndex(int itemIndex) {
- return mItemIndices[itemIndex];
- }
-
- public View getView(int index) {
- return mCells[index];
- }
- }
-
- static class SingleRowViewHolder extends ItemGroupViewHolder {
- private final ViewGroup mRow;
-
- SingleRowViewHolder(ViewGroup row, int cellCount, int viewType) {
- super(cellCount, row, viewType);
-
- this.mRow = row;
- }
-
- public ViewGroup getViewGroup() {
- return mRow;
- }
-
- public ViewGroup getRowByIndex(int index) {
- return mRow;
- }
-
- public ViewGroup getRow(int rowNumber) {
- if (rowNumber == 0) return mRow;
- return null;
- }
-
- public ViewGroup addView(int index, View v) {
- mRow.addView(v);
- mCells[index] = v;
-
- return mRow;
- }
-
- public void setViewVisibility(int i, int visibility) {
- getView(i).setVisibility(visibility);
- }
- }
-
- static class DirectShareViewHolder extends ItemGroupViewHolder {
- private final ViewGroup mParent;
- private final List<ViewGroup> mRows;
- private int mCellCountPerRow;
-
- private boolean mHideDirectShareExpansion = false;
- private int mDirectShareMinHeight = 0;
- private int mDirectShareCurrHeight = 0;
- private int mDirectShareMaxHeight = 0;
-
- private final boolean[] mCellVisibility;
-
- private final Supplier<ChooserListAdapter> mListAdapterSupplier;
-
- DirectShareViewHolder(ViewGroup parent, List<ViewGroup> rows, int cellCountPerRow,
- int viewType, Supplier<ChooserListAdapter> listAdapterSupplier) {
- super(rows.size() * cellCountPerRow, parent, viewType);
-
- this.mParent = parent;
- this.mRows = rows;
- this.mCellCountPerRow = cellCountPerRow;
- this.mCellVisibility = new boolean[rows.size() * cellCountPerRow];
- this.mListAdapterSupplier = listAdapterSupplier;
- }
-
- public ViewGroup addView(int index, View v) {
- ViewGroup row = getRowByIndex(index);
- row.addView(v);
- mCells[index] = v;
-
- return row;
- }
-
- public ViewGroup getViewGroup() {
- return mParent;
- }
-
- public ViewGroup getRowByIndex(int index) {
- return mRows.get(index / mCellCountPerRow);
- }
-
- public ViewGroup getRow(int rowNumber) {
- return mRows.get(rowNumber);
- }
-
- public void measure() {
- final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- getRow(0).measure(spec, spec);
- getRow(1).measure(spec, spec);
-
- mDirectShareMinHeight = getRow(0).getMeasuredHeight();
- mDirectShareCurrHeight = mDirectShareCurrHeight > 0
- ? mDirectShareCurrHeight : mDirectShareMinHeight;
- mDirectShareMaxHeight = 2 * mDirectShareMinHeight;
- }
-
- public int getMeasuredRowHeight() {
- return mDirectShareCurrHeight;
- }
-
- public int getMinRowHeight() {
- return mDirectShareMinHeight;
- }
-
- public void setViewVisibility(int i, int visibility) {
- final View v = getView(i);
- if (visibility == View.VISIBLE) {
- mCellVisibility[i] = true;
- v.setVisibility(visibility);
- v.setAlpha(1.0f);
- } else if (visibility == View.INVISIBLE && mCellVisibility[i]) {
- mCellVisibility[i] = false;
-
- ValueAnimator fadeAnim = ObjectAnimator.ofFloat(v, "alpha", 1.0f, 0f);
- fadeAnim.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
- fadeAnim.setInterpolator(new AccelerateInterpolator(1.0f));
- fadeAnim.addListener(new AnimatorListenerAdapter() {
- public void onAnimationEnd(Animator animation) {
- v.setVisibility(View.INVISIBLE);
- }
- });
- fadeAnim.start();
- }
- }
-
- public void handleScroll(RecyclerView view, int y, int oldy, int maxTargetsPerRow) {
- // only exit early if fully collapsed, otherwise onListRebuilt() with shifting
- // targets can lock us into an expanded mode
- boolean notExpanded = mDirectShareCurrHeight == mDirectShareMinHeight;
- if (notExpanded) {
- if (mHideDirectShareExpansion) {
- return;
- }
-
- // only expand if we have more than maxTargetsPerRow, and delay that decision
- // until they start to scroll
- ChooserListAdapter adapter = mListAdapterSupplier.get();
- int validTargets = adapter.getSelectableServiceTargetCount();
- if (validTargets <= maxTargetsPerRow) {
- mHideDirectShareExpansion = true;
- return;
- }
- }
-
- int yDiff = (int) ((oldy - y) * DIRECT_SHARE_EXPANSION_RATE);
-
- int prevHeight = mDirectShareCurrHeight;
- int newHeight = Math.min(prevHeight + yDiff, mDirectShareMaxHeight);
- newHeight = Math.max(newHeight, mDirectShareMinHeight);
- yDiff = newHeight - prevHeight;
-
- updateDirectShareRowHeight(view, yDiff, newHeight);
- }
-
- void expand(RecyclerView view) {
- updateDirectShareRowHeight(view, mDirectShareMaxHeight - mDirectShareCurrHeight,
- mDirectShareMaxHeight);
- }
-
- void collapse(RecyclerView view) {
- updateDirectShareRowHeight(view, mDirectShareMinHeight - mDirectShareCurrHeight,
- mDirectShareMinHeight);
- }
-
- private void updateDirectShareRowHeight(RecyclerView view, int yDiff, int newHeight) {
- if (view == null || view.getChildCount() == 0 || yDiff == 0) {
- return;
- }
-
- // locate the item to expand, and offset the rows below that one
- boolean foundExpansion = false;
- for (int i = 0; i < view.getChildCount(); i++) {
- View child = view.getChildAt(i);
-
- if (foundExpansion) {
- child.offsetTopAndBottom(yDiff);
- } else {
- if (child.getTag() != null && child.getTag() instanceof DirectShareViewHolder) {
- int widthSpec = MeasureSpec.makeMeasureSpec(child.getWidth(),
- MeasureSpec.EXACTLY);
- int heightSpec = MeasureSpec.makeMeasureSpec(newHeight,
- MeasureSpec.EXACTLY);
- child.measure(widthSpec, heightSpec);
- child.getLayoutParams().height = child.getMeasuredHeight();
- child.layout(child.getLeft(), child.getTop(), child.getRight(),
- child.getTop() + child.getMeasuredHeight());
-
- foundExpansion = true;
- }
- }
- }
-
- if (foundExpansion) {
- mDirectShareCurrHeight = newHeight;
- }
- }
- }
-
- static class ServiceResultInfo {
- public final DisplayResolveInfo originalTarget;
- public final List<ChooserTarget> resultTargets;
- public final UserHandle userHandle;
-
- public ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt,
- UserHandle userHandle) {
- originalTarget = ot;
- resultTargets = rt;
- this.userHandle = userHandle;
- }
- }
-
static class ChooserTargetRankingInfo {
public final List<AppTarget> scores;
public final UserHandle userHandle;
@@ -3918,164 +2162,17 @@ public class ChooserActivity extends ResolverActivity implements
}
/**
- * Used internally to round image corners while obeying view padding.
- */
- public static class RoundedRectImageView extends ImageView {
- private int mRadius = 0;
- private Path mPath = new Path();
- private Paint mOverlayPaint = new Paint(0);
- private Paint mRoundRectPaint = new Paint(0);
- private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private String mExtraImageCount = null;
-
- public RoundedRectImageView(Context context) {
- super(context);
- }
-
- public RoundedRectImageView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius);
-
- mOverlayPaint.setColor(0x99000000);
- mOverlayPaint.setStyle(Paint.Style.FILL);
-
- mRoundRectPaint.setColor(context.getResources().getColor(R.color.chooser_row_divider));
- mRoundRectPaint.setStyle(Paint.Style.STROKE);
- mRoundRectPaint.setStrokeWidth(context.getResources()
- .getDimensionPixelSize(R.dimen.chooser_preview_image_border));
-
- mTextPaint.setColor(Color.WHITE);
- mTextPaint.setTextSize(context.getResources()
- .getDimensionPixelSize(R.dimen.chooser_preview_image_font_size));
- mTextPaint.setTextAlign(Paint.Align.CENTER);
- }
-
- private void updatePath(int width, int height) {
- mPath.reset();
-
- int imageWidth = width - getPaddingRight() - getPaddingLeft();
- int imageHeight = height - getPaddingBottom() - getPaddingTop();
- mPath.addRoundRect(getPaddingLeft(), getPaddingTop(), imageWidth, imageHeight, mRadius,
- mRadius, Path.Direction.CW);
- }
-
- /**
- * Sets the corner radius on all corners
- *
- * param radius 0 for no radius, &gt; 0 for a visible corner radius
- */
- public void setRadius(int radius) {
- mRadius = radius;
- updatePath(getWidth(), getHeight());
- }
-
- /**
- * Display an overlay with extra image count on 3rd image
- */
- public void setExtraImageCount(int count) {
- if (count > 0) {
- this.mExtraImageCount = "+" + count;
- } else {
- this.mExtraImageCount = null;
- }
- }
-
- @Override
- protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
- super.onSizeChanged(width, height, oldWidth, oldHeight);
- updatePath(width, height);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- if (mRadius != 0) {
- canvas.clipPath(mPath);
- }
-
- super.onDraw(canvas);
-
- int x = getPaddingLeft();
- int y = getPaddingRight();
- int width = getWidth() - getPaddingRight() - getPaddingLeft();
- int height = getHeight() - getPaddingBottom() - getPaddingTop();
- if (mExtraImageCount != null) {
- canvas.drawRect(x, y, width, height, mOverlayPaint);
-
- int xPos = canvas.getWidth() / 2;
- int yPos = (int) ((canvas.getHeight() / 2.0f)
- - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f));
-
- canvas.drawText(mExtraImageCount, xPos, yPos, mTextPaint);
- }
-
- canvas.drawRoundRect(x, y, width, height, mRadius, mRadius, mRoundRectPaint);
- }
- }
-
- /**
- * A helper class to track app's readiness for the scene transition animation.
- * The app is ready when both the image is laid out and the drawer offset is calculated.
- */
- private class EnterTransitionAnimationDelegate implements View.OnLayoutChangeListener {
- private boolean mPreviewReady = false;
- private boolean mOffsetCalculated = false;
-
- void postponeTransition() {
- postponeEnterTransition();
- }
-
- void markImagePreviewReady() {
- if (!mPreviewReady) {
- mPreviewReady = true;
- maybeStartListenForLayout();
- }
- }
-
- void markOffsetCalculated() {
- if (!mOffsetCalculated) {
- mOffsetCalculated = true;
- maybeStartListenForLayout();
- }
- }
-
- private void maybeStartListenForLayout() {
- if (mPreviewReady && mOffsetCalculated && mResolverDrawerLayout != null) {
- if (mResolverDrawerLayout.isInLayout()) {
- startPostponedEnterTransition();
- } else {
- mResolverDrawerLayout.addOnLayoutChangeListener(this);
- mResolverDrawerLayout.requestLayout();
- }
- }
- }
-
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
- int oldTop, int oldRight, int oldBottom) {
- v.removeOnLayoutChangeListener(this);
- startPostponedEnterTransition();
- }
- }
-
- /**
* Used in combination with the scene transition when launching the image editor
*/
private static class FinishAnimation extends AlphaAnimation implements
Animation.AnimationListener {
+ @Nullable
private Activity mActivity;
+ @Nullable
private View mRootView;
private final float mFromAlpha;
- FinishAnimation(Activity activity, View rootView) {
+ FinishAnimation(@NonNull Activity activity, @NonNull View rootView) {
super(rootView.getAlpha(), 0.0f);
mActivity = activity;
mRootView = rootView;
@@ -4099,7 +2196,9 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void cancel() {
- mRootView.setAlpha(mFromAlpha);
+ if (mRootView != null) {
+ mRootView.setAlpha(mFromAlpha);
+ }
cleanup();
super.cancel();
}
@@ -4110,9 +2209,10 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void onAnimationEnd(Animation animation) {
- if (mActivity != null) {
- mActivity.finish();
- cleanup();
+ Activity activity = mActivity;
+ cleanup();
+ if (activity != null) {
+ activity.finish();
}
}
@@ -4128,14 +2228,34 @@ public class ChooserActivity extends ResolverActivity implements
@Override
protected void maybeLogProfileChange() {
- getChooserActivityLogger().logShareheetProfileChanged();
+ getChooserActivityLogger().logSharesheetProfileChanged();
}
- private boolean shouldNearbyShareBeFirstInRankedRow() {
- return ActivityManager.isLowRamDeviceStatic() && mIsNearbyShareFirstTargetInRankedApp;
- }
+ private static class ProfileRecord {
+ /** The {@link AppPredictor} for this profile, if any. */
+ @Nullable
+ public final AppPredictor appPredictor;
+ /**
+ * null if we should not load shortcuts.
+ */
+ @Nullable
+ public final ShortcutLoader shortcutLoader;
+ public long loadingStartTime;
- private boolean shouldNearbyShareBeIncludedAsActionButton() {
- return !shouldNearbyShareBeFirstInRankedRow();
+ private ProfileRecord(
+ @Nullable AppPredictor appPredictor,
+ @Nullable ShortcutLoader shortcutLoader) {
+ this.appPredictor = appPredictor;
+ this.shortcutLoader = shortcutLoader;
+ }
+
+ public void destroy() {
+ if (shortcutLoader != null) {
+ shortcutLoader.destroy();
+ }
+ if (appPredictor != null) {
+ appPredictor.destroy();
+ }
+ }
}
}
diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/ChooserActivityLogger.java
index 1daae01a..9109bf93 100644
--- a/java/src/com/android/intentresolver/ChooserActivityLogger.java
+++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java
@@ -16,48 +16,228 @@
package com.android.intentresolver;
+import android.annotation.Nullable;
import android.content.Intent;
+import android.metrics.LogMaker;
+import android.net.Uri;
import android.provider.MediaStore;
+import android.util.HashedStringCache;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLoggerImpl;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FrameworkStatsLog;
/**
- * Interface for writing Sharesheet atoms to statsd log.
+ * Helper for writing Sharesheet atoms to statsd log.
* @hide
*/
-public interface ChooserActivityLogger {
+public class ChooserActivityLogger {
+ private static final String TAG = "ChooserActivity";
+ private static final boolean DEBUG = true;
+
+ public static final int SELECTION_TYPE_SERVICE = 1;
+ public static final int SELECTION_TYPE_APP = 2;
+ public static final int SELECTION_TYPE_STANDARD = 3;
+ public static final int SELECTION_TYPE_COPY = 4;
+ public static final int SELECTION_TYPE_NEARBY = 5;
+ public static final int SELECTION_TYPE_EDIT = 6;
+
+ /**
+ * This shim is provided only for testing. In production, clients will only ever use a
+ * {@link DefaultFrameworkStatsLogger}.
+ */
+ @VisibleForTesting
+ interface FrameworkStatsLogger {
+ /** Overload to use for logging {@code FrameworkStatsLog.SHARESHEET_STARTED}. */
+ void write(
+ int frameworkEventId,
+ int appEventId,
+ String packageName,
+ int instanceId,
+ String mimeType,
+ int numAppProvidedDirectTargets,
+ int numAppProvidedAppTargets,
+ boolean isWorkProfile,
+ int previewType,
+ int intentType);
+
+ /** Overload to use for logging {@code FrameworkStatsLog.RANKING_SELECTED}. */
+ void write(
+ int frameworkEventId,
+ int appEventId,
+ String packageName,
+ int instanceId,
+ int positionPicked,
+ boolean isPinned);
+ }
+
+ private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13);
+
+ // A small per-notification ID, used for statsd logging.
+ // TODO: consider precomputing and storing as final.
+ private static InstanceIdSequence sInstanceIdSequence;
+ private InstanceId mInstanceId;
+
+ private final UiEventLogger mUiEventLogger;
+ private final FrameworkStatsLogger mFrameworkStatsLogger;
+ private final MetricsLogger mMetricsLogger;
+
+ public ChooserActivityLogger() {
+ this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger());
+ }
+
+ @VisibleForTesting
+ ChooserActivityLogger(
+ UiEventLogger uiEventLogger,
+ FrameworkStatsLogger frameworkLogger,
+ MetricsLogger metricsLogger) {
+ mUiEventLogger = uiEventLogger;
+ mFrameworkStatsLogger = frameworkLogger;
+ mMetricsLogger = metricsLogger;
+ }
+
+ /** Records metrics for the start time of the {@link ChooserActivity}. */
+ public void logChooserActivityShown(
+ boolean isWorkProfile, String targetMimeType, long systemCost) {
+ mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)
+ .setSubtype(
+ isWorkProfile ? MetricsEvent.MANAGED_PROFILE : MetricsEvent.PARENT_PROFILE)
+ .addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, targetMimeType)
+ .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost));
+ }
+
/** Logs a UiEventReported event for the system sharesheet completing initial start-up. */
- void logShareStarted(int eventId, String packageName, String mimeType, int appProvidedDirect,
- int appProvidedApp, boolean isWorkprofile, int previewType, String intent);
+ public void logShareStarted(int eventId, String packageName, String mimeType,
+ int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
+ String intent) {
+ mFrameworkStatsLogger.write(FrameworkStatsLog.SHARESHEET_STARTED,
+ /* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),
+ /* package_name = 2 */ packageName,
+ /* instance_id = 3 */ getInstanceId().getId(),
+ /* mime_type = 4 */ mimeType,
+ /* num_app_provided_direct_targets = 5 */ appProvidedDirect,
+ /* num_app_provided_app_targets = 6 */ appProvidedApp,
+ /* is_workprofile = 7 */ isWorkprofile,
+ /* previewType = 8 */ typeFromPreviewInt(previewType),
+ /* intentType = 9 */ typeFromIntentString(intent));
+ }
- /** Logs a UiEventReported event for the system sharesheet when the user selects a target. */
- void logShareTargetSelected(int targetType, String packageName, int positionPicked,
- boolean isPinned);
+ /**
+ * Logs a UiEventReported event for the system sharesheet when the user selects a target.
+ * TODO: document parameters and/or consider breaking up by targetType so we don't have to
+ * support an overly-generic signature.
+ */
+ public void logShareTargetSelected(
+ int targetType,
+ String packageName,
+ int positionPicked,
+ int directTargetAlsoRanked,
+ int numCallerProvided,
+ @Nullable HashedStringCache.HashResult directTargetHashed,
+ boolean isPinned,
+ boolean successfullySelected,
+ long selectionCost) {
+ mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
+ /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
+ /* package_name = 2 */ packageName,
+ /* instance_id = 3 */ getInstanceId().getId(),
+ /* position_picked = 4 */ positionPicked,
+ /* is_pinned = 5 */ isPinned);
+
+ int category = getTargetSelectionCategory(targetType);
+ if (category != 0) {
+ LogMaker targetLogMaker = new LogMaker(category).setSubtype(positionPicked);
+ if (directTargetHashed != null) {
+ targetLogMaker.addTaggedData(
+ MetricsEvent.FIELD_HASHED_TARGET_NAME, directTargetHashed.hashedString);
+ targetLogMaker.addTaggedData(
+ MetricsEvent.FIELD_HASHED_TARGET_SALT_GEN,
+ directTargetHashed.saltGeneration);
+ targetLogMaker.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION,
+ directTargetAlsoRanked);
+ }
+ targetLogMaker.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, numCallerProvided);
+ mMetricsLogger.write(targetLogMaker);
+ }
+
+ if (successfullySelected) {
+ if (DEBUG) {
+ Log.d(TAG, "User Selection Time Cost is " + selectionCost);
+ Log.d(TAG, "position of selected app/service/caller is " + positionPicked);
+ }
+ MetricsLogger.histogram(
+ null, "user_selection_cost_for_smart_sharing", (int) selectionCost);
+ MetricsLogger.histogram(null, "app_position_for_smart_sharing", positionPicked);
+ }
+ }
+
+ /** Log when direct share targets were received. */
+ public void logDirectShareTargetReceived(int category, int latency) {
+ mMetricsLogger.write(new LogMaker(category).setSubtype(latency));
+ }
+
+ /**
+ * Log when we display a preview UI of the specified {@code previewType} as part of our
+ * Sharesheet session.
+ */
+ public void logActionShareWithPreview(int previewType) {
+ mMetricsLogger.write(
+ new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW).setSubtype(previewType));
+ }
+
+ /** Log when the user selects an action button with the specified {@code targetType}. */
+ public void logActionSelected(int targetType) {
+ if (targetType == SELECTION_TYPE_COPY) {
+ LogMaker targetLogMaker = new LogMaker(
+ MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET).setSubtype(1);
+ mMetricsLogger.write(targetLogMaker);
+ }
+ mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
+ /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
+ /* package_name = 2 */ "",
+ /* instance_id = 3 */ getInstanceId().getId(),
+ /* position_picked = 4 */ -1,
+ /* is_pinned = 5 */ false);
+ }
+
+ /** Log a warning that we couldn't display the content preview from the supplied {@code uri}. */
+ public void logContentPreviewWarning(Uri uri) {
+ // The ContentResolver already logs the exception. Log something more informative.
+ Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
+ + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
+ + "and set your Intent's clipData and flags in accordance with that method's "
+ + "documentation");
+
+ }
/** Logs a UiEventReported event for the system sharesheet being triggered by the user. */
- default void logSharesheetTriggered() {
+ public void logSharesheetTriggered() {
log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, getInstanceId());
}
/** Logs a UiEventReported event for the system sharesheet completing loading app targets. */
- default void logSharesheetAppLoadComplete() {
+ public void logSharesheetAppLoadComplete() {
log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, getInstanceId());
}
/**
* Logs a UiEventReported event for the system sharesheet completing loading service targets.
*/
- default void logSharesheetDirectLoadComplete() {
+ public void logSharesheetDirectLoadComplete() {
log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, getInstanceId());
}
/**
* Logs a UiEventReported event for the system sharesheet timing out loading service targets.
*/
- default void logSharesheetDirectLoadTimeout() {
+ public void logSharesheetDirectLoadTimeout() {
log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, getInstanceId());
}
@@ -65,12 +245,12 @@ public interface ChooserActivityLogger {
* Logs a UiEventReported event for the system sharesheet switching
* between work and main profile.
*/
- default void logShareheetProfileChanged() {
+ public void logSharesheetProfileChanged() {
log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, getInstanceId());
}
/** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */
- default void logSharesheetExpansionChanged(boolean isCollapsed) {
+ public void logSharesheetExpansionChanged(boolean isCollapsed) {
log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED :
SharesheetStandardEvent.SHARESHEET_EXPANDED, getInstanceId());
}
@@ -78,14 +258,14 @@ public interface ChooserActivityLogger {
/**
* Logs a UiEventReported event for the system sharesheet app share ranking timing out.
*/
- default void logSharesheetAppShareRankingTimeout() {
+ public void logSharesheetAppShareRankingTimeout() {
log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, getInstanceId());
}
/**
* Logs a UiEventReported event for the system sharesheet when direct share row is empty.
*/
- default void logSharesheetEmptyDirectShareRow() {
+ public void logSharesheetEmptyDirectShareRow() {
log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, getInstanceId());
}
@@ -94,13 +274,26 @@ public interface ChooserActivityLogger {
* @param event
* @param instanceId
*/
- void log(UiEventLogger.UiEventEnum event, InstanceId instanceId);
+ private void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) {
+ mUiEventLogger.logWithInstanceId(
+ event,
+ 0,
+ null,
+ instanceId);
+ }
/**
- *
- * @return
+ * @return A unique {@link InstanceId} to join across events recorded by this logger instance.
*/
- InstanceId getInstanceId();
+ private InstanceId getInstanceId() {
+ if (mInstanceId == null) {
+ if (sInstanceIdSequence == null) {
+ sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);
+ }
+ mInstanceId = sInstanceIdSequence.newInstanceId();
+ }
+ return mInstanceId;
+ }
/**
* The UiEvent enums that this class can log.
@@ -147,17 +340,17 @@ public interface ChooserActivityLogger {
public static SharesheetTargetSelectedEvent fromTargetType(int targetType) {
switch(targetType) {
- case ChooserActivity.SELECTION_TYPE_SERVICE:
+ case SELECTION_TYPE_SERVICE:
return SHARESHEET_SERVICE_TARGET_SELECTED;
- case ChooserActivity.SELECTION_TYPE_APP:
+ case SELECTION_TYPE_APP:
return SHARESHEET_APP_TARGET_SELECTED;
- case ChooserActivity.SELECTION_TYPE_STANDARD:
+ case SELECTION_TYPE_STANDARD:
return SHARESHEET_STANDARD_TARGET_SELECTED;
- case ChooserActivity.SELECTION_TYPE_COPY:
+ case SELECTION_TYPE_COPY:
return SHARESHEET_COPY_TARGET_SELECTED;
- case ChooserActivity.SELECTION_TYPE_NEARBY:
+ case SELECTION_TYPE_NEARBY:
return SHARESHEET_NEARBY_TARGET_SELECTED;
- case ChooserActivity.SELECTION_TYPE_EDIT:
+ case SELECTION_TYPE_EDIT:
return SHARESHEET_EDIT_TARGET_SELECTED;
default:
return INVALID;
@@ -201,13 +394,13 @@ public interface ChooserActivityLogger {
/**
* Returns the enum used in sharesheet started atom to indicate what preview type was used.
*/
- default int typeFromPreviewInt(int previewType) {
+ private static int typeFromPreviewInt(int previewType) {
switch(previewType) {
- case ChooserActivity.CONTENT_PREVIEW_IMAGE:
+ case ChooserContentPreviewUi.CONTENT_PREVIEW_IMAGE:
return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_IMAGE;
- case ChooserActivity.CONTENT_PREVIEW_FILE:
+ case ChooserContentPreviewUi.CONTENT_PREVIEW_FILE:
return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE;
- case ChooserActivity.CONTENT_PREVIEW_TEXT:
+ case ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT:
default:
return FrameworkStatsLog
.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_TYPE_UNKNOWN;
@@ -218,7 +411,7 @@ public interface ChooserActivityLogger {
* Returns the enum used in sharesheet started atom to indicate what intent triggers the
* ChooserActivity.
*/
- default int typeFromIntentString(String intent) {
+ private static int typeFromIntentString(String intent) {
if (intent == null) {
return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT;
}
@@ -243,4 +436,62 @@ public interface ChooserActivityLogger {
return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT;
}
}
+
+ @VisibleForTesting
+ static int getTargetSelectionCategory(int targetType) {
+ switch (targetType) {
+ case SELECTION_TYPE_SERVICE:
+ return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET;
+ case SELECTION_TYPE_APP:
+ return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET;
+ case SELECTION_TYPE_STANDARD:
+ return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET;
+ default:
+ return 0;
+ }
+ }
+
+ private static class DefaultFrameworkStatsLogger implements FrameworkStatsLogger {
+ @Override
+ public void write(
+ int frameworkEventId,
+ int appEventId,
+ String packageName,
+ int instanceId,
+ String mimeType,
+ int numAppProvidedDirectTargets,
+ int numAppProvidedAppTargets,
+ boolean isWorkProfile,
+ int previewType,
+ int intentType) {
+ FrameworkStatsLog.write(
+ frameworkEventId,
+ /* event_id = 1 */ appEventId,
+ /* package_name = 2 */ packageName,
+ /* instance_id = 3 */ instanceId,
+ /* mime_type = 4 */ mimeType,
+ /* num_app_provided_direct_targets */ numAppProvidedDirectTargets,
+ /* num_app_provided_app_targets */ numAppProvidedAppTargets,
+ /* is_workprofile */ isWorkProfile,
+ /* previewType = 8 */ previewType,
+ /* intentType = 9 */ intentType);
+ }
+
+ @Override
+ public void write(
+ int frameworkEventId,
+ int appEventId,
+ String packageName,
+ int instanceId,
+ int positionPicked,
+ boolean isPinned) {
+ FrameworkStatsLog.write(
+ frameworkEventId,
+ /* event_id = 1 */ appEventId,
+ /* package_name = 2 */ packageName,
+ /* instance_id = 3 */ instanceId,
+ /* position_picked = 4 */ positionPicked,
+ /* is_pinned = 5 */ isPinned);
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java b/java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java
deleted file mode 100644
index 08a345bc..00000000
--- a/java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2020 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;
-
-import com.android.internal.logging.InstanceId;
-import com.android.internal.logging.InstanceIdSequence;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.logging.UiEventLoggerImpl;
-import com.android.internal.util.FrameworkStatsLog;
-
-/**
- * Standard implementation of ChooserActivityLogger interface.
- * @hide
- */
-public class ChooserActivityLoggerImpl implements ChooserActivityLogger {
- private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13);
-
- private UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
- // A small per-notification ID, used for statsd logging.
- private InstanceId mInstanceId;
- private static InstanceIdSequence sInstanceIdSequence;
-
- @Override
- public void logShareStarted(int eventId, String packageName, String mimeType,
- int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
- String intent) {
- FrameworkStatsLog.write(FrameworkStatsLog.SHARESHEET_STARTED,
- /* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),
- /* package_name = 2 */ packageName,
- /* instance_id = 3 */ getInstanceId().getId(),
- /* mime_type = 4 */ mimeType,
- /* num_app_provided_direct_targets = 5 */ appProvidedDirect,
- /* num_app_provided_app_targets = 6 */ appProvidedApp,
- /* is_workprofile = 7 */ isWorkprofile,
- /* previewType = 8 */ typeFromPreviewInt(previewType),
- /* intentType = 9 */ typeFromIntentString(intent));
- }
-
- @Override
- public void logShareTargetSelected(int targetType, String packageName, int positionPicked,
- boolean isPinned) {
- FrameworkStatsLog.write(FrameworkStatsLog.RANKING_SELECTED,
- /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
- /* package_name = 2 */ packageName,
- /* instance_id = 3 */ getInstanceId().getId(),
- /* position_picked = 4 */ positionPicked,
- /* is_pinned = 5 */ isPinned);
- }
-
- @Override
- public void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) {
- mUiEventLogger.logWithInstanceId(
- event,
- 0,
- null,
- instanceId);
- }
-
- @Override
- public InstanceId getInstanceId() {
- if (mInstanceId == null) {
- if (sInstanceIdSequence == null) {
- sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);
- }
- mInstanceId = sInstanceIdSequence.newInstanceId();
- }
- return mInstanceId;
- }
-
-}
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java b/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
new file mode 100644
index 00000000..0b8dbe35
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2008 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;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.Size;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+
+/**
+ * Delegate to manage deferred resource loads for content preview assets, while
+ * implementing Chooser's application logic for determining timeout/success/failure conditions.
+ */
+public class ChooserContentPreviewCoordinator implements
+ ChooserContentPreviewUi.ContentPreviewCoordinator {
+ public ChooserContentPreviewCoordinator(
+ ExecutorService backgroundExecutor,
+ ChooserActivity chooserActivity,
+ Runnable onFailCallback) {
+ this.mBackgroundExecutor = MoreExecutors.listeningDecorator(backgroundExecutor);
+ this.mChooserActivity = chooserActivity;
+ this.mOnFailCallback = onFailCallback;
+
+ this.mImageLoadTimeoutMillis =
+ chooserActivity.getResources().getInteger(R.integer.config_shortAnimTime);
+ }
+
+ @Override
+ public void loadImage(final Uri imageUri, final Consumer<Bitmap> callback) {
+ final int size = mChooserActivity.getResources().getDimensionPixelSize(
+ R.dimen.chooser_preview_image_max_dimen);
+
+ // TODO: apparently this timeout is only used for not holding shared element transition
+ // animation for too long. If so, we already have a better place for it
+ // EnterTransitionAnimationDelegate.
+ mHandler.postDelayed(this::onWatchdogTimeout, mImageLoadTimeoutMillis);
+
+ ListenableFuture<Bitmap> bitmapFuture = mBackgroundExecutor.submit(
+ () -> mChooserActivity.loadThumbnail(imageUri, new Size(size, size)));
+
+ Futures.addCallback(
+ bitmapFuture,
+ new FutureCallback<Bitmap>() {
+ @Override
+ public void onSuccess(Bitmap loadedBitmap) {
+ try {
+ callback.accept(loadedBitmap);
+ onLoadCompleted(loadedBitmap);
+ } catch (Exception e) { /* unimportant */ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ callback.accept(null);
+ }
+ },
+ mHandler::post);
+ }
+
+ private final ChooserActivity mChooserActivity;
+ private final ListeningExecutorService mBackgroundExecutor;
+ private final Runnable mOnFailCallback;
+ private final int mImageLoadTimeoutMillis;
+
+ // TODO: this uses a `Handler` because there doesn't seem to be a straightforward way to get a
+ // `ScheduledExecutorService` that posts to the UI thread unless we use Dagger. Eventually we'll
+ // use Dagger and can inject this as a `@UiThread ScheduledExecutorService`.
+ private final Handler mHandler = new Handler();
+
+ private boolean mAtLeastOneLoaded = false;
+
+ @MainThread
+ private void onWatchdogTimeout() {
+ if (mChooserActivity.isFinishing()) {
+ return;
+ }
+
+ // If at least one image loads within the timeout period, allow other loads to continue.
+ if (!mAtLeastOneLoaded) {
+ mOnFailCallback.run();
+ }
+ }
+
+ @MainThread
+ private void onLoadCompleted(@Nullable Bitmap loadedBitmap) {
+ if (mChooserActivity.isFinishing()) {
+ return;
+ }
+
+ // TODO: the following logic can be described as "invoke the fail callback when the first
+ // image loading has failed". Historically, before we had switched from a single-threaded
+ // pool to a multi-threaded pool, we first loaded the transition element's image (the image
+ // preview is the only case when those callbacks matter) and aborting the animation on it's
+ // failure was reasonable. With the multi-thread pool, the first result may belong to any
+ // image and thus we can falsely abort the animation.
+ // Now, when we track the transition view state directly and after the timeout logic will
+ // be moved into ChooserActivity$EnterTransitionAnimationDelegate, we can just get rid of
+ // the fail callback and the following logic altogether.
+ mAtLeastOneLoaded |= loadedBitmap != null;
+ boolean wholeBatchFailed = !mAtLeastOneLoaded;
+
+ if (wholeBatchFailed) {
+ mOnFailCallback.run();
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
new file mode 100644
index 00000000..ff88e5e1
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
@@ -0,0 +1,566 @@
+/*
+ * 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.intentresolver;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.content.ClipData;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.Downloads;
+import android.provider.OpenableColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.PluralsMessageFormatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.ImagePreviewView;
+import com.android.intentresolver.widget.RoundedRectImageView;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Collection of helpers for building the content preview UI displayed in {@link ChooserActivity}.
+ *
+ * TODO: this "namespace" was pulled out of {@link ChooserActivity} as a bucket of static methods
+ * to show that they're one-shot procedures with no dependencies back to {@link ChooserActivity}
+ * state other than the delegates that are explicitly provided. There may be more appropriate
+ * abstractions (e.g., maybe this can be a "widget" added directly to the view hierarchy to show the
+ * appropriate preview), or it may at least be safe (and more convenient) to adopt a more "object
+ * oriented" design where the static specifiers are removed and some of the dependencies are cached
+ * as ivars when this "class" is initialized.
+ */
+public final class ChooserContentPreviewUi {
+ private static final int IMAGE_FADE_IN_MILLIS = 150;
+
+ /**
+ * Delegate to handle background resource loads that are dependencies of content previews.
+ */
+ public interface ContentPreviewCoordinator {
+ /**
+ * Request that an image be loaded in the background and set into a view.
+ *
+ * @param imageUri The {@link Uri} of the image to load.
+ *
+ * TODO: it looks like clients are probably capable of passing the view directly, but the
+ * deferred computation here is a closer match to the legacy model for now.
+ */
+ void loadImage(Uri imageUri, Consumer<Bitmap> callback);
+ }
+
+ /**
+ * Delegate to build the default system action buttons to display in the preview layout, if/when
+ * they're determined to be appropriate for the particular preview we display.
+ * TODO: clarify why action buttons are part of preview logic.
+ */
+ public interface ActionFactory {
+ /** Create an action that copies the share content to the clipboard. */
+ ActionRow.Action createCopyButton();
+
+ /** Create an action that opens the share content in a system-default editor. */
+ @Nullable
+ ActionRow.Action createEditButton();
+
+ /** Create an "Share to Nearby" action. */
+ @Nullable
+ ActionRow.Action createNearbyButton();
+ }
+
+ /**
+ * Testing shim to specify whether a given mime type is considered to be an "image."
+ *
+ * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests,
+ * then migrate {@link ChooserActivity#isImageType(String)} into this class.
+ */
+ public interface ImageMimeTypeClassifier {
+ /** @return whether the specified {@code mimeType} is classified as an "image" type. */
+ boolean isImageType(String mimeType);
+ }
+
+ @Retention(SOURCE)
+ @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT})
+ private @interface ContentPreviewType {
+ }
+
+ // Starting at 1 since 0 is considered "undefined" for some of the database transformations
+ // of tron logs.
+ @VisibleForTesting
+ public static final int CONTENT_PREVIEW_IMAGE = 1;
+ @VisibleForTesting
+ public static final int CONTENT_PREVIEW_FILE = 2;
+ @VisibleForTesting
+ public static final int CONTENT_PREVIEW_TEXT = 3;
+
+ private static final String TAG = "ChooserPreview";
+
+ private static final String PLURALS_COUNT = "count";
+ private static final String PLURALS_FILE_NAME = "file_name";
+
+ /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */
+ @ContentPreviewType
+ public static int findPreferredContentPreview(
+ Intent targetIntent,
+ ContentResolver resolver,
+ ImageMimeTypeClassifier imageClassifier) {
+ /* In {@link android.content.Intent#getType}, the app may specify a very general mime type
+ * that broadly covers all data being shared, such as {@literal *}/* when sending an image
+ * and text. We therefore should inspect each item for the preferred type, in order: IMAGE,
+ * FILE, TEXT. */
+ String action = targetIntent.getAction();
+ if (Intent.ACTION_SEND.equals(action)) {
+ Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ return findPreferredContentPreview(uri, resolver, imageClassifier);
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (uris == null || uris.isEmpty()) {
+ return CONTENT_PREVIEW_TEXT;
+ }
+
+ for (Uri uri : uris) {
+ // Defaulting to file preview when there are mixed image/file types is
+ // preferable, as it shows the user the correct number of items being shared
+ int uriPreviewType = findPreferredContentPreview(uri, resolver, imageClassifier);
+ if (uriPreviewType == CONTENT_PREVIEW_FILE) {
+ return CONTENT_PREVIEW_FILE;
+ }
+ }
+
+ return CONTENT_PREVIEW_IMAGE;
+ }
+
+ return CONTENT_PREVIEW_TEXT;
+ }
+
+ /**
+ * Display a content preview of the specified {@code previewType} to preview the content of the
+ * specified {@code intent}.
+ */
+ public static ViewGroup displayContentPreview(
+ @ContentPreviewType int previewType,
+ Intent targetIntent,
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ActionFactory actionFactory,
+ @LayoutRes int actionRowLayout,
+ ViewGroup parent,
+ ContentPreviewCoordinator previewCoord,
+ Consumer<Boolean> onTransitionTargetReady,
+ ContentResolver contentResolver,
+ ImageMimeTypeClassifier imageClassifier) {
+ ViewGroup layout = null;
+
+ switch (previewType) {
+ case CONTENT_PREVIEW_TEXT:
+ layout = displayTextContentPreview(
+ targetIntent,
+ layoutInflater,
+ createTextPreviewActions(actionFactory),
+ parent,
+ previewCoord,
+ actionRowLayout);
+ break;
+ case CONTENT_PREVIEW_IMAGE:
+ layout = displayImageContentPreview(
+ targetIntent,
+ layoutInflater,
+ createImagePreviewActions(actionFactory),
+ parent,
+ previewCoord,
+ onTransitionTargetReady,
+ contentResolver,
+ imageClassifier,
+ actionRowLayout);
+ break;
+ case CONTENT_PREVIEW_FILE:
+ layout = displayFileContentPreview(
+ targetIntent,
+ resources,
+ layoutInflater,
+ createFilePreviewActions(actionFactory),
+ parent,
+ previewCoord,
+ contentResolver,
+ actionRowLayout);
+ break;
+ default:
+ Log.e(TAG, "Unexpected content preview type: " + previewType);
+ }
+
+ return layout;
+ }
+
+ private static Cursor queryResolver(ContentResolver resolver, Uri uri) {
+ return resolver.query(uri, null, null, null, null);
+ }
+
+ @ContentPreviewType
+ private static int findPreferredContentPreview(
+ Uri uri, ContentResolver resolver, ImageMimeTypeClassifier imageClassifier) {
+ if (uri == null) {
+ return CONTENT_PREVIEW_TEXT;
+ }
+
+ String mimeType = resolver.getType(uri);
+ return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
+ }
+
+ private static ViewGroup displayTextContentPreview(
+ Intent targetIntent,
+ LayoutInflater layoutInflater,
+ List<ActionRow.Action> actions,
+ ViewGroup parent,
+ ContentPreviewCoordinator previewCoord,
+ @LayoutRes int actionRowLayout) {
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_text, parent, false);
+
+ final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
+
+ CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
+ if (sharingText == null) {
+ contentPreviewLayout
+ .findViewById(com.android.internal.R.id.content_preview_text_layout)
+ .setVisibility(View.GONE);
+ } else {
+ TextView textView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_text);
+ textView.setText(sharingText);
+ }
+
+ String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
+ if (TextUtils.isEmpty(previewTitle)) {
+ contentPreviewLayout
+ .findViewById(com.android.internal.R.id.content_preview_title_layout)
+ .setVisibility(View.GONE);
+ } else {
+ TextView previewTitleView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_title);
+ previewTitleView.setText(previewTitle);
+
+ ClipData previewData = targetIntent.getClipData();
+ Uri previewThumbnail = null;
+ if (previewData != null) {
+ if (previewData.getItemCount() > 0) {
+ ClipData.Item previewDataItem = previewData.getItemAt(0);
+ previewThumbnail = previewDataItem.getUri();
+ }
+ }
+
+ ImageView previewThumbnailView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_thumbnail);
+ if (previewThumbnail == null) {
+ previewThumbnailView.setVisibility(View.GONE);
+ } else {
+ previewCoord.loadImage(
+ previewThumbnail,
+ (bitmap) -> updateViewWithImage(
+ contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_thumbnail),
+ bitmap));
+ }
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private static List<ActionRow.Action> createTextPreviewActions(ActionFactory actionFactory) {
+ ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+ actions.add(actionFactory.createCopyButton());
+ ActionRow.Action nearbyAction = actionFactory.createNearbyButton();
+ if (nearbyAction != null) {
+ actions.add(nearbyAction);
+ }
+ return actions;
+ }
+
+ private static ViewGroup displayImageContentPreview(
+ Intent targetIntent,
+ LayoutInflater layoutInflater,
+ List<ActionRow.Action> actions,
+ ViewGroup parent,
+ ContentPreviewCoordinator previewCoord,
+ Consumer<Boolean> onTransitionTargetReady,
+ ContentResolver contentResolver,
+ ImageMimeTypeClassifier imageClassifier,
+ @LayoutRes int actionRowLayout) {
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_image, parent, false);
+ ImagePreviewView imagePreview = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_image_area);
+
+ final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
+
+ final ImagePreviewImageLoader imageLoader = new ImagePreviewImageLoader(previewCoord);
+ final ArrayList<Uri> imageUris = new ArrayList<>();
+ String action = targetIntent.getAction();
+ if (Intent.ACTION_SEND.equals(action)) {
+ // TODO: why don't we use image classifier in this case as well?
+ Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ imageUris.add(uri);
+ } else {
+ List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ for (Uri uri : uris) {
+ if (imageClassifier.isImageType(contentResolver.getType(uri))) {
+ imageUris.add(uri);
+ }
+ }
+ }
+
+ if (imageUris.size() == 0) {
+ Log.i(TAG, "Attempted to display image preview area with zero"
+ + " available images detected in EXTRA_STREAM list");
+ imagePreview.setVisibility(View.GONE);
+ onTransitionTargetReady.accept(false);
+ return contentPreviewLayout;
+ }
+
+ imagePreview.setSharedElementTransitionTarget(
+ ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME,
+ onTransitionTargetReady);
+ imagePreview.setImages(imageUris, imageLoader);
+
+ return contentPreviewLayout;
+ }
+
+ private static List<ActionRow.Action> createImagePreviewActions(
+ ActionFactory buttonFactory) {
+ ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+ //TODO: add copy action;
+ ActionRow.Action action = buttonFactory.createNearbyButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ action = buttonFactory.createEditButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ return actions;
+ }
+
+ private static ViewGroup displayFileContentPreview(
+ Intent targetIntent,
+ Resources resources,
+ LayoutInflater layoutInflater,
+ List<ActionRow.Action> actions,
+ ViewGroup parent,
+ ContentPreviewCoordinator previewCoord,
+ ContentResolver contentResolver,
+ @LayoutRes int actionRowLayout) {
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_file, parent, false);
+
+ final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
+
+ String action = targetIntent.getAction();
+ if (Intent.ACTION_SEND.equals(action)) {
+ Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ loadFileUriIntoView(uri, contentPreviewLayout, previewCoord, contentResolver);
+ } else {
+ List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ int uriCount = uris.size();
+
+ if (uriCount == 0) {
+ contentPreviewLayout.setVisibility(View.GONE);
+ Log.i(TAG,
+ "Appears to be no uris available in EXTRA_STREAM, removing "
+ + "preview area");
+ return contentPreviewLayout;
+ } else if (uriCount == 1) {
+ loadFileUriIntoView(
+ uris.get(0), contentPreviewLayout, previewCoord, contentResolver);
+ } else {
+ FileInfo fileInfo = extractFileInfo(uris.get(0), contentResolver);
+ int remUriCount = uriCount - 1;
+ Map<String, Object> arguments = new HashMap<>();
+ arguments.put(PLURALS_COUNT, remUriCount);
+ arguments.put(PLURALS_FILE_NAME, fileInfo.name);
+ String fileName =
+ PluralsMessageFormatter.format(resources, arguments, R.string.file_count);
+
+ TextView fileNameView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_filename);
+ fileNameView.setText(fileName);
+
+ View thumbnailView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_file_thumbnail);
+ thumbnailView.setVisibility(View.GONE);
+
+ ImageView fileIconView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_file_icon);
+ fileIconView.setVisibility(View.VISIBLE);
+ fileIconView.setImageResource(R.drawable.ic_file_copy);
+ }
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private static List<ActionRow.Action> createFilePreviewActions(ActionFactory actionFactory) {
+ List<ActionRow.Action> actions = new ArrayList<>(1);
+ //TODO(b/120417119):
+ // add action buttonFactory.createCopyButton()
+ ActionRow.Action action = actionFactory.createNearbyButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ return actions;
+ }
+
+ private static ActionRow inflateActionRow(ViewGroup parent, @LayoutRes int actionRowLayout) {
+ final ViewStub stub = parent.findViewById(com.android.intentresolver.R.id.action_row_stub);
+ if (stub != null) {
+ stub.setLayoutResource(actionRowLayout);
+ stub.inflate();
+ }
+ return parent.findViewById(com.android.internal.R.id.chooser_action_row);
+ }
+
+ private static void logContentPreviewWarning(Uri uri) {
+ // The ContentResolver already logs the exception. Log something more informative.
+ Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
+ + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
+ + "and set your Intent's clipData and flags in accordance with that method's "
+ + "documentation");
+ }
+
+ private static void loadFileUriIntoView(
+ final Uri uri,
+ final View parent,
+ final ContentPreviewCoordinator previewCoord,
+ final ContentResolver contentResolver) {
+ FileInfo fileInfo = extractFileInfo(uri, contentResolver);
+
+ TextView fileNameView = parent.findViewById(
+ com.android.internal.R.id.content_preview_filename);
+ fileNameView.setText(fileInfo.name);
+
+ if (fileInfo.hasThumbnail) {
+ previewCoord.loadImage(
+ uri,
+ (bitmap) -> updateViewWithImage(
+ parent.findViewById(
+ com.android.internal.R.id.content_preview_file_thumbnail),
+ bitmap));
+ } else {
+ View thumbnailView = parent.findViewById(
+ com.android.internal.R.id.content_preview_file_thumbnail);
+ thumbnailView.setVisibility(View.GONE);
+
+ ImageView fileIconView = parent.findViewById(
+ com.android.internal.R.id.content_preview_file_icon);
+ fileIconView.setVisibility(View.VISIBLE);
+ fileIconView.setImageResource(R.drawable.chooser_file_generic);
+ }
+ }
+
+ private static void updateViewWithImage(RoundedRectImageView imageView, Bitmap image) {
+ if (image == null) {
+ imageView.setVisibility(View.GONE);
+ return;
+ }
+ imageView.setVisibility(View.VISIBLE);
+ imageView.setAlpha(0.0f);
+ imageView.setImageBitmap(image);
+
+ ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 1.0f);
+ fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+ fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
+ fadeAnim.start();
+ }
+
+ private static FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
+ String fileName = null;
+ boolean hasThumbnail = false;
+
+ try (Cursor cursor = queryResolver(resolver, uri)) {
+ if (cursor != null && cursor.getCount() > 0) {
+ int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
+ int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
+
+ cursor.moveToFirst();
+ if (nameIndex != -1) {
+ fileName = cursor.getString(nameIndex);
+ } else if (titleIndex != -1) {
+ fileName = cursor.getString(titleIndex);
+ }
+
+ if (flagsIndex != -1) {
+ hasThumbnail = (cursor.getInt(flagsIndex)
+ & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
+ }
+ }
+ } catch (SecurityException | NullPointerException e) {
+ logContentPreviewWarning(uri);
+ }
+
+ if (TextUtils.isEmpty(fileName)) {
+ fileName = uri.getPath();
+ int index = fileName.lastIndexOf('/');
+ if (index != -1) {
+ fileName = fileName.substring(index + 1);
+ }
+ }
+
+ return new FileInfo(fileName, hasThumbnail);
+ }
+
+ private static class FileInfo {
+ public final String name;
+ public final boolean hasThumbnail;
+
+ FileInfo(String name, boolean hasThumbnail) {
+ this.name = name;
+ this.hasThumbnail = hasThumbnail;
+ }
+ }
+
+ private ChooserContentPreviewUi() {}
+}
diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
index 7c4b0c1f..5f373525 100644
--- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
+++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
@@ -19,8 +19,8 @@ package com.android.intentresolver;
import android.content.Context;
import android.util.AttributeSet;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.RecyclerView;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
/**
* For a11y and per {@link RecyclerView#onInitializeAccessibilityNodeInfo}, override
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 6d0c8337..699190f9 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -19,17 +19,22 @@ package com.android.intentresolver;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import android.annotation.Nullable;
import android.app.ActivityManager;
-import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.LabeledIntent;
+import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.os.AsyncTask;
import android.os.Trace;
import android.os.UserHandle;
@@ -42,27 +47,27 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
-import com.android.intentresolver.chooser.ChooserTargetInfo;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
+import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
public class ChooserListAdapter extends ResolverListAdapter {
private static final String TAG = "ChooserListAdapter";
private static final boolean DEBUG = false;
- private boolean mEnableStackedApps = true;
-
public static final int NO_POSITION = -1;
public static final int TARGET_BAD = -1;
public static final int TARGET_CALLER = 0;
@@ -71,40 +76,28 @@ public class ChooserListAdapter extends ResolverListAdapter {
public static final int TARGET_STANDARD_AZ = 3;
private static final int MAX_SUGGESTED_APP_TARGETS = 4;
- private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
/** {@link #getBaseScore} */
public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
/** {@link #getBaseScore} */
public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
- private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
- private final int mMaxShortcutTargetsPerApp;
- private final ChooserListCommunicator mChooserListCommunicator;
- private final SelectableTargetInfo.SelectableTargetInfoCommunicator
- mSelectableTargetInfoCommunicator;
+ private final ChooserRequestParameters mChooserRequest;
+ private final int mMaxRankedTargets;
+
private final ChooserActivityLogger mChooserActivityLogger;
- private int mNumShortcutResults = 0;
private final Map<TargetInfo, AsyncTask> mIconLoaders = new HashMap<>();
- private boolean mApplySharingAppLimits;
// Reserve spots for incoming direct share targets by adding placeholders
- private ChooserTargetInfo
- mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
- private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
+ private final TargetInfo mPlaceHolderTargetInfo;
+ private final List<TargetInfo> mServiceTargets = new ArrayList<>();
private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>();
- private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
- new ChooserActivity.BaseChooserTargetComparator();
- private boolean mListViewDataChanged = false;
+ private final ShortcutSelectionLogic mShortcutSelectionLogic;
// Sorted list of DisplayResolveInfos for the alphabetical app section.
private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
- private AppPredictor mAppPredictor;
- private AppPredictor.Callback mAppPredictorCallback;
-
- private LoadDirectShareIconTaskProvider mTestLoadDirectShareTaskProvider;
// 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
@@ -137,24 +130,47 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
};
- public ChooserListAdapter(Context context, List<Intent> payloadIntents,
- Intent[] initialIntents, List<ResolveInfo> rList,
- boolean filterLastUsed, ResolverListController resolverListController,
- ChooserListCommunicator chooserListCommunicator,
- SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator,
+ public ChooserListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ ResolverListCommunicator resolverListCommunicator,
PackageManager packageManager,
- ChooserActivityLogger chooserActivityLogger) {
+ ChooserActivityLogger chooserActivityLogger,
+ ChooserRequestParameters chooserRequest,
+ int maxRankedTargets) {
// Don't send the initial intents through the shared ResolverActivity path,
// we want to separate them into a different section.
- super(context, payloadIntents, null, rList, filterLastUsed,
- resolverListController, chooserListCommunicator, false);
-
- mMaxShortcutTargetsPerApp =
- context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
- mChooserListCommunicator = chooserListCommunicator;
+ super(
+ context,
+ payloadIntents,
+ null,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ false);
+
+ mChooserRequest = chooserRequest;
+ mMaxRankedTargets = maxRankedTargets;
+
+ mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);
createPlaceHolders();
- mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator;
mChooserActivityLogger = chooserActivityLogger;
+ mShortcutSelectionLogic = new ShortcutSelectionLogic(
+ context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp),
+ DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ true)
+ );
if (initialIntents != null) {
for (int i = 0; i < initialIntents.length; i++) {
@@ -172,7 +188,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
final ComponentName cn = ii.getComponent();
if (cn != null) {
try {
- ai = packageManager.getActivityInfo(ii.getComponent(), 0);
+ ai = packageManager.getActivityInfo(
+ ii.getComponent(),
+ PackageManager.ComponentInfoFlags.of(PackageManager.GET_META_DATA));
ri = new ResolveInfo();
ri.activityInfo = ai;
} catch (PackageManager.NameNotFoundException ignored) {
@@ -182,7 +200,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
if (ai == null) {
// Because of AIDL bug, resolveActivity can't accept subclasses of Intent.
final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii);
- ri = packageManager.resolveActivity(rii, PackageManager.MATCH_DEFAULT_ONLY);
+ ri = packageManager.resolveActivity(
+ rii,
+ PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
ai = ri != null ? ri.activityInfo : null;
}
if (ai == null) {
@@ -203,18 +223,12 @@ public class ChooserListAdapter extends ResolverListAdapter {
ri.noResourceId = true;
ri.icon = 0;
}
- mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
+ DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ ii, ri, ii, mPresentationFactory.makePresentationGetter(ri));
+ mCallerTargets.add(displayResolveInfo);
if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
}
}
- mApplySharingAppLimits = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
- true);
- }
-
- AppPredictor getAppPredictor() {
- return mAppPredictor;
}
@Override
@@ -223,73 +237,54 @@ public class ChooserListAdapter extends ResolverListAdapter {
Log.d(TAG, "clearing queryTargets on package change");
}
createPlaceHolders();
- mChooserListCommunicator.onHandlePackagesChanged(this);
+ mResolverListCommunicator.onHandlePackagesChanged(this);
}
- @Override
- public void notifyDataSetChanged() {
- if (!mListViewDataChanged) {
- mChooserListCommunicator.sendListViewUpdateMessage(getUserHandle());
- mListViewDataChanged = true;
- }
- }
-
- void refreshListView() {
- if (mListViewDataChanged) {
- super.notifyDataSetChanged();
- }
- mListViewDataChanged = false;
- }
-
private void createPlaceHolders() {
- mNumShortcutResults = 0;
mServiceTargets.clear();
- for (int i = 0; i < mChooserListCommunicator.getMaxRankedTargets(); i++) {
+ for (int i = 0; i < mMaxRankedTargets; ++i) {
mServiceTargets.add(mPlaceHolderTargetInfo);
}
}
@Override
View onCreateView(ViewGroup parent) {
- return mInflater.inflate(
- R.layout.resolve_grid_item, parent, false);
+ return mInflater.inflate(R.layout.resolve_grid_item, parent, false);
}
+ @VisibleForTesting
@Override
- protected void onBindView(View view, TargetInfo info, int position) {
+ public void onBindView(View view, TargetInfo info, int position) {
final ViewHolder holder = (ViewHolder) view.getTag();
if (info == null) {
- holder.icon.setImageDrawable(
- mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ holder.icon.setImageDrawable(loadIconPlaceholder());
return;
}
- if (info instanceof DisplayResolveInfo) {
+ holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+ holder.bindIcon(info);
+ if (info.isSelectableTargetInfo()) {
+ // direct share targets should append the application name for a better readout
+ DisplayResolveInfo rInfo = 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);
+ if (!info.hasDisplayIcon()) {
+ loadDirectShareIcon((SelectableTargetInfo) info);
+ }
+ } else if (info.isDisplayResolveInfo()) {
DisplayResolveInfo dri = (DisplayResolveInfo) info;
- holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
- startDisplayResolveInfoIconLoading(holder, dri);
- } else {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
-
- 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 {
- holder.bindIcon(info);
+ if (!dri.hasDisplayIcon()) {
+ loadIcon(dri);
}
}
// If target is loading, show a special placeholder shape in the label, make unclickable
- if (info instanceof ChooserActivity.PlaceHolderTargetInfo) {
+ if (info.isPlaceHolderTargetInfo()) {
final int maxWidth = mContext.getResources().getDimensionPixelSize(
R.dimen.chooser_direct_share_label_placeholder_max_width);
holder.text.setMaxWidth(maxWidth);
@@ -306,7 +301,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
// Always remove the spacing listener, attach as needed to direct share targets below.
holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
- if (info instanceof MultiDisplayResolveInfo) {
+ if (info.isMultiDisplayResolveInfo()) {
// If the target is grouped show an indicator
Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background);
holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0);
@@ -325,64 +320,47 @@ 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) {
+ private void loadDirectShareIcon(SelectableTargetInfo info) {
LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
if (task == null) {
- task = mTestLoadDirectShareTaskProvider == null
- ? new LoadDirectShareIconTask(info)
- : mTestLoadDirectShareTaskProvider.get();
+ task = createLoadDirectShareIconTask(info);
mIconLoaders.put(info, task);
task.loadIcon();
}
- task.setViewHolder(holder);
+ }
+
+ @VisibleForTesting
+ protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) {
+ return new LoadDirectShareIconTask(
+ mContext.createContextAsUser(getUserHandle(), 0),
+ info);
}
void updateAlphabeticalList() {
+ // TODO: this procedure seems like it should be relatively lightweight. Why does it need to
+ // run in an `AsyncTask`?
new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
@Override
protected List<DisplayResolveInfo> doInBackground(Void... voids) {
List<DisplayResolveInfo> allTargets = new ArrayList<>();
- allTargets.addAll(mDisplayList);
+ allTargets.addAll(getTargetsInCurrentDisplayList());
allTargets.addAll(mCallerTargets);
- if (!mEnableStackedApps) {
- return allTargets;
- }
+
// Consolidate multiple targets from same app.
- Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
- for (DisplayResolveInfo info : allTargets) {
- String resolvedTarget = info.getResolvedComponentName().getPackageName()
- + '#' + info.getDisplayLabel();
- DisplayResolveInfo multiDri = consolidated.get(resolvedTarget);
- if (multiDri == null) {
- consolidated.put(resolvedTarget, info);
- } else if (multiDri instanceof MultiDisplayResolveInfo) {
- ((MultiDisplayResolveInfo) multiDri).addTarget(info);
- } else {
- // create consolidated target from the single DisplayResolveInfo
- MultiDisplayResolveInfo multiDisplayResolveInfo =
- new MultiDisplayResolveInfo(resolvedTarget, multiDri);
- multiDisplayResolveInfo.addTarget(info);
- consolidated.put(resolvedTarget, multiDisplayResolveInfo);
- }
- }
- List<DisplayResolveInfo> groupedTargets = new ArrayList<>();
- groupedTargets.addAll(consolidated.values());
- Collections.sort(groupedTargets, new ChooserActivity.AzInfoComparator(mContext));
- return groupedTargets;
+ return allTargets
+ .stream()
+ .collect(Collectors.groupingBy(target ->
+ target.getResolvedComponentName().getPackageName()
+ + "#" + target.getDisplayLabel()
+ ))
+ .values()
+ .stream()
+ .map(appTargets ->
+ (appTargets.size() == 1)
+ ? appTargets.get(0)
+ : MultiDisplayResolveInfo.newMultiDisplayResolveInfo(appTargets))
+ .sorted(new ChooserActivity.AzInfoComparator(mContext))
+ .collect(Collectors.toList());
}
@Override
protected void onPostExecute(List<DisplayResolveInfo> newList) {
@@ -401,8 +379,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
@Override
public int getUnfilteredCount() {
int appTargets = super.getUnfilteredCount();
- if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) {
- appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets();
+ if (appTargets > mMaxRankedTargets) {
+ // TODO: what does this condition mean?
+ appTargets = appTargets + mMaxRankedTargets;
}
return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
}
@@ -417,8 +396,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
*/
public int getSelectableServiceTargetCount() {
int count = 0;
- for (ChooserTargetInfo info : mServiceTargets) {
- if (info instanceof SelectableTargetInfo) {
+ for (TargetInfo info : mServiceTargets) {
+ if (info.isSelectableTargetInfo()) {
count++;
}
}
@@ -426,29 +405,37 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
public int getServiceTargetCount() {
- if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent())
- && !ActivityManager.isLowRamDeviceStatic()) {
- return Math.min(mServiceTargets.size(), mChooserListCommunicator.getMaxRankedTargets());
+ if (mChooserRequest.isSendActionTarget() && !ActivityManager.isLowRamDeviceStatic()) {
+ return Math.min(mServiceTargets.size(), mMaxRankedTargets);
}
return 0;
}
- int getAlphaTargetCount() {
+ public int getAlphaTargetCount() {
int groupedCount = mSortedList.size();
- int ungroupedCount = mCallerTargets.size() + mDisplayList.size();
- return ungroupedCount > mChooserListCommunicator.getMaxRankedTargets() ? groupedCount : 0;
+ int ungroupedCount = mCallerTargets.size() + getDisplayResolveInfoCount();
+ return (ungroupedCount > mMaxRankedTargets) ? groupedCount : 0;
}
/**
* Fetch ranked app target count
*/
public int getRankedTargetCount() {
- int spacesAvailable =
- mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount();
+ int spacesAvailable = mMaxRankedTargets - getCallerTargetCount();
return Math.min(spacesAvailable, super.getCount());
}
+ /** Get all the {@link DisplayResolveInfo} data for our targets. */
+ public DisplayResolveInfo[] getDisplayResolveInfos() {
+ int size = getDisplayResolveInfoCount();
+ DisplayResolveInfo[] resolvedTargets = new DisplayResolveInfo[size];
+ for (int i = 0; i < size; i++) {
+ resolvedTargets[i] = getDisplayResolveInfo(i);
+ }
+ return resolvedTargets;
+ }
+
public int getPositionTargetType(int position) {
int offset = 0;
@@ -483,7 +470,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
return targetInfoForPosition(position, true);
}
-
/**
* Find target info for a given position.
* Since ChooserActivity displays several sections of content, determine which
@@ -533,8 +519,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
// Checks if this info is already listed in callerTargets.
for (TargetInfo existingInfo : mCallerTargets) {
- if (mResolverListCommunicator
- .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
+ if (mResolverListCommunicator.resolveInfoMatch(
+ dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return false;
}
}
@@ -544,10 +530,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
/**
* Fetch surfaced direct share target info
*/
- public List<ChooserTargetInfo> getSurfacedTargetInfo() {
- int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets();
+ public List<TargetInfo> getSurfacedTargetInfo() {
return mServiceTargets.subList(0,
- Math.min(maxSurfacedTargets, getSelectableServiceTargetCount()));
+ Math.min(mMaxRankedTargets, getSelectableServiceTargetCount()));
}
@@ -555,83 +540,36 @@ public class ChooserListAdapter extends ResolverListAdapter {
* Evaluate targets for inclusion in the direct share area. May not be included
* if score is too low.
*/
- public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
+ public void addServiceResults(
+ @Nullable DisplayResolveInfo origTarget,
+ List<ChooserTarget> targets,
@ChooserActivity.ShareTargetType int targetType,
- Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos) {
- if (DEBUG) {
- Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", "
- + targets.size()
- + " targets");
- }
- if (targets.size() == 0) {
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
+ Map<ChooserTarget, AppTarget> directShareToAppTargets) {
+ // Avoid inserting any potentially late results.
+ if ((mServiceTargets.size() == 1) && mServiceTargets.get(0).isEmptyTargetInfo()) {
return;
}
- final float baseScore = getBaseScore(origTarget, targetType);
- Collections.sort(targets, mBaseTargetComparator);
- final boolean isShortcutResult =
- (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
- || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
- final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
- : MAX_CHOOSER_TARGETS_PER_APP;
- final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets)
- : targets.size();
- float lastScore = 0;
- boolean shouldNotify = false;
- for (int i = 0, count = targetsLimit; i < count; i++) {
- final ChooserTarget target = targets.get(i);
- float targetScore = target.getScore();
- if (mApplySharingAppLimits) {
- targetScore *= baseScore;
- if (i > 0 && targetScore >= lastScore) {
- // Apply a decay so that the top app can't crowd out everything else.
- // This incents ChooserTargetServices to define what's truly better.
- targetScore = lastScore * 0.95f;
- }
- }
- ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target)
- : null;
- if ((shortcutInfo != null) && shortcutInfo.isPinned()) {
- targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST;
- }
- UserHandle userHandle = getUserHandle();
- Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */);
- boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser,
- origTarget, target, targetScore, mSelectableTargetInfoCommunicator,
- shortcutInfo));
-
- if (isInserted && isShortcutResult) {
- mNumShortcutResults++;
- }
-
- shouldNotify |= isInserted;
-
- if (DEBUG) {
- Log.d(TAG, " => " + target.toString() + " score=" + targetScore
- + " base=" + target.getScore()
- + " lastScore=" + lastScore
- + " baseScore=" + baseScore
- + " applyAppLimit=" + mApplySharingAppLimits);
- }
-
- lastScore = targetScore;
- }
-
- if (shouldNotify) {
+ boolean isShortcutResult = targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
+ || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
+ boolean isUpdated = mShortcutSelectionLogic.addServiceResults(
+ origTarget,
+ getBaseScore(origTarget, targetType),
+ targets,
+ isShortcutResult,
+ directShareToShortcutInfos,
+ directShareToAppTargets,
+ mContext.createContextAsUser(getUserHandle(), 0),
+ mChooserRequest.getTargetIntent(),
+ mChooserRequest.getReferrerFillInIntent(),
+ mMaxRankedTargets,
+ mServiceTargets);
+ if (isUpdated) {
notifyDataSetChanged();
}
}
/**
- * The return number have to exceed a minimum limit to make direct share area expandable. When
- * append direct share targets is enabled, return count of all available targets parking in the
- * memory; otherwise, it is shortcuts count which will help reduce the amount of visible
- * shuffling due to older-style direct share targets.
- */
- int getNumServiceTargetsForExpand() {
- return mNumShortcutResults;
- }
-
- /**
* Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
* <ol>
* <li>App-supplied targets
@@ -659,54 +597,14 @@ public class ChooserListAdapter extends ResolverListAdapter {
* update the direct share area.
*/
public void completeServiceTargetLoading() {
- mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
+ mServiceTargets.removeIf(o -> o.isPlaceHolderTargetInfo());
if (mServiceTargets.isEmpty()) {
- mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
+ mServiceTargets.add(NotSelectableTargetInfo.newEmptyTargetInfo());
mChooserActivityLogger.logSharesheetEmptyDirectShareRow();
}
notifyDataSetChanged();
}
- private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
- // Avoid inserting any potentially late results
- if (mServiceTargets.size() == 1
- && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
- return false;
- }
-
- // Check for duplicates and abort if found
- for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
- if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
- return false;
- }
- }
-
- int currentSize = mServiceTargets.size();
- final float newScore = chooserTargetInfo.getModifiedScore();
- for (int i = 0; i < Math.min(currentSize, mChooserListCommunicator.getMaxRankedTargets());
- i++) {
- final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
- if (serviceTarget == null) {
- mServiceTargets.set(i, chooserTargetInfo);
- return true;
- } else if (newScore > serviceTarget.getModifiedScore()) {
- mServiceTargets.add(i, chooserTargetInfo);
- return true;
- }
- }
-
- if (currentSize < mChooserListCommunicator.getMaxRankedTargets()) {
- mServiceTargets.add(chooserTargetInfo);
- return true;
- }
-
- return false;
- }
-
- public ChooserTarget getChooserTargetForValue(int value) {
- return mServiceTargets.get(value).getChooserTarget();
- }
-
protected boolean alwaysShowSubLabel() {
// Always show a subLabel for visual consistency across list items. Show an empty
// subLabel if the subLabel is the same as the label
@@ -728,8 +626,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
protected List<ResolvedComponentInfo> doInBackground(
List<ResolvedComponentInfo>... params) {
Trace.beginSection("ChooserListAdapter#SortingTask");
- mResolverListController.topK(params[0],
- mChooserListCommunicator.getMaxRankedTargets());
+ mResolverListController.topK(params[0], mMaxRankedTargets);
Trace.endSection();
return params[0];
}
@@ -737,88 +634,95 @@ public class ChooserListAdapter extends ResolverListAdapter {
protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
processSortedList(sortedComponents, doPostProcessing);
if (doPostProcessing) {
- mChooserListCommunicator.updateProfileViewButton();
+ mResolverListCommunicator.updateProfileViewButton();
notifyDataSetChanged();
}
}
};
}
- public void setAppPredictor(AppPredictor appPredictor) {
- mAppPredictor = appPredictor;
- }
-
- public void setAppPredictorCallback(AppPredictor.Callback appPredictorCallback) {
- mAppPredictorCallback = appPredictorCallback;
- }
-
- public void destroyAppPredictor() {
- if (getAppPredictor() != null) {
- getAppPredictor().unregisterPredictionUpdates(mAppPredictorCallback);
- getAppPredictor().destroy();
- setAppPredictor(null);
- }
- }
-
- /**
- * 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}
- * and {@link ChooserActivity}.
- */
- @VisibleForTesting
- public interface ChooserListCommunicator extends ResolverListCommunicator {
-
- int getMaxRankedTargets();
-
- void sendListViewUpdateMessage(UserHandle userHandle);
-
- boolean isSendAction(Intent targetIntent);
- }
-
/**
* Loads direct share targets icons.
*/
@VisibleForTesting
- public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Void> {
+ public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Drawable> {
+ private final Context mContext;
private final SelectableTargetInfo mTargetInfo;
- private ViewHolder mViewHolder;
- private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) {
+ private LoadDirectShareIconTask(Context context, SelectableTargetInfo targetInfo) {
+ mContext = context;
mTargetInfo = targetInfo;
}
@Override
- protected Void doInBackground(Void... voids) {
- mTargetInfo.loadIcon();
- return null;
+ protected Drawable doInBackground(Void... voids) {
+ Drawable drawable;
+ try {
+ drawable = getChooserTargetIconDrawable(
+ mContext,
+ mTargetInfo.getChooserTargetIcon(),
+ mTargetInfo.getChooserTargetComponentName(),
+ mTargetInfo.getDirectShareShortcutInfo());
+ } catch (Exception e) {
+ Log.e(TAG,
+ "Failed to load shortcut icon for "
+ + mTargetInfo.getChooserTargetComponentName(),
+ e);
+ drawable = loadIconPlaceholder();
+ }
+ return drawable;
}
@Override
- protected void onPostExecute(Void arg) {
- if (mViewHolder != null) {
- mViewHolder.bindIcon(mTargetInfo);
+ protected void onPostExecute(@Nullable Drawable icon) {
+ if (icon != null && !mTargetInfo.hasDisplayIcon()) {
+ mTargetInfo.getDisplayIconHolder().setDisplayIcon(icon);
notifyDataSetChanged();
}
}
- /**
- * Specifies a view holder that will be updated when the task is completed.
- */
- public void setViewHolder(ViewHolder viewHolder) {
- mViewHolder = viewHolder;
- mViewHolder.bindIcon(mTargetInfo);
+ @WorkerThread
+ private Drawable getChooserTargetIconDrawable(
+ Context context,
+ @Nullable Icon icon,
+ ComponentName targetComponentName,
+ @Nullable ShortcutInfo shortcutInfo) {
+ Drawable directShareIcon = null;
+
+ // First get the target drawable and associated activity info
+ if (icon != null) {
+ directShareIcon = icon.loadDrawable(context);
+ } else if (shortcutInfo != null) {
+ LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+ if (launcherApps != null) {
+ directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0);
+ }
+ }
+
+ if (directShareIcon == null) {
+ return null;
+ }
+
+ ActivityInfo info = null;
+ try {
+ info = context.getPackageManager().getActivityInfo(targetComponentName, 0);
+ } catch (PackageManager.NameNotFoundException error) {
+ Log.e(TAG, "Could not find activity associated with ChooserTarget");
+ }
+
+ if (info == null) {
+ return null;
+ }
+
+ // Now fetch app icon and raster with no badging even in work profile
+ Bitmap appIcon = mPresentationFactory.makePresentationGetter(info).getIconBitmap(null);
+
+ // Raster target drawable with appIcon as a badge
+ SimpleIconFactory sif = SimpleIconFactory.obtain(context);
+ Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
+ sif.recycle();
+
+ return new BitmapDrawable(context.getResources(), directShareBadgedIcon);
}
/**
@@ -828,16 +732,4 @@ public class ChooserListAdapter extends ResolverListAdapter {
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();
- }
}
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
index da78fc81..39d1fab0 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -16,306 +16,159 @@
package com.android.intentresolver;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
-
-import android.annotation.Nullable;
-import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.UserHandle;
import android.view.LayoutInflater;
-import android.view.View;
import android.view.ViewGroup;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.PagerAdapter;
-import com.android.internal.widget.RecyclerView;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Optional;
+import java.util.function.Supplier;
/**
* A {@link PagerAdapter} which describes the work and personal profile share sheet screens.
*/
@VisibleForTesting
-public class ChooserMultiProfilePagerAdapter extends AbstractMultiProfilePagerAdapter {
+public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAdapter<
+ RecyclerView, ChooserGridAdapter, ChooserListAdapter> {
private static final int SINGLE_CELL_SPAN_SIZE = 1;
- private final ChooserProfileDescriptor[] mItems;
- private final boolean mIsSendAction;
- private int mBottomOffset;
- private int mMaxTargetsPerRow;
+ private final ChooserProfileAdapterBinder mAdapterBinder;
+ private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
- ChooserMultiProfilePagerAdapter(Context context,
- ChooserActivity.ChooserGridAdapter adapter,
- UserHandle personalProfileUserHandle,
+ ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserGridAdapter adapter,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
UserHandle workProfileUserHandle,
- boolean isSendAction, int maxTargetsPerRow) {
- super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle);
- mItems = new ChooserProfileDescriptor[] {
- createProfileDescriptor(adapter)
- };
- mIsSendAction = isSendAction;
- mMaxTargetsPerRow = maxTargetsPerRow;
- }
-
- ChooserMultiProfilePagerAdapter(Context context,
- ChooserActivity.ChooserGridAdapter personalAdapter,
- ChooserActivity.ChooserGridAdapter workAdapter,
+ int maxTargetsPerRow) {
+ this(
+ context,
+ new ChooserProfileAdapterBinder(maxTargetsPerRow),
+ ImmutableList.of(adapter),
+ emptyStateProvider,
+ quietModeManager,
+ /* defaultProfile= */ 0,
+ workProfileUserHandle,
+ new BottomPaddingOverrideSupplier(context));
+ }
+
+ ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserGridAdapter personalAdapter,
+ ChooserGridAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
@Profile int defaultProfile,
- UserHandle personalProfileUserHandle,
UserHandle workProfileUserHandle,
- boolean isSendAction, int maxTargetsPerRow) {
- super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
- workProfileUserHandle);
- mItems = new ChooserProfileDescriptor[] {
- createProfileDescriptor(personalAdapter),
- createProfileDescriptor(workAdapter)
- };
- mIsSendAction = isSendAction;
- mMaxTargetsPerRow = maxTargetsPerRow;
- }
-
- private ChooserProfileDescriptor createProfileDescriptor(
- ChooserActivity.ChooserGridAdapter adapter) {
- final LayoutInflater inflater = LayoutInflater.from(getContext());
- final ViewGroup rootView =
- (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);
- ChooserProfileDescriptor profileDescriptor =
- new ChooserProfileDescriptor(rootView, adapter);
- profileDescriptor.recyclerView.setAccessibilityDelegateCompat(
- new ChooserRecyclerViewAccessibilityDelegate(profileDescriptor.recyclerView));
- return profileDescriptor;
+ int maxTargetsPerRow) {
+ this(
+ context,
+ new ChooserProfileAdapterBinder(maxTargetsPerRow),
+ ImmutableList.of(personalAdapter, workAdapter),
+ emptyStateProvider,
+ quietModeManager,
+ defaultProfile,
+ workProfileUserHandle,
+ new BottomPaddingOverrideSupplier(context));
+ }
+
+ private ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserProfileAdapterBinder adapterBinder,
+ ImmutableList<ChooserGridAdapter> gridAdapters,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+ super(
+ context,
+ gridAdapter -> gridAdapter.getListAdapter(),
+ adapterBinder,
+ gridAdapters,
+ emptyStateProvider,
+ quietModeManager,
+ defaultProfile,
+ workProfileUserHandle,
+ () -> makeProfileView(context),
+ bottomPaddingOverrideSupplier);
+ mAdapterBinder = adapterBinder;
+ mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
}
public void setMaxTargetsPerRow(int maxTargetsPerRow) {
- mMaxTargetsPerRow = maxTargetsPerRow;
+ mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow);
}
- RecyclerView getListViewForIndex(int index) {
- return getItem(index).recyclerView;
+ public void setEmptyStateBottomOffset(int bottomOffset) {
+ mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset);
}
- @Override
- ChooserProfileDescriptor getItem(int pageIndex) {
- return mItems[pageIndex];
+ private static ViewGroup makeProfileView(Context context) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ ViewGroup rootView = (ViewGroup) inflater.inflate(
+ R.layout.chooser_list_per_profile, null, false);
+ RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+ recyclerView.setAccessibilityDelegateCompat(
+ new ChooserRecyclerViewAccessibilityDelegate(recyclerView));
+ return rootView;
}
- @Override
- int getItemCount() {
- return mItems.length;
- }
+ private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
+ private final Context mContext;
+ private int mBottomOffset;
- @Override
- @VisibleForTesting
- public ChooserActivity.ChooserGridAdapter getAdapterForIndex(int pageIndex) {
- return mItems[pageIndex].chooserGridAdapter;
- }
-
- @Override
- @Nullable
- ChooserListAdapter getListAdapterForUserHandle(UserHandle userHandle) {
- if (getActiveListAdapter().getUserHandle().equals(userHandle)) {
- return getActiveListAdapter();
- } else if (getInactiveListAdapter() != null
- && getInactiveListAdapter().getUserHandle().equals(userHandle)) {
- return getInactiveListAdapter();
+ BottomPaddingOverrideSupplier(Context context) {
+ mContext = context;
}
- return null;
- }
- @Override
- void setupListAdapter(int pageIndex) {
- final RecyclerView recyclerView = getItem(pageIndex).recyclerView;
- ChooserActivity.ChooserGridAdapter chooserGridAdapter =
- getItem(pageIndex).chooserGridAdapter;
- GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager();
- glm.setSpanCount(mMaxTargetsPerRow);
- glm.setSpanSizeLookup(
- new GridLayoutManager.SpanSizeLookup() {
- @Override
- public int getSpanSize(int position) {
- return chooserGridAdapter.shouldCellSpan(position)
- ? SINGLE_CELL_SPAN_SIZE
- : glm.getSpanCount();
- }
- });
- }
-
- @Override
- @VisibleForTesting
- public ChooserListAdapter getActiveListAdapter() {
- return getAdapterForIndex(getCurrentPage()).getListAdapter();
- }
-
- @Override
- @VisibleForTesting
- public ChooserListAdapter getInactiveListAdapter() {
- if (getCount() == 1) {
- return null;
+ public void setEmptyStateBottomOffset(int bottomOffset) {
+ mBottomOffset = bottomOffset;
}
- return getAdapterForIndex(1 - getCurrentPage()).getListAdapter();
- }
-
- @Override
- public ResolverListAdapter getPersonalListAdapter() {
- return getAdapterForIndex(PROFILE_PERSONAL).getListAdapter();
- }
-
- @Override
- @Nullable
- public ResolverListAdapter getWorkListAdapter() {
- return getAdapterForIndex(PROFILE_WORK).getListAdapter();
- }
-
- @Override
- ChooserActivity.ChooserGridAdapter getCurrentRootAdapter() {
- return getAdapterForIndex(getCurrentPage());
- }
- @Override
- RecyclerView getActiveAdapterView() {
- return getListViewForIndex(getCurrentPage());
- }
-
- @Override
- @Nullable
- RecyclerView getInactiveAdapterView() {
- if (getCount() == 1) {
- return null;
+ public Optional<Integer> get() {
+ int initialBottomPadding = mContext.getResources().getDimensionPixelSize(
+ R.dimen.resolver_empty_state_container_padding_bottom);
+ return Optional.of(initialBottomPadding + mBottomOffset);
}
- return getListViewForIndex(1 - getCurrentPage());
- }
-
- @Override
- String getMetricsCategory() {
- return ResolverActivity.METRICS_CATEGORY_CHOOSER;
}
- @Override
- protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter,
- View.OnClickListener listener) {
- showEmptyState(activeListAdapter,
- getWorkAppPausedTitle(),
- /* subtitle = */ null,
- listener);
- }
+ private static class ChooserProfileAdapterBinder implements
+ AdapterBinder<RecyclerView, ChooserGridAdapter> {
+ private int mMaxTargetsPerRow;
- @Override
- protected void showNoPersonalToWorkIntentsEmptyState(ResolverListAdapter activeListAdapter) {
- if (mIsSendAction) {
- showEmptyState(activeListAdapter,
- getCrossProfileBlockedTitle(),
- getCantShareWithWorkMessage());
- } else {
- showEmptyState(activeListAdapter,
- getCrossProfileBlockedTitle(),
- getCantAccessWorkMessage());
+ ChooserProfileAdapterBinder(int maxTargetsPerRow) {
+ mMaxTargetsPerRow = maxTargetsPerRow;
}
- }
- @Override
- protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) {
- if (mIsSendAction) {
- showEmptyState(activeListAdapter,
- getCrossProfileBlockedTitle(),
- getCantShareWithPersonalMessage());
- } else {
- showEmptyState(activeListAdapter,
- getCrossProfileBlockedTitle(),
- getCantAccessPersonalMessage());
+ public void setMaxTargetsPerRow(int maxTargetsPerRow) {
+ mMaxTargetsPerRow = maxTargetsPerRow;
}
- }
-
- @Override
- protected void showNoPersonalAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
- showEmptyState(listAdapter, getNoPersonalAppsAvailableMessage(), /* subtitle= */ null);
-
- }
-
- @Override
- protected void showNoWorkAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
- showEmptyState(listAdapter, getNoWorkAppsAvailableMessage(), /* subtitle = */ null);
- }
-
- private String getWorkAppPausedTitle() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_WORK_PAUSED_TITLE,
- () -> getContext().getString(R.string.resolver_turn_on_work_apps));
- }
-
- private String getCrossProfileBlockedTitle() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
- () -> getContext().getString(R.string.resolver_cross_profile_blocked));
- }
-
- private String getCantShareWithWorkMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_CANT_SHARE_WITH_WORK,
- () -> getContext().getString(
- R.string.resolver_cant_share_with_work_apps_explanation));
- }
-
- private String getCantShareWithPersonalMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_CANT_SHARE_WITH_PERSONAL,
- () -> getContext().getString(
- R.string.resolver_cant_share_with_personal_apps_explanation));
- }
-
- private String getCantAccessWorkMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_CANT_ACCESS_WORK,
- () -> getContext().getString(
- R.string.resolver_cant_access_work_apps_explanation));
- }
-
- private String getCantAccessPersonalMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_CANT_ACCESS_PERSONAL,
- () -> getContext().getString(
- R.string.resolver_cant_access_personal_apps_explanation));
- }
-
- private String getNoWorkAppsAvailableMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_NO_WORK_APPS,
- () -> getContext().getString(
- R.string.resolver_no_work_apps_available));
- }
-
- private String getNoPersonalAppsAvailableMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_NO_PERSONAL_APPS,
- () -> getContext().getString(
- R.string.resolver_no_personal_apps_available));
- }
-
-
- void setEmptyStateBottomOffset(int bottomOffset) {
- mBottomOffset = bottomOffset;
- }
-
- @Override
- protected void setupContainerPadding(View container) {
- int initialBottomPadding = getContext().getResources().getDimensionPixelSize(
- R.dimen.resolver_empty_state_container_padding_bottom);
- container.setPadding(container.getPaddingLeft(), container.getPaddingTop(),
- container.getPaddingRight(), initialBottomPadding + mBottomOffset);
- }
- class ChooserProfileDescriptor extends ProfileDescriptor {
- private ChooserActivity.ChooserGridAdapter chooserGridAdapter;
- private RecyclerView recyclerView;
- ChooserProfileDescriptor(ViewGroup rootView, ChooserActivity.ChooserGridAdapter adapter) {
- super(rootView);
- chooserGridAdapter = adapter;
- recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+ @Override
+ public void bind(
+ RecyclerView recyclerView, ChooserGridAdapter chooserGridAdapter) {
+ GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager();
+ glm.setSpanCount(mMaxTargetsPerRow);
+ glm.setSpanSizeLookup(
+ new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ return chooserGridAdapter.shouldCellSpan(position)
+ ? SINGLE_CELL_SPAN_SIZE
+ : glm.getSpanCount();
+ }
+ });
}
}
}
diff --git a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
index 67571b44..250b6827 100644
--- a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
+++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
@@ -22,8 +22,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
-import com.android.internal.widget.RecyclerView;
-import com.android.internal.widget.RecyclerViewAccessibilityDelegate;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
private final Rect mTempRect = new Rect();
diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java
new file mode 100644
index 00000000..81481bf1
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2008 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.PatternMatcher;
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.google.common.collect.ImmutableList;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Utility to parse and validate parameters from the client-supplied {@link Intent} that launched
+ * the Sharesheet {@link ChooserActivity}. The validated parameters are stored as immutable ivars.
+ *
+ * TODO: field nullability in this class reflects legacy use, and typically would indicate that the
+ * client's intent didn't provide the respective data. In some cases we may be able to provide
+ * defaults instead of nulls -- especially for methods that return nullable lists or arrays, if the
+ * client code could instead handle empty collections equally well.
+ *
+ * TODO: some of these fields (especially getTargetIntent() and any other getters that delegate to
+ * it internally) differ from the legacy model because they're computed directly from the initial
+ * Chooser intent, where in the past they've been relayed up to ResolverActivity and then retrieved
+ * through methods on the base class. The base always seems to return them exactly as they were
+ * provided, so this should be safe -- and clients can reasonably switch to retrieving through these
+ * parameters instead. For now, the other convention is still used in some places. Ideally we'd like
+ * to normalize on a single source of truth, but we'll have to clean up the delegation up to the
+ * resolver (or perhaps this needs to be a subclass of some `ResolverRequestParameters` class?).
+ */
+public class ChooserRequestParameters {
+ private static final String TAG = "ChooserActivity";
+
+ private static final int LAUNCH_FLAGS_FOR_SEND_ACTION =
+ Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+
+ private final Intent mTarget;
+ private final Pair<CharSequence, Integer> mTitleSpec;
+ private final Intent mReferrerFillInIntent;
+ private final ImmutableList<ComponentName> mFilteredComponentNames;
+ private final ImmutableList<ChooserTarget> mCallerChooserTargets;
+ private final boolean mRetainInOnStop;
+
+ @Nullable
+ private final ImmutableList<Intent> mAdditionalTargets;
+
+ @Nullable
+ private final Bundle mReplacementExtras;
+
+ @Nullable
+ private final ImmutableList<Intent> mInitialIntents;
+
+ @Nullable
+ private final IntentSender mChosenComponentSender;
+
+ @Nullable
+ private final IntentSender mRefinementIntentSender;
+
+ @Nullable
+ private final String mSharedText;
+
+ @Nullable
+ private final IntentFilter mTargetIntentFilter;
+
+ public ChooserRequestParameters(
+ final Intent clientIntent,
+ final Uri referrer,
+ @Nullable final ComponentName nearbySharingComponent) {
+ final Intent requestedTarget = parseTargetIntentExtra(
+ clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));
+ mTarget = intentWithModifiedLaunchFlags(requestedTarget);
+
+ mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
+ clientIntent, Intent.EXTRA_ALTERNATE_INTENTS);
+
+ mReplacementExtras = clientIntent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS);
+
+ mTitleSpec = makeTitleSpec(
+ clientIntent.getCharSequenceExtra(Intent.EXTRA_TITLE),
+ isSendAction(mTarget.getAction()));
+
+ mInitialIntents = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
+ clientIntent, Intent.EXTRA_INITIAL_INTENTS);
+
+ mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, referrer);
+
+ mChosenComponentSender = clientIntent.getParcelableExtra(
+ Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
+ mRefinementIntentSender = clientIntent.getParcelableExtra(
+ Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
+
+ mFilteredComponentNames = getFilteredComponentNames(clientIntent, nearbySharingComponent);
+
+ mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent);
+
+ mRetainInOnStop = clientIntent.getBooleanExtra(
+ ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false);
+
+ mSharedText = mTarget.getStringExtra(Intent.EXTRA_TEXT);
+
+ mTargetIntentFilter = getTargetIntentFilter(mTarget);
+ }
+
+ public Intent getTargetIntent() {
+ return mTarget;
+ }
+
+ @Nullable
+ public String getTargetAction() {
+ return getTargetIntent().getAction();
+ }
+
+ public boolean isSendActionTarget() {
+ return isSendAction(getTargetAction());
+ }
+
+ @Nullable
+ public String getTargetType() {
+ return getTargetIntent().getType();
+ }
+
+ @Nullable
+ public CharSequence getTitle() {
+ return mTitleSpec.first;
+ }
+
+ public int getDefaultTitleResource() {
+ return mTitleSpec.second;
+ }
+
+ public Intent getReferrerFillInIntent() {
+ return mReferrerFillInIntent;
+ }
+
+ public ImmutableList<ComponentName> getFilteredComponentNames() {
+ return mFilteredComponentNames;
+ }
+
+ public ImmutableList<ChooserTarget> getCallerChooserTargets() {
+ return mCallerChooserTargets;
+ }
+
+ /**
+ * Whether the {@link ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP} behavior was requested.
+ */
+ public boolean shouldRetainInOnStop() {
+ return mRetainInOnStop;
+ }
+
+ /**
+ * TODO: this returns a nullable array for convenience, but if the legacy APIs can be
+ * refactored, returning {@link mAdditionalTargets} directly is simpler and safer.
+ */
+ @Nullable
+ public Intent[] getAdditionalTargets() {
+ return (mAdditionalTargets == null) ? null : mAdditionalTargets.toArray(new Intent[0]);
+ }
+
+ @Nullable
+ public Bundle getReplacementExtras() {
+ return mReplacementExtras;
+ }
+
+ /**
+ * TODO: this returns a nullable array for convenience, but if the legacy APIs can be
+ * refactored, returning {@link mInitialIntents} directly is simpler and safer.
+ */
+ @Nullable
+ public Intent[] getInitialIntents() {
+ return (mInitialIntents == null) ? null : mInitialIntents.toArray(new Intent[0]);
+ }
+
+ @Nullable
+ public IntentSender getChosenComponentSender() {
+ return mChosenComponentSender;
+ }
+
+ @Nullable
+ public IntentSender getRefinementIntentSender() {
+ return mRefinementIntentSender;
+ }
+
+ @Nullable
+ public String getSharedText() {
+ return mSharedText;
+ }
+
+ @Nullable
+ public IntentFilter getTargetIntentFilter() {
+ return mTargetIntentFilter;
+ }
+
+ private static boolean isSendAction(@Nullable String action) {
+ return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action));
+ }
+
+ private static Intent parseTargetIntentExtra(@Nullable Parcelable targetParcelable) {
+ if (targetParcelable instanceof Uri) {
+ try {
+ targetParcelable = Intent.parseUri(targetParcelable.toString(),
+ Intent.URI_INTENT_SCHEME);
+ } catch (URISyntaxException ex) {
+ throw new IllegalArgumentException("Failed to parse EXTRA_INTENT from URI", ex);
+ }
+ }
+
+ if (!(targetParcelable instanceof Intent)) {
+ throw new IllegalArgumentException(
+ "EXTRA_INTENT is neither an Intent nor a Uri: " + targetParcelable);
+ }
+
+ return ((Intent) targetParcelable);
+ }
+
+ private static Intent intentWithModifiedLaunchFlags(Intent intent) {
+ if (isSendAction(intent.getAction())) {
+ intent.addFlags(LAUNCH_FLAGS_FOR_SEND_ACTION);
+ }
+ return intent;
+ }
+
+ /**
+ * Build a pair of values specifying the title to use from the client request. The first
+ * ({@link CharSequence}) value is the client-specified title, if there was one and their
+ * requested target <em>wasn't</em> a send action; otherwise it is null. The second value is
+ * the resource ID of a default title string; this is nonzero only if the first value is null.
+ *
+ * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate()}, or
+ * create a real type (not {@link Pair}) to express the semantics described in this comment.
+ */
+ private static Pair<CharSequence, Integer> makeTitleSpec(
+ @Nullable CharSequence requestedTitle, boolean hasSendActionTarget) {
+ if (hasSendActionTarget && (requestedTitle != null)) {
+ // Do not allow the title to be changed when sharing content
+ Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a"
+ + " preview title by using EXTRA_TITLE property of the wrapped"
+ + " EXTRA_INTENT.");
+ requestedTitle = null;
+ }
+
+ int defaultTitleRes =
+ (requestedTitle == null) ? com.android.internal.R.string.chooseActivity : 0;
+
+ return Pair.create(requestedTitle, defaultTitleRes);
+ }
+
+ private static ImmutableList<ComponentName> getFilteredComponentNames(
+ Intent clientIntent, @Nullable ComponentName nearbySharingComponent) {
+ Stream<ComponentName> filteredComponents = streamParcelableArrayExtra(
+ clientIntent, Intent.EXTRA_EXCLUDE_COMPONENTS, ComponentName.class, true, true);
+
+ if (nearbySharingComponent != null) {
+ // Exclude Nearby from main list if chip is present, to avoid duplication.
+ // TODO: we don't have an explicit guarantee that the chip will be displayed just
+ // because we have a non-null component; that's ultimately determined by the preview
+ // layout. Maybe we can make that decision further upstream?
+ filteredComponents = Stream.concat(
+ filteredComponents, Stream.of(nearbySharingComponent));
+ }
+
+ return filteredComponents.collect(toImmutableList());
+ }
+
+ private static ImmutableList<ChooserTarget> parseCallerTargetsFromClientIntent(
+ Intent clientIntent) {
+ return
+ streamParcelableArrayExtra(
+ clientIntent, Intent.EXTRA_CHOOSER_TARGETS, ChooserTarget.class, true, true)
+ .collect(toImmutableList());
+ }
+
+ private static <T> Collector<T, ?, ImmutableList<T>> toImmutableList() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+ }
+
+ @Nullable
+ private static ImmutableList<Intent> intentsWithModifiedLaunchFlagsFromExtraIfPresent(
+ Intent clientIntent, String extra) {
+ Stream<Intent> intents =
+ streamParcelableArrayExtra(clientIntent, extra, Intent.class, true, false);
+ if (intents == null) {
+ return null;
+ }
+ return intents
+ .map(ChooserRequestParameters::intentWithModifiedLaunchFlags)
+ .collect(toImmutableList());
+ }
+
+ /**
+ * Make a {@link Stream} of the {@link Parcelable} objects given in the provided {@link Intent}
+ * as the optional parcelable array extra with key {@code extra}. The stream elements, if any,
+ * are all of the type specified by {@code clazz}.
+ *
+ * @param intent The intent that may contain the optional extras.
+ * @param extra The extras key to identify the parcelable array.
+ * @param clazz A class that is assignable from any elements in the result stream.
+ * @param warnOnTypeError Whether to log a warning (and ignore) if the client extra doesn't have
+ * the required type. If false, throw an {@link IllegalArgumentException} if the extra is
+ * non-null but can't be assigned to variables of type {@code T}.
+ * @param streamEmptyIfNull Whether to return an empty stream if the optional extra isn't
+ * present in the intent (or if it had the wrong type, but {@link warnOnTypeError} is true).
+ * If false, return null in these cases, and only return an empty stream if the intent
+ * explicitly provided an empty array for the specified extra.
+ */
+ @Nullable
+ private static <T extends Parcelable> Stream<T> streamParcelableArrayExtra(
+ final Intent intent,
+ String extra,
+ @NonNull Class<T> clazz,
+ boolean warnOnTypeError,
+ boolean streamEmptyIfNull) {
+ T[] result = null;
+
+ try {
+ result = getParcelableArrayExtraIfPresent(intent, extra, clazz);
+ } catch (IllegalArgumentException e) {
+ if (warnOnTypeError) {
+ Log.w(TAG, "Ignoring client-requested " + extra, e);
+ } else {
+ throw e;
+ }
+ }
+
+ if (result != null) {
+ return Arrays.stream(result);
+ } else if (streamEmptyIfNull) {
+ return Stream.empty();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * If the specified {@code extra} is provided in the {@code intent}, cast it to type {@code T[]}
+ * or throw an {@code IllegalArgumentException} if the cast fails. If the {@code extra} isn't
+ * present in the {@code intent}, return null.
+ */
+ @Nullable
+ private static <T extends Parcelable> T[] getParcelableArrayExtraIfPresent(
+ final Intent intent, String extra, @NonNull Class<T> clazz) throws
+ IllegalArgumentException {
+ if (!intent.hasExtra(extra)) {
+ return null;
+ }
+
+ T[] castResult = intent.getParcelableArrayExtra(extra, clazz);
+ if (castResult == null) {
+ Parcelable[] actualExtrasArray = intent.getParcelableArrayExtra(extra);
+ if (actualExtrasArray != null) {
+ throw new IllegalArgumentException(
+ String.format(
+ "%s is not of type %s[]: %s",
+ extra,
+ clazz.getSimpleName(),
+ Arrays.toString(actualExtrasArray)));
+ } else if (intent.getParcelableExtra(extra) != null) {
+ throw new IllegalArgumentException(
+ String.format(
+ "%s is not of type %s[] (or any array type): %s",
+ extra,
+ clazz.getSimpleName(),
+ intent.getParcelableExtra(extra)));
+ } else {
+ throw new IllegalArgumentException(
+ String.format(
+ "%s is not of type %s (or any Parcelable type): %s",
+ extra,
+ clazz.getSimpleName(),
+ intent.getExtras().get(extra)));
+ }
+ }
+
+ return castResult;
+ }
+
+ private static IntentFilter getTargetIntentFilter(final Intent intent) {
+ try {
+ String dataString = intent.getDataString();
+ if (intent.getType() == null) {
+ if (!TextUtils.isEmpty(dataString)) {
+ return new IntentFilter(intent.getAction(), dataString);
+ }
+ Log.e(TAG, "Failed to get target intent filter: intent data and type are null");
+ return null;
+ }
+ IntentFilter intentFilter = new IntentFilter(intent.getAction(), intent.getType());
+ List<Uri> contentUris = new ArrayList<>();
+ if (Intent.ACTION_SEND.equals(intent.getAction())) {
+ Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ contentUris.add(uri);
+ }
+ } else {
+ List<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (uris != null) {
+ contentUris.addAll(uris);
+ }
+ }
+ for (Uri uri : contentUris) {
+ intentFilter.addDataScheme(uri.getScheme());
+ intentFilter.addDataAuthority(uri.getAuthority(), null);
+ intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
+ }
+ return intentFilter;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get target intent filter", e);
+ return null;
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
index ae08ace2..2cfceeae 100644
--- a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
@@ -20,9 +20,10 @@ package com.android.intentresolver;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
-import android.os.Bundle;
import android.os.UserHandle;
+import androidx.fragment.app.FragmentManager;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
@@ -30,29 +31,39 @@ import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
* Shows individual actions for a "stacked" app target - such as an app with multiple posting
* streams represented in the Sharesheet.
*/
-public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogFragment
- implements DialogInterface.OnClickListener {
-
- static final String WHICH_KEY = "which_key";
- static final String MULTI_DRI_KEY = "multi_dri_key";
-
- private MultiDisplayResolveInfo mMultiDisplayResolveInfo;
- private int mParentWhich;
-
- public ChooserStackedAppDialogFragment() {}
+public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogFragment {
- void setStateFromBundle(Bundle b) {
- mMultiDisplayResolveInfo = (MultiDisplayResolveInfo) b.get(MULTI_DRI_KEY);
- mTargetInfos = mMultiDisplayResolveInfo.getTargets();
- mUserHandle = (UserHandle) b.get(USER_HANDLE_KEY);
- mParentWhich = b.getInt(WHICH_KEY);
+ /**
+ * Display a fragment for the user to select one of the members of a target "stack."
+ * @param stackedTarget The display info for the full stack to select within.
+ * @param stackedTargetParentWhich The "which" value that the {@link ChooserActivity} uses to
+ * identify the {@code stackedTarget} as presented in the chooser menu UI. If the user selects
+ * a target in this fragment, the selection will be saved in the {@link MultiDisplayResolveInfo}
+ * and then the {@link ChooserActivity} will receive a {@code #startSelected()} callback using
+ * this "which" value to identify the stack that's now unambiguously resolved.
+ * @param userHandle
+ *
+ * TODO: consider taking a client-provided callback instead of {@code stackedTargetParentWhich}
+ * to avoid coupling with {@link ChooserActivity}'s mechanism for handling the selection.
+ */
+ public static void show(
+ FragmentManager fragmentManager,
+ MultiDisplayResolveInfo stackedTarget,
+ int stackedTargetParentWhich,
+ UserHandle userHandle) {
+ ChooserStackedAppDialogFragment fragment = new ChooserStackedAppDialogFragment(
+ stackedTarget, stackedTargetParentWhich, userHandle);
+ fragment.show(fragmentManager, TARGET_DETAILS_FRAGMENT_TAG);
}
+ private final MultiDisplayResolveInfo mMultiDisplayResolveInfo;
+ private final int mParentWhich;
+
@Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(WHICH_KEY, mParentWhich);
- outState.putParcelable(MULTI_DRI_KEY, mMultiDisplayResolveInfo);
+ public void onClick(DialogInterface dialog, int which) {
+ mMultiDisplayResolveInfo.setSelected(which);
+ ((ChooserActivity) getActivity()).startSelected(mParentWhich, false, true);
+ dismiss();
}
@Override
@@ -63,15 +74,16 @@ public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogF
@Override
protected Drawable getItemIcon(DisplayResolveInfo dri) {
-
// Show no icon for the group disambig dialog, null hides the imageview
return null;
}
- @Override
- public void onClick(DialogInterface dialog, int which) {
- mMultiDisplayResolveInfo.setSelected(which);
- ((ChooserActivity) getActivity()).startSelected(mParentWhich, false, true);
- dismiss();
+ private ChooserStackedAppDialogFragment(
+ MultiDisplayResolveInfo stackedTarget,
+ int stackedTargetParentWhich,
+ UserHandle userHandle) {
+ super(stackedTarget.getAllDisplayTargets(), userHandle);
+ mMultiDisplayResolveInfo = stackedTarget;
+ mParentWhich = stackedTargetParentWhich;
}
}
diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
index ffd173c7..0aa32505 100644
--- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -19,15 +19,12 @@ package com.android.intentresolver;
import static android.content.Context.ACTIVITY_SERVICE;
-import static com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
-
import static java.util.stream.Collectors.toList;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.Dialog;
-import android.app.DialogFragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
@@ -49,11 +46,12 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.intentresolver.chooser.DisplayResolveInfo;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.RecyclerView;
-import com.android.internal.widget.RecyclerView;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
-import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -64,68 +62,61 @@ import java.util.stream.Collectors;
public class ChooserTargetActionsDialogFragment extends DialogFragment
implements DialogInterface.OnClickListener {
- protected ArrayList<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
- protected UserHandle mUserHandle;
- protected String mShortcutId;
- protected String mShortcutTitle;
- protected boolean mIsShortcutPinned;
- protected IntentFilter mIntentFilter;
+ protected final static String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment";
+
+ private final List<DisplayResolveInfo> mTargetInfos;
+ private final UserHandle mUserHandle;
+ private final boolean mIsShortcutPinned;
+
+ @Nullable
+ private final String mShortcutId;
- public static final String USER_HANDLE_KEY = "user_handle";
- public static final String TARGET_INFOS_KEY = "target_infos";
- public static final String SHORTCUT_ID_KEY = "shortcut_id";
- public static final String SHORTCUT_TITLE_KEY = "shortcut_title";
- public static final String IS_SHORTCUT_PINNED_KEY = "is_shortcut_pinned";
- public static final String INTENT_FILTER_KEY = "intent_filter";
+ @Nullable
+ private final String mShortcutTitle;
- public ChooserTargetActionsDialogFragment() {}
+ @Nullable
+ private final IntentFilter mIntentFilter;
+
+ public static void show(
+ FragmentManager fragmentManager,
+ List<DisplayResolveInfo> targetInfos,
+ UserHandle userHandle,
+ @Nullable String shortcutId,
+ @Nullable String shortcutTitle,
+ boolean isShortcutPinned,
+ @Nullable IntentFilter intentFilter) {
+ ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment(
+ targetInfos,
+ userHandle,
+ shortcutId,
+ shortcutTitle,
+ isShortcutPinned,
+ intentFilter);
+ fragment.show(fragmentManager, TARGET_DETAILS_FRAGMENT_TAG);
+ }
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
if (savedInstanceState != null) {
- setStateFromBundle(savedInstanceState);
- } else {
- setStateFromBundle(getArguments());
+ // Bail. It's probably not possible to trigger reloading our fragments from a saved
+ // instance since Sharesheet isn't kept in history and the entire session will probably
+ // be lost under any conditions that would've triggered our retention. Nevertheless, if
+ // we ever *did* try to load from a saved state, we wouldn't be able to populate valid
+ // data (since we wouldn't be able to get back our original TargetInfos if we had to
+ // restore them from a Bundle).
+ dismissAllowingStateLoss();
}
}
- void setStateFromBundle(Bundle b) {
- mTargetInfos = (ArrayList<DisplayResolveInfo>) b.get(TARGET_INFOS_KEY);
- mUserHandle = (UserHandle) b.get(USER_HANDLE_KEY);
- mShortcutId = b.getString(SHORTCUT_ID_KEY);
- mShortcutTitle = b.getString(SHORTCUT_TITLE_KEY);
- mIsShortcutPinned = b.getBoolean(IS_SHORTCUT_PINNED_KEY);
- mIntentFilter = (IntentFilter) b.get(INTENT_FILTER_KEY);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
- mUserHandle);
- outState.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY,
- mTargetInfos);
- outState.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY, mShortcutId);
- outState.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY,
- mIsShortcutPinned);
- outState.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY, mShortcutTitle);
- outState.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY, mIntentFilter);
- }
-
/**
- * Recreate the layout from scratch to match new Sharesheet redlines
+ * Build the menu UI according to our design spec.
*/
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
Bundle savedInstanceState) {
- if (savedInstanceState != null) {
- setStateFromBundle(savedInstanceState);
- } else {
- setStateFromBundle(getArguments());
- }
// Make the background transparent to show dialog rounding
Optional.of(getDialog()).map(Dialog::getWindow)
.ifPresent(window -> {
@@ -143,7 +134,7 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment
ImageView icon = v.findViewById(com.android.internal.R.id.icon);
RecyclerView rv = v.findViewById(com.android.internal.R.id.listContainer);
- final ResolveInfoPresentationGetter pg = getProvidingAppPresentationGetter();
+ final TargetPresentationGetter pg = getProvidingAppPresentationGetter();
title.setText(isShortcutTarget() ? mShortcutTitle : pg.getLabel());
icon.setImageDrawable(pg.getIcon(mUserHandle));
rv.setAdapter(new VHAdapter(items));
@@ -277,14 +268,14 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment
return getPinIcon(isPinned(dri));
}
- private ResolveInfoPresentationGetter getProvidingAppPresentationGetter() {
+ private TargetPresentationGetter getProvidingAppPresentationGetter() {
final ActivityManager am = (ActivityManager) getContext()
.getSystemService(ACTIVITY_SERVICE);
final int iconDpi = am.getLauncherLargeIconDensity();
// Use the matching application icon and label for the title, any TargetInfo will do
- return new ResolveInfoPresentationGetter(getContext(), iconDpi,
- mTargetInfos.get(0).getResolveInfo());
+ return new TargetPresentationGetter.Factory(getContext(), iconDpi)
+ .makePresentationGetter(mTargetInfos.get(0).getResolveInfo());
}
private boolean isPinned(DisplayResolveInfo dri) {
@@ -294,4 +285,24 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment
private boolean isShortcutTarget() {
return mShortcutId != null;
}
+
+ protected ChooserTargetActionsDialogFragment(
+ List<DisplayResolveInfo> targetInfos, UserHandle userHandle) {
+ this(targetInfos, userHandle, null, null, false, null);
+ }
+
+ private ChooserTargetActionsDialogFragment(
+ List<DisplayResolveInfo> targetInfos,
+ UserHandle userHandle,
+ @Nullable String shortcutId,
+ @Nullable String shortcutTitle,
+ boolean isShortcutPinned,
+ @Nullable IntentFilter intentFilter) {
+ mTargetInfos = targetInfos;
+ mUserHandle = userHandle;
+ mShortcutId = shortcutId;
+ mShortcutTitle = shortcutTitle;
+ mIsShortcutPinned = isShortcutPinned;
+ mIntentFilter = intentFilter;
+ }
}
diff --git a/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
new file mode 100644
index 00000000..a0bf61b6
--- /dev/null
+++ b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.intentresolver
+
+import android.app.Activity
+import android.app.SharedElementCallback
+import android.view.View
+import com.android.intentresolver.widget.ResolverDrawerLayout
+import java.util.function.Supplier
+
+/**
+ * A helper class to track app's readiness for the scene transition animation.
+ * The app is ready when both the image is laid out and the drawer offset is calculated.
+ */
+internal class EnterTransitionAnimationDelegate(
+ private val activity: Activity,
+ private val resolverDrawerLayoutSupplier: Supplier<ResolverDrawerLayout?>
+) : View.OnLayoutChangeListener {
+ private var removeSharedElements = false
+ private var previewReady = false
+ private var offsetCalculated = false
+
+ init {
+ activity.setEnterSharedElementCallback(
+ object : SharedElementCallback() {
+ override fun onMapSharedElements(
+ names: MutableList<String>, sharedElements: MutableMap<String, View>
+ ) {
+ this@EnterTransitionAnimationDelegate.onMapSharedElements(
+ names, sharedElements
+ )
+ }
+ })
+ }
+
+ fun postponeTransition() = activity.postponeEnterTransition()
+
+ fun markImagePreviewReady(runTransitionAnimation: Boolean) {
+ if (!runTransitionAnimation) {
+ removeSharedElements = true
+ }
+ if (!previewReady) {
+ previewReady = true
+ maybeStartListenForLayout()
+ }
+ }
+
+ fun markOffsetCalculated() {
+ if (!offsetCalculated) {
+ offsetCalculated = true
+ maybeStartListenForLayout()
+ }
+ }
+
+ private fun onMapSharedElements(
+ names: MutableList<String>,
+ sharedElements: MutableMap<String, View>
+ ) {
+ if (removeSharedElements) {
+ names.remove(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
+ sharedElements.remove(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
+ }
+ removeSharedElements = false
+ }
+
+ private fun maybeStartListenForLayout() {
+ val drawer = resolverDrawerLayoutSupplier.get()
+ if (previewReady && offsetCalculated && drawer != null) {
+ if (drawer.isInLayout) {
+ startPostponedEnterTransition()
+ } else {
+ drawer.addOnLayoutChangeListener(this)
+ drawer.requestLayout()
+ }
+ }
+ }
+
+ override fun onLayoutChange(
+ v: View,
+ left: Int, top: Int, right: Int, bottom: Int,
+ oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int
+ ) {
+ v.removeOnLayoutChangeListener(this)
+ startPostponedEnterTransition()
+ }
+
+ private fun startPostponedEnterTransition() {
+ if (!removeSharedElements && activity.isActivityTransitionRunning) {
+ // Disable the window animations as it interferes with the transition animation.
+ activity.window.setWindowAnimations(0)
+ }
+ activity.startPostponedEnterTransition()
+ }
+}
diff --git a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
new file mode 100644
index 00000000..9bbdf7c7
--- /dev/null
+++ b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
@@ -0,0 +1,225 @@
+/*
+ * 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.intentresolver;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.UserHandle;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Implementation of {@link AbstractMultiProfilePagerAdapter} that consolidates the variation in
+ * existing implementations; most overrides were only to vary type signatures (which are better
+ * represented via generic types), and a few minor behavioral customizations are now implemented
+ * through small injectable delegate classes.
+ * TODO: now that the existing implementations are shown to be expressible in terms of this new
+ * generic type, merge up into the base class and simplify the public APIs.
+ * TODO: attempt to further restrict visibility in the methods we expose.
+ * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive"
+ * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident
+ * waiting to happen since clients seem to make assumptions about which adapter will be "active" in
+ * a particular context, and more explicit APIs would make sure those were valid.
+ * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?)
+ *
+ * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter
+ * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
+ * the per-profile records.
+ * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to
+ * control the contents of a given per-profile list. This is provided for convenience, since it must
+ * be possible to get the list adapter from the page adapter via our {@link mListAdapterExtractor}.
+ *
+ * TODO: this class doesn't make any explicit usage of the {@link ResolverListAdapter} API, so the
+ * type constraint can probably be dropped once the API is merged upwards and cleaned.
+ */
+class GenericMultiProfilePagerAdapter<
+ PageViewT extends ViewGroup,
+ SinglePageAdapterT,
+ ListAdapterT extends ResolverListAdapter> extends AbstractMultiProfilePagerAdapter {
+
+ /** Delegate to set up a given adapter and page view to be used together. */
+ public interface AdapterBinder<PageViewT, SinglePageAdapterT> {
+ /**
+ * The given {@code view} will be associated with the given {@code adapter}. Do any work
+ * necessary to configure them compatibly, introduce them to each other, etc.
+ */
+ void bind(PageViewT view, SinglePageAdapterT adapter);
+ }
+
+ private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
+ private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
+ private final Supplier<ViewGroup> mPageViewInflater;
+ private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
+
+ private final ImmutableList<GenericProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
+
+ GenericMultiProfilePagerAdapter(
+ Context context,
+ Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
+ AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
+ ImmutableList<SinglePageAdapterT> adapters,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ Supplier<ViewGroup> pageViewInflater,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ super(
+ context,
+ /* currentPage= */ defaultProfile,
+ emptyStateProvider,
+ quietModeManager,
+ workProfileUserHandle);
+
+ mListAdapterExtractor = listAdapterExtractor;
+ mAdapterBinder = adapterBinder;
+ mPageViewInflater = pageViewInflater;
+ mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
+
+ ImmutableList.Builder<GenericProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
+ new ImmutableList.Builder<>();
+ for (SinglePageAdapterT adapter : adapters) {
+ items.add(createProfileDescriptor(adapter));
+ }
+ mItems = items.build();
+ }
+
+ private GenericProfileDescriptor<PageViewT, SinglePageAdapterT>
+ createProfileDescriptor(SinglePageAdapterT adapter) {
+ return new GenericProfileDescriptor<>(mPageViewInflater.get(), adapter);
+ }
+
+ @Override
+ protected GenericProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
+ return mItems.get(pageIndex);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ public PageViewT getListViewForIndex(int index) {
+ return getItem(index).mView;
+ }
+
+ @Override
+ @VisibleForTesting
+ public SinglePageAdapterT getAdapterForIndex(int index) {
+ return getItem(index).mAdapter;
+ }
+
+ @Override
+ protected void setupListAdapter(int pageIndex) {
+ mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
+ }
+
+ @Override
+ public ViewGroup instantiateItem(ViewGroup container, int position) {
+ setupListAdapter(position);
+ return super.instantiateItem(container, position);
+ }
+
+ @Override
+ @Nullable
+ protected ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
+ if (getActiveListAdapter().getUserHandle().equals(userHandle)) {
+ return getActiveListAdapter();
+ }
+ if ((getInactiveListAdapter() != null) && getInactiveListAdapter().getUserHandle().equals(
+ userHandle)) {
+ return getInactiveListAdapter();
+ }
+ return null;
+ }
+
+ @Override
+ @VisibleForTesting
+ public ListAdapterT getActiveListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage()));
+ }
+
+ @Override
+ @VisibleForTesting
+ public ListAdapterT getInactiveListAdapter() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage()));
+ }
+
+ @Override
+ public ListAdapterT getPersonalListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL));
+ }
+
+ @Override
+ public ListAdapterT getWorkListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
+ }
+
+ @Override
+ protected SinglePageAdapterT getCurrentRootAdapter() {
+ return getAdapterForIndex(getCurrentPage());
+ }
+
+ @Override
+ protected PageViewT getActiveAdapterView() {
+ return getListViewForIndex(getCurrentPage());
+ }
+
+ @Override
+ protected PageViewT getInactiveAdapterView() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return getListViewForIndex(1 - getCurrentPage());
+ }
+
+ @Override
+ protected void setupContainerPadding(View container) {
+ Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
+ bottomPaddingOverride.ifPresent(paddingBottom ->
+ container.setPadding(
+ container.getPaddingLeft(),
+ container.getPaddingTop(),
+ container.getPaddingRight(),
+ paddingBottom));
+ }
+
+ // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager"
+ // should be the owner of all per-profile data (especially now that the API is generic)?
+ private static class GenericProfileDescriptor<PageViewT, SinglePageAdapterT> extends
+ ProfileDescriptor {
+ private final SinglePageAdapterT mAdapter;
+ private final PageViewT mView;
+
+ GenericProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) {
+ super(rootView);
+ mAdapter = adapter;
+ mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list);
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
new file mode 100644
index 00000000..e68eb66a
--- /dev/null
+++ b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.intentresolver
+
+import android.graphics.Bitmap
+import android.net.Uri
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+// TODO: convert ChooserContentPreviewCoordinator to Kotlin and merge this class into it.
+internal class ImagePreviewImageLoader(
+ private val previewCoordinator: ChooserContentPreviewUi.ContentPreviewCoordinator
+) : suspend (Uri) -> Bitmap? {
+
+ override suspend fun invoke(uri: Uri): Bitmap? =
+ suspendCancellableCoroutine { continuation ->
+ val callback = java.util.function.Consumer<Bitmap?> { bitmap ->
+ try {
+ continuation.resumeWith(Result.success(bitmap))
+ } catch (ignored: Exception) {
+ }
+ }
+ previewCoordinator.loadImage(uri, callback)
+ }
+}
diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java
index 9b853c95..78240250 100644
--- a/java/src/com/android/intentresolver/IntentForwarderActivity.java
+++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java
@@ -28,7 +28,6 @@ import android.app.Activity;
import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.admin.DevicePolicyManager;
-import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Intent;
@@ -38,7 +37,6 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.metrics.LogMaker;
-import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -65,7 +63,6 @@ import java.util.concurrent.Executors;
* be passed in and out of a managed profile.
*/
public class IntentForwarderActivity extends Activity {
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static String TAG = "IntentForwarderActivity";
public static String FORWARD_INTENT_TO_PARENT
diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
new file mode 100644
index 00000000..5bf994d6
--- /dev/null
+++ b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
@@ -0,0 +1,154 @@
+/*
+ * 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.intentresolver;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+import android.stats.devicepolicy.nano.DevicePolicyEnums;
+
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.internal.R;
+
+import java.util.List;
+
+/**
+ * Chooser/ResolverActivity empty state provider that returns empty state which is shown when
+ * there are no apps available.
+ */
+public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
+
+ @NonNull
+ private final Context mContext;
+ @Nullable
+ private final UserHandle mWorkProfileUserHandle;
+ @Nullable
+ private final UserHandle mPersonalProfileUserHandle;
+ @NonNull
+ private final String mMetricsCategory;
+ @NonNull
+ private final MyUserIdProvider mMyUserIdProvider;
+
+ public NoAppsAvailableEmptyStateProvider(Context context, UserHandle workProfileUserHandle,
+ UserHandle personalProfileUserHandle, String metricsCategory,
+ MyUserIdProvider myUserIdProvider) {
+ mContext = context;
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mPersonalProfileUserHandle = personalProfileUserHandle;
+ mMetricsCategory = metricsCategory;
+ mMyUserIdProvider = myUserIdProvider;
+ }
+
+ @Nullable
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ UserHandle listUserHandle = resolverListAdapter.getUserHandle();
+
+ if (mWorkProfileUserHandle != null
+ && (mMyUserIdProvider.getMyUserId() == listUserHandle.getIdentifier()
+ || !hasAppsInOtherProfile(resolverListAdapter))) {
+
+ String title;
+ if (listUserHandle == mPersonalProfileUserHandle) {
+ title = mContext.getSystemService(
+ DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_PERSONAL_APPS,
+ () -> mContext.getString(R.string.resolver_no_personal_apps_available));
+ } else {
+ title = mContext.getSystemService(
+ DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_WORK_APPS,
+ () -> mContext.getString(R.string.resolver_no_work_apps_available));
+ }
+
+ return new NoAppsAvailableEmptyState(
+ title, mMetricsCategory,
+ /* isPersonalProfile= */ listUserHandle == mPersonalProfileUserHandle
+ );
+ } else if (mWorkProfileUserHandle == null) {
+ // Return default empty state without tracking
+ return new DefaultEmptyState();
+ }
+
+ return null;
+ }
+
+ private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) {
+ if (mWorkProfileUserHandle == null) {
+ return false;
+ }
+ List<ResolverActivity.ResolvedComponentInfo> resolversForIntent =
+ adapter.getResolversForUser(UserHandle.of(mMyUserIdProvider.getMyUserId()));
+ for (ResolverActivity.ResolvedComponentInfo info : resolversForIntent) {
+ ResolveInfo resolveInfo = info.getResolveInfoAt(0);
+ if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static class DefaultEmptyState implements EmptyState {
+ @Override
+ public boolean useDefaultEmptyView() {
+ return true;
+ }
+ }
+
+ public static class NoAppsAvailableEmptyState implements EmptyState {
+
+ @NonNull
+ private String mTitle;
+
+ @NonNull
+ private String mMetricsCategory;
+
+ private boolean mIsPersonalProfile;
+
+ public NoAppsAvailableEmptyState(String title, String metricsCategory,
+ boolean isPersonalProfile) {
+ mTitle = title;
+ mMetricsCategory = metricsCategory;
+ mIsPersonalProfile = isPersonalProfile;
+ }
+
+ @Nullable
+ @Override
+ public String getTitle() {
+ return mTitle;
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger.createEvent(
+ DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED)
+ .setStrings(mMetricsCategory)
+ .setBoolean(/*isPersonalProfile*/ mIsPersonalProfile)
+ .write();
+ }
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java
new file mode 100644
index 00000000..420d26c5
--- /dev/null
+++ b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java
@@ -0,0 +1,137 @@
+/*
+ * 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.intentresolver;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+
+/**
+ * Empty state provider that does not allow cross profile sharing, it will return a blocker
+ * in case if the profile of the current tab is not the same as the profile of the calling app.
+ */
+public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider {
+
+ private final UserHandle mPersonalProfileUserHandle;
+ private final EmptyState mNoWorkToPersonalEmptyState;
+ private final EmptyState mNoPersonalToWorkEmptyState;
+ private final CrossProfileIntentsChecker mCrossProfileIntentsChecker;
+ private final MyUserIdProvider mUserIdProvider;
+
+ public NoCrossProfileEmptyStateProvider(UserHandle personalUserHandle,
+ EmptyState noWorkToPersonalEmptyState,
+ EmptyState noPersonalToWorkEmptyState,
+ CrossProfileIntentsChecker crossProfileIntentsChecker,
+ MyUserIdProvider myUserIdProvider) {
+ mPersonalProfileUserHandle = personalUserHandle;
+ mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState;
+ mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState;
+ mCrossProfileIntentsChecker = crossProfileIntentsChecker;
+ mUserIdProvider = myUserIdProvider;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ boolean shouldShowBlocker =
+ mUserIdProvider.getMyUserId() != resolverListAdapter.getUserHandle().getIdentifier()
+ && !mCrossProfileIntentsChecker
+ .hasCrossProfileIntents(resolverListAdapter.getIntents(),
+ mUserIdProvider.getMyUserId(),
+ resolverListAdapter.getUserHandle().getIdentifier());
+
+ if (!shouldShowBlocker) {
+ return null;
+ }
+
+ if (resolverListAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) {
+ return mNoWorkToPersonalEmptyState;
+ } else {
+ return mNoPersonalToWorkEmptyState;
+ }
+ }
+
+
+ /**
+ * Empty state that gets strings from the device policy manager and tracks events into
+ * event logger of the device policy events.
+ */
+ public static class DevicePolicyBlockerEmptyState implements EmptyState {
+
+ @NonNull
+ private final Context mContext;
+ private final String mDevicePolicyStringTitleId;
+ @StringRes
+ private final int mDefaultTitleResource;
+ private final String mDevicePolicyStringSubtitleId;
+ @StringRes
+ private final int mDefaultSubtitleResource;
+ private final int mEventId;
+ @NonNull
+ private final String mEventCategory;
+
+ public DevicePolicyBlockerEmptyState(Context context, String devicePolicyStringTitleId,
+ @StringRes int defaultTitleResource, String devicePolicyStringSubtitleId,
+ @StringRes int defaultSubtitleResource,
+ int devicePolicyEventId, String devicePolicyEventCategory) {
+ mContext = context;
+ mDevicePolicyStringTitleId = devicePolicyStringTitleId;
+ mDefaultTitleResource = defaultTitleResource;
+ mDevicePolicyStringSubtitleId = devicePolicyStringSubtitleId;
+ mDefaultSubtitleResource = defaultSubtitleResource;
+ mEventId = devicePolicyEventId;
+ mEventCategory = devicePolicyEventCategory;
+ }
+
+ @Nullable
+ @Override
+ public String getTitle() {
+ return mContext.getSystemService(DevicePolicyManager.class).getResources().getString(
+ mDevicePolicyStringTitleId,
+ () -> mContext.getString(mDefaultTitleResource));
+ }
+
+ @Nullable
+ @Override
+ public String getSubtitle() {
+ return mContext.getSystemService(DevicePolicyManager.class).getResources().getString(
+ mDevicePolicyStringSubtitleId,
+ () -> mContext.getString(mDefaultSubtitleResource));
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger.createEvent(mEventId)
+ .setStrings(mEventCategory)
+ .write();
+ }
+
+ @Override
+ public boolean shouldSkipDataRebuild() {
+ return true;
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 453a6e84..5573e18a 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -19,6 +19,9 @@ package com.android.intentresolver;
import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
@@ -26,6 +29,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.PermissionChecker.PID_UNKNOWN;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import android.annotation.Nullable;
@@ -39,7 +44,6 @@ import android.app.VoiceInteractor.PickOptionRequest.Option;
import android.app.VoiceInteractor.Prompt;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
-import android.compat.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -55,7 +59,9 @@ import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Insets;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.PatternMatcher;
@@ -90,18 +96,25 @@ import android.widget.TabWidget;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CompositeEmptyStateProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile;
-import com.android.intentresolver.chooser.ChooserTargetInfo;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
+import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
-
+import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.util.LatencyTracker;
-import com.android.internal.widget.ResolverDrawerLayout;
-import com.android.internal.widget.ViewPager;
import java.util.ArrayList;
import java.util.Arrays;
@@ -109,6 +122,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.function.Supplier;
/**
* This activity is displayed when the system attempts to start an Intent for
@@ -116,10 +130,9 @@ import java.util.Set;
* which to go to. It is not normally used directly by application developers.
*/
@UiThread
-public class ResolverActivity extends Activity implements
+public class ResolverActivity extends FragmentActivity implements
ResolverListAdapter.ResolverListCommunicator {
- @UnsupportedAppUsage
public ResolverActivity() {
mIsIntentPicker = getClass().equals(ResolverActivity.class);
}
@@ -149,7 +162,6 @@ public class ResolverActivity extends Activity implements
@VisibleForTesting
protected boolean mSupportsAlwaysUseOption;
protected ResolverDrawerLayout mResolverDrawerLayout;
- @UnsupportedAppUsage
protected PackageManager mPm;
protected int mLaunchedFromUid;
@@ -165,17 +177,12 @@ public class ResolverActivity extends Activity implements
/** See {@link #setRetainInOnStop}. */
private boolean mRetainInOnStop;
- private static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
- private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
- private static final String OPEN_LINKS_COMPONENT_KEY = "app_link_state";
protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";
protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
/** Tracks if we should ignore future broadcasts telling us the work profile is enabled */
private boolean mWorkProfileHasBeenEnabled = false;
- @VisibleForTesting
- public static boolean ENABLE_TABBED_VIEW = true;
private static final String TAB_TAG_PERSONAL = "personal";
private static final String TAB_TAG_WORK = "work";
@@ -185,6 +192,8 @@ public class ResolverActivity extends Activity implements
@VisibleForTesting
protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter;
+ protected QuietModeManager mQuietModeManager;
+
// Intent extra for connected audio devices
public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
@@ -214,7 +223,14 @@ public class ResolverActivity extends Activity implements
private BroadcastReceiver mWorkProfileStateReceiver;
private UserHandle mHeaderCreatorUser;
- private UserHandle mWorkProfileUserHandle;
+ private Supplier<UserHandle> mLazyWorkProfileUserHandle = () -> {
+ final UserHandle result = fetchWorkProfileUserProfile();
+ mLazyWorkProfileUserHandle = () -> result;
+ return result;
+ };
+
+ @Nullable
+ private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
protected final LatencyTracker mLatencyTracker = getLatencyTracker();
@@ -360,7 +376,6 @@ public class ResolverActivity extends Activity implements
* Compatibility version for other bundled services that use this overload without
* a default title resource
*/
- @UnsupportedAppUsage
protected void onCreate(Bundle savedInstanceState, Intent intent,
CharSequence title, Intent[] initialIntents,
List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
@@ -374,6 +389,8 @@ public class ResolverActivity extends Activity implements
setTheme(appliedThemeResId());
super.onCreate(savedInstanceState);
+ mQuietModeManager = createQuietModeManager();
+
// Determine whether we should show that intent is forwarded
// from managed profile to owner or other way around.
setProfileSwitchMessage(intent.getContentUserHint());
@@ -395,7 +412,6 @@ public class ResolverActivity extends Activity implements
mDefaultTitleResId = defaultTitleRes;
mSupportsAlwaysUseOption = supportsAlwaysUseOption;
- mWorkProfileUserHandle = fetchWorkProfileUserProfile();
// The last argument of createResolverListAdapter is whether to do special handling
// of the last used choice to highlight it in the list. We need to always
@@ -474,6 +490,111 @@ public class ResolverActivity extends Activity implements
return resolverMultiProfilePagerAdapter;
}
+ @VisibleForTesting
+ protected MyUserIdProvider createMyUserIdProvider() {
+ return new MyUserIdProvider();
+ }
+
+ @VisibleForTesting
+ protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+ return new CrossProfileIntentsChecker(getContentResolver());
+ }
+
+ @VisibleForTesting
+ protected QuietModeManager createQuietModeManager() {
+ UserManager userManager = getSystemService(UserManager.class);
+ return new QuietModeManager() {
+
+ private boolean mIsWaitingToEnableWorkProfile = false;
+
+ @Override
+ public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+ return userManager.isQuietModeEnabled(workProfileUserHandle);
+ }
+
+ @Override
+ public void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle) {
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+ userManager.requestQuietModeEnabled(enabled, workProfileUserHandle);
+ });
+ mIsWaitingToEnableWorkProfile = true;
+ }
+
+ @Override
+ public void markWorkProfileEnabledBroadcastReceived() {
+ mIsWaitingToEnableWorkProfile = false;
+ }
+
+ @Override
+ public boolean isWaitingToEnableWorkProfile() {
+ return mIsWaitingToEnableWorkProfile;
+ }
+ };
+ }
+
+ protected EmptyStateProvider createBlockerEmptyStateProvider() {
+ final boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser());
+
+ if (!shouldShowNoCrossProfileIntentsEmptyState) {
+ // Implementation that doesn't show any blockers
+ return new EmptyStateProvider() {};
+ }
+
+ final AbstractMultiProfilePagerAdapter.EmptyState
+ noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(/* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL,
+ /* defaultSubtitleResource= */
+ R.string.resolver_cant_access_personal_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+
+ final AbstractMultiProfilePagerAdapter.EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(/* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK,
+ /* defaultSubtitleResource= */
+ R.string.resolver_cant_access_work_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+
+ return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(),
+ noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(), createMyUserIdProvider());
+ }
+
+ protected EmptyStateProvider createEmptyStateProvider(
+ @Nullable UserHandle workProfileUserHandle) {
+ final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();
+
+ final EmptyStateProvider workProfileOffEmptyStateProvider =
+ new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle,
+ mQuietModeManager,
+ /* onSwitchOnWorkSelectedListener= */
+ () -> { if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }},
+ getMetricsCategory());
+
+ final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
+ this,
+ workProfileUserHandle,
+ getPersonalProfileUserHandle(),
+ getMetricsCategory(),
+ createMyUserIdProvider()
+ );
+
+ // Return composite provider, the order matters (the higher, the more priority)
+ return new CompositeEmptyStateProvider(
+ blockerEmptyStateProvider,
+ workProfileOffEmptyStateProvider,
+ noAppsEmptyStateProvider
+ );
+ }
+
private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForOneProfile(
Intent[] initialIntents,
List<ResolveInfo> rList, boolean filterLastUsed) {
@@ -484,13 +605,21 @@ public class ResolverActivity extends Activity implements
rList,
filterLastUsed,
/* userHandle */ UserHandle.of(UserHandle.myUserId()));
+ QuietModeManager quietModeManager = createQuietModeManager();
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
adapter,
- getPersonalProfileUserHandle(),
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ quietModeManager,
/* workProfileUserHandle= */ null);
}
+ private UserHandle getIntentUser() {
+ return getIntent().hasExtra(EXTRA_CALLING_USER)
+ ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
+ : getUser();
+ }
+
private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
Intent[] initialIntents,
List<ResolveInfo> rList,
@@ -499,9 +628,7 @@ public class ResolverActivity extends Activity implements
// the intent resolver is started in the other profile. Since this is the only case when
// this happens, we check for it here and set the current profile's tab.
int selectedProfile = getCurrentProfile();
- UserHandle intentUser = getIntent().hasExtra(EXTRA_CALLING_USER)
- ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
- : getUser();
+ UserHandle intentUser = getIntentUser();
if (!getUser().equals(intentUser)) {
if (getPersonalProfileUserHandle().equals(intentUser)) {
selectedProfile = PROFILE_PERSONAL;
@@ -534,14 +661,15 @@ public class ResolverActivity extends Activity implements
(filterLastUsed && UserHandle.myUserId()
== workProfileUserHandle.getIdentifier()),
/* userHandle */ workProfileUserHandle);
+ QuietModeManager quietModeManager = createQuietModeManager();
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
personalAdapter,
workAdapter,
+ createEmptyStateProvider(getWorkProfileUserHandle()),
+ quietModeManager,
selectedProfile,
- getPersonalProfileUserHandle(),
- getWorkProfileUserHandle(),
- /* shouldShowNoCrossProfileIntentsEmptyState= */ getUser().equals(intentUser));
+ getWorkProfileUserHandle());
}
protected int appliedThemeResId() {
@@ -574,19 +702,25 @@ public class ResolverActivity extends Activity implements
protected UserHandle getPersonalProfileUserHandle() {
return UserHandle.of(ActivityManager.getCurrentUser());
}
- protected @Nullable UserHandle getWorkProfileUserHandle() {
- return mWorkProfileUserHandle;
+
+ @Nullable
+ protected UserHandle getWorkProfileUserHandle() {
+ return mLazyWorkProfileUserHandle.get();
}
- protected @Nullable UserHandle fetchWorkProfileUserProfile() {
- mWorkProfileUserHandle = null;
+ @Nullable
+ private UserHandle fetchWorkProfileUserProfile() {
UserManager userManager = getSystemService(UserManager.class);
+ if (userManager == null) {
+ return null;
+ }
+ UserHandle result = null;
for (final UserInfo userInfo : userManager.getProfiles(ActivityManager.getCurrentUser())) {
if (userInfo.isManagedProfile()) {
- mWorkProfileUserHandle = userInfo.getUserHandle();
+ result = userInfo.getUserHandle();
}
}
- return mWorkProfileUserHandle;
+ return result;
}
private boolean hasWorkProfile() {
@@ -594,7 +728,7 @@ public class ResolverActivity extends Activity implements
}
protected boolean shouldShowTabs() {
- return hasWorkProfile() && ENABLE_TABBED_VIEW;
+ return hasWorkProfile();
}
protected void onProfileClick(View v) {
@@ -726,7 +860,6 @@ public class ResolverActivity extends Activity implements
}
}
- @Override // SelectableTargetInfoCommunicator ResolverListCommunicator
public Intent getTargetIntent() {
return mIntents.isEmpty() ? null : mIntents.get(0);
}
@@ -848,9 +981,9 @@ public class ResolverActivity extends Activity implements
}
mRegistered = true;
}
- if (shouldShowTabs() && mMultiProfilePagerAdapter.isWaitingToEnableWorkProfile()) {
- if (mMultiProfilePagerAdapter.isQuietModeEnabled(getWorkProfileUserHandle())) {
- mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived();
+ if (shouldShowTabs() && mQuietModeManager.isWaitingToEnableWorkProfile()) {
+ if (mQuietModeManager.isQuietModeEnabled(getWorkProfileUserHandle())) {
+ mQuietModeManager.markWorkProfileEnabledBroadcastReceived();
}
}
mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
@@ -1375,7 +1508,7 @@ public class ResolverActivity extends Activity implements
.createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
.setBoolean(currentUserHandle.equals(getPersonalProfileUserHandle()))
.setStrings(getMetricsCategory(),
- cti instanceof ChooserTargetInfo ? "direct_share" : "other_target")
+ cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
.write();
}
@@ -1407,8 +1540,16 @@ public class ResolverActivity extends Activity implements
Intent startIntent = getIntent();
boolean isAudioCaptureDevice =
startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
- return new ResolverListAdapter(context, payloadIntents, initialIntents, rList,
- filterLastUsed, createListController(userHandle), this,
+ return new ResolverListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ getTargetIntent(),
+ this,
isAudioCaptureDevice);
}
@@ -1472,17 +1613,25 @@ public class ResolverActivity extends Activity implements
setContentView(mLayoutId);
DisplayResolveInfo sameProfileResolveInfo =
- mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0);
+ mMultiProfilePagerAdapter.getActiveListAdapter().getFirstDisplayResolveInfo();
boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
- ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter();
- DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
+ final ResolverListAdapter inactiveAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ final DisplayResolveInfo otherProfileResolveInfo =
+ inactiveAdapter.getFirstDisplayResolveInfo();
// Load the icon asynchronously
ImageView icon = findViewById(com.android.internal.R.id.icon);
- ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask(
- otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon));
- iconTask.execute();
+ inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) {
+ @Override
+ protected void onPostExecute(Drawable drawable) {
+ if (!isDestroyed()) {
+ otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);
+ new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+ }
+ }
+ }.execute();
((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
getResources().getString(
@@ -1521,31 +1670,29 @@ public class ResolverActivity extends Activity implements
|| mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
return false;
}
- List<DisplayResolveInfo> sameProfileList =
- mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList;
- List<DisplayResolveInfo> otherProfileList =
- mMultiProfilePagerAdapter.getInactiveListAdapter().mDisplayList;
+ ResolverListAdapter sameProfileAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ ResolverListAdapter otherProfileAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
- if (sameProfileList.isEmpty()) {
+ if (sameProfileAdapter.getDisplayResolveInfoCount() == 0) {
Log.d(TAG, "No targets in the current profile");
return false;
}
- if (otherProfileList.size() != 1) {
- Log.d(TAG, "Found " + otherProfileList.size() + " resolvers in the other profile");
+ if (otherProfileAdapter.getDisplayResolveInfoCount() != 1) {
+ Log.d(TAG, "Other-profile count: " + otherProfileAdapter.getDisplayResolveInfoCount());
return false;
}
- if (otherProfileList.get(0).getResolveInfo().handleAllWebDataURI) {
+ if (otherProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
Log.d(TAG, "Other profile is a web browser");
return false;
}
- for (DisplayResolveInfo info : sameProfileList) {
- if (!info.getResolveInfo().handleAllWebDataURI) {
- Log.d(TAG, "Non-browser found in this profile");
- return false;
- }
+ if (!sameProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
+ Log.d(TAG, "Non-browser found in this profile");
+ return false;
}
return true;
@@ -1800,13 +1947,12 @@ public class ResolverActivity extends Activity implements
onHorizontalSwipeStateChanged(state);
}
});
- mMultiProfilePagerAdapter.setOnSwitchOnWorkSelectedListener(
- () -> {
- final View workTab = tabHost.getTabWidget().getChildAt(1);
- workTab.setFocusable(true);
- workTab.setFocusableInTouchMode(true);
- workTab.requestFocus();
- });
+ mOnSwitchOnWorkSelectedListener = () -> {
+ final View workTab = tabHost.getTabWidget().getChildAt(1);
+ workTab.setFocusable(true);
+ workTab.setFocusableInTouchMode(true);
+ workTab.requestFocus();
+ };
}
private String getPersonalTabLabel() {
@@ -2067,7 +2213,7 @@ public class ResolverActivity extends Activity implements
public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) {
if (listAdapter.getUserHandle().equals(getWorkProfileUserHandle())
- && mMultiProfilePagerAdapter.isWaitingToEnableWorkProfile()) {
+ && mQuietModeManager.isWaitingToEnableWorkProfile()) {
// We have just turned on the work profile and entered the pass code to start it,
// now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
// point in reloading the list now, since the work profile user is still
@@ -2119,7 +2265,7 @@ public class ResolverActivity extends Activity implements
}
mWorkProfileHasBeenEnabled = true;
- mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived();
+ mQuietModeManager.markWorkProfileEnabledBroadcastReceived();
} else {
// Must be an UNAVAILABLE broadcast, so we watch for the next availability
mWorkProfileHasBeenEnabled = false;
@@ -2135,13 +2281,11 @@ public class ResolverActivity extends Activity implements
};
}
- @VisibleForTesting
public static final class ResolvedComponentInfo {
public final ComponentName name;
private final List<Intent> mIntents = new ArrayList<>();
private final List<ResolveInfo> mResolveInfos = new ArrayList<>();
private boolean mPinned;
- private boolean mFixedAtTop;
public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) {
this.name = name;
@@ -2190,14 +2334,6 @@ public class ResolverActivity extends Activity implements
public void setPinned(boolean pinned) {
mPinned = pinned;
}
-
- public boolean isFixedAtTop() {
- return mFixedAtTop;
- }
-
- public void setFixedAtTop(boolean isFixedAtTop) {
- mFixedAtTop = isFixedAtTop;
- }
}
class ItemClickListener implements AdapterView.OnItemClickListener,
@@ -2254,8 +2390,9 @@ public class ResolverActivity extends Activity implements
}
- static final boolean isSpecificUriMatch(int match) {
- match = match&IntentFilter.MATCH_CATEGORY_MASK;
+ /** Determine whether a given match result is considered "specific" in our application. */
+ public static final boolean isSpecificUriMatch(int match) {
+ match = (match & IntentFilter.MATCH_CATEGORY_MASK);
return match >= IntentFilter.MATCH_CATEGORY_HOST
&& match <= IntentFilter.MATCH_CATEGORY_PATH;
}
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 898d8c8e..eecb914c 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -26,15 +26,11 @@ import android.content.Context;
import android.content.Intent;
import android.content.PermissionChecker;
import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
import android.content.pm.LabeledIntent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
-import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.RemoteException;
@@ -54,44 +50,62 @@ import android.widget.TextView;
import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
-
import com.android.internal.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class ResolverListAdapter extends BaseAdapter {
private static final String TAG = "ResolverListAdapter";
+ @Nullable // TODO: other model for lazy computation? Or just precompute?
+ private static ColorMatrixColorFilter sSuspendedMatrixColorFilter;
+
+ protected final Context mContext;
+ protected final LayoutInflater mInflater;
+ protected final ResolverListCommunicator mResolverListCommunicator;
+ protected final ResolverListController mResolverListController;
+ protected final TargetPresentationGetter.Factory mPresentationFactory;
+
private final List<Intent> mIntents;
private final Intent[] mInitialIntents;
private final List<ResolveInfo> mBaseResolveList;
private final PackageManager mPm;
- protected final Context mContext;
- private static ColorMatrixColorFilter sSuspendedMatrixColorFilter;
private final int mIconDpi;
- protected ResolveInfo mLastChosen;
+ private final boolean mIsAudioCaptureDevice;
+ private final UserHandle mUserHandle;
+ private final Intent mTargetIntent;
+
+ private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
+ private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>();
+
+ private ResolveInfo mLastChosen;
private DisplayResolveInfo mOtherProfile;
- ResolverListController mResolverListController;
private int mPlaceholderCount;
- protected final LayoutInflater mInflater;
-
// This one is the list that the Adapter will actually present.
- List<DisplayResolveInfo> mDisplayList;
+ private List<DisplayResolveInfo> mDisplayList;
private List<ResolvedComponentInfo> mUnfilteredResolveList;
private int mLastChosenPosition = -1;
private boolean mFilterLastUsed;
- final ResolverListCommunicator mResolverListCommunicator;
private Runnable mPostListReadyRunnable;
- private final boolean mIsAudioCaptureDevice;
private boolean mIsTabLoaded;
- public ResolverListAdapter(Context context, List<Intent> payloadIntents,
- Intent[] initialIntents, List<ResolveInfo> rList,
+ public ResolverListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
boolean filterLastUsed,
ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
ResolverListCommunicator resolverListCommunicator,
boolean isAudioCaptureDevice) {
mContext = context;
@@ -103,10 +117,21 @@ public class ResolverListAdapter extends BaseAdapter {
mDisplayList = new ArrayList<>();
mFilterLastUsed = filterLastUsed;
mResolverListController = resolverListController;
+ mUserHandle = userHandle;
+ mTargetIntent = targetIntent;
mResolverListCommunicator = resolverListCommunicator;
mIsAudioCaptureDevice = isAudioCaptureDevice;
final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE);
mIconDpi = am.getLauncherLargeIconDensity();
+ mPresentationFactory = new TargetPresentationGetter.Factory(mContext, mIconDpi);
+ }
+
+ public final DisplayResolveInfo getFirstDisplayResolveInfo() {
+ return mDisplayList.get(0);
+ }
+
+ public final ImmutableList<DisplayResolveInfo> getTargetsInCurrentDisplayList() {
+ return ImmutableList.copyOf(mDisplayList);
}
public void handlePackagesChanged() {
@@ -258,7 +283,7 @@ public class ResolverListAdapter extends BaseAdapter {
if (mBaseResolveList != null) {
List<ResolvedComponentInfo> currentResolveList = new ArrayList<>();
mResolverListController.addResolveListDedupe(currentResolveList,
- mResolverListCommunicator.getTargetIntent(),
+ mTargetIntent,
mBaseResolveList);
return currentResolveList;
} else {
@@ -334,7 +359,12 @@ public class ResolverListAdapter extends BaseAdapter {
if (otherProfileInfo != null) {
mOtherProfile = makeOtherProfileDisplayResolveInfo(
- mContext, otherProfileInfo, mPm, mResolverListCommunicator, mIconDpi);
+ mContext,
+ otherProfileInfo,
+ mPm,
+ mTargetIntent,
+ mResolverListCommunicator,
+ mIconDpi);
} else {
mOtherProfile = null;
try {
@@ -441,8 +471,13 @@ public class ResolverListAdapter extends BaseAdapter {
ri.icon = 0;
}
- addResolveInfo(new DisplayResolveInfo(ii, ri,
- ri.loadLabel(mPm), null, ii, makePresentationGetter(ri)));
+ addResolveInfo(DisplayResolveInfo.newDisplayResolveInfo(
+ ii,
+ ri,
+ ri.loadLabel(mPm),
+ null,
+ ii,
+ mPresentationFactory.makePresentationGetter(ri)));
}
}
@@ -490,10 +525,12 @@ public class ResolverListAdapter extends BaseAdapter {
final Intent replaceIntent =
mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent);
final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent(
- add.activityInfo, mResolverListCommunicator.getTargetIntent());
- final DisplayResolveInfo
- dri = new DisplayResolveInfo(intent, add,
- replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add));
+ add.activityInfo, mTargetIntent);
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ intent,
+ add,
+ (replaceIntent != null) ? replaceIntent : defaultIntent,
+ mPresentationFactory.makePresentationGetter(add));
dri.setPinned(rci.isPinned());
if (rci.isPinned()) {
Log.i(TAG, "Pinned item: " + rci.name);
@@ -597,11 +634,15 @@ public class ResolverListAdapter extends BaseAdapter {
return position;
}
- public int getDisplayResolveInfoCount() {
+ public final int getDisplayResolveInfoCount() {
return mDisplayList.size();
}
- public DisplayResolveInfo getDisplayResolveInfo(int index) {
+ public final boolean allResolveInfosHandleAllWebDataUri() {
+ return mDisplayList.stream().allMatch(t -> t.getResolveInfo().handleAllWebDataURI);
+ }
+
+ public final DisplayResolveInfo getDisplayResolveInfo(int index) {
// Used to query services. We only query services for primary targets, not alternates.
return mDisplayList.get(index);
}
@@ -634,28 +675,49 @@ public class ResolverListAdapter extends BaseAdapter {
protected void onBindView(View view, TargetInfo info, int position) {
final ViewHolder holder = (ViewHolder) view.getTag();
if (info == null) {
- holder.icon.setImageDrawable(
- mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ holder.icon.setImageDrawable(loadIconPlaceholder());
+ holder.bindLabel("", "", false);
return;
}
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayLabel()) {
- getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
- } else {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+ if (info.isDisplayResolveInfo()) {
+ DisplayResolveInfo dri = (DisplayResolveInfo) info;
+ if (dri.hasDisplayLabel()) {
+ holder.bindLabel(
+ dri.getDisplayLabel(),
+ dri.getExtendedInfo(),
+ alwaysShowSubLabel());
+ } else {
+ holder.bindLabel("", "", false);
+ loadLabel(dri);
+ }
+ holder.bindIcon(info);
+ if (!dri.hasDisplayIcon()) {
+ loadIcon(dri);
+ }
}
+ }
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayIcon()) {
- new LoadIconTask((DisplayResolveInfo) info, holder).execute();
- } else {
- holder.bindIcon(info);
+ protected final void loadIcon(DisplayResolveInfo info) {
+ LoadIconTask task = mIconLoaders.get(info);
+ if (task == null) {
+ task = new LoadIconTask(info);
+ mIconLoaders.put(info, task);
+ task.execute();
+ }
+ }
+
+ private void loadLabel(DisplayResolveInfo info) {
+ LoadLabelTask task = mLabelLoaders.get(info);
+ if (task == null) {
+ task = createLoadLabelTask(info);
+ mLabelLoaders.put(info, task);
+ task.execute();
}
}
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelTask(info, holder);
+ protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+ return new LoadLabelTask(info);
}
public void onDestroy() {
@@ -666,6 +728,16 @@ public class ResolverListAdapter extends BaseAdapter {
if (mResolverListController != null) {
mResolverListController.destroy();
}
+ cancelTasks(mIconLoaders.values());
+ cancelTasks(mLabelLoaders.values());
+ mIconLoaders.clear();
+ mLabelLoaders.clear();
+ }
+
+ private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) {
+ for (T task: tasks) {
+ task.cancel(false);
+ }
}
private static ColorMatrixColorFilter getSuspendedColorMatrix() {
@@ -691,17 +763,13 @@ public class ResolverListAdapter extends BaseAdapter {
return sSuspendedMatrixColorFilter;
}
- ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) {
- return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai);
- }
-
- ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) {
- return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri);
- }
-
Drawable loadIconForResolveInfo(ResolveInfo ri) {
// Load icons based on the current process. If in work profile icons should be badged.
- return makePresentationGetter(ri).getIcon(getUserHandle());
+ return mPresentationFactory.makePresentationGetter(ri).getIcon(getUserHandle());
+ }
+
+ protected final Drawable loadIconPlaceholder() {
+ return mContext.getDrawable(R.drawable.resolver_icon_placeholder);
}
void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
@@ -710,7 +778,15 @@ public class ResolverListAdapter extends BaseAdapter {
new AsyncTask<Void, Void, Drawable>() {
@Override
protected Drawable doInBackground(Void... params) {
- return loadIconForResolveInfo(iconInfo.getResolveInfo());
+ Drawable drawable;
+ try {
+ drawable = loadIconForResolveInfo(iconInfo.getResolveInfo());
+ } catch (Exception e) {
+ ComponentName componentName = iconInfo.getResolvedComponentName();
+ Log.e(TAG, "Failed to load app icon for " + componentName, e);
+ drawable = loadIconPlaceholder();
+ }
+ return drawable;
}
@Override
@@ -721,9 +797,8 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
- @VisibleForTesting
public UserHandle getUserHandle() {
- return mResolverListController.getUserHandle();
+ return mUserHandle;
}
protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
@@ -779,6 +854,7 @@ public class ResolverListAdapter extends BaseAdapter {
Context context,
ResolvedComponentInfo resolvedComponentInfo,
PackageManager pm,
+ Intent targetIntent,
ResolverListCommunicator resolverListCommunicator,
int iconDpi) {
ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);
@@ -787,13 +863,13 @@ public class ResolverListAdapter extends BaseAdapter {
resolveInfo.activityInfo,
resolvedComponentInfo.getIntentAt(0));
Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
- resolveInfo.activityInfo,
- resolverListCommunicator.getTargetIntent());
+ resolveInfo.activityInfo, targetIntent);
- ResolveInfoPresentationGetter presentationGetter =
- new ResolveInfoPresentationGetter(context, iconDpi, resolveInfo);
+ TargetPresentationGetter presentationGetter =
+ new TargetPresentationGetter.Factory(context, iconDpi)
+ .makePresentationGetter(resolveInfo);
- return new DisplayResolveInfo(
+ return DisplayResolveInfo.newDisplayResolveInfo(
resolvedComponentInfo.getIntentAt(0),
resolveInfo,
resolveInfo.loadLabel(pm),
@@ -829,13 +905,12 @@ public class ResolverListAdapter extends BaseAdapter {
*/
default boolean shouldGetOnlyDefaultActivities() { return true; };
- Intent getTargetIntent();
-
void onHandlePackagesChanged(ResolverListAdapter listAdapter);
}
/**
- * A view holder.
+ * A view holder keeps a reference to a list view and provides functionality for managing its
+ * state.
*/
@VisibleForTesting
public static class ViewHolder {
@@ -877,7 +952,7 @@ public class ResolverListAdapter extends BaseAdapter {
}
public void bindIcon(TargetInfo info) {
- icon.setImageDrawable(info.getDisplayIcon(itemView.getContext()));
+ icon.setImageDrawable(info.getDisplayIconHolder().getDisplayIcon());
if (info.isSuspended()) {
icon.setColorFilter(getSuspendedColorMatrix());
} else {
@@ -888,17 +963,15 @@ public class ResolverListAdapter extends BaseAdapter {
protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
private final DisplayResolveInfo mDisplayResolveInfo;
- private final ViewHolder mHolder;
- protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
+ protected LoadLabelTask(DisplayResolveInfo dri) {
mDisplayResolveInfo = dri;
- mHolder = holder;
}
@Override
protected CharSequence[] doInBackground(Void... voids) {
- ResolveInfoPresentationGetter pg =
- makePresentationGetter(mDisplayResolveInfo.getResolveInfo());
+ TargetPresentationGetter pg = mPresentationFactory.makePresentationGetter(
+ mDisplayResolveInfo.getResolveInfo());
if (mIsAudioCaptureDevice) {
// This is an audio capture device, so check record permissions
@@ -930,26 +1003,33 @@ public class ResolverListAdapter extends BaseAdapter {
@Override
protected void onPostExecute(CharSequence[] result) {
+ if (mDisplayResolveInfo.hasDisplayLabel()) {
+ return;
+ }
mDisplayResolveInfo.setDisplayLabel(result[0]);
mDisplayResolveInfo.setExtendedInfo(result[1]);
- mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel());
+ notifyDataSetChanged();
}
}
class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
protected final DisplayResolveInfo mDisplayResolveInfo;
private final ResolveInfo mResolveInfo;
- private ViewHolder mHolder;
- LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) {
+ LoadIconTask(DisplayResolveInfo dri) {
mDisplayResolveInfo = dri;
mResolveInfo = dri.getResolveInfo();
- mHolder = holder;
}
@Override
protected Drawable doInBackground(Void... params) {
- return loadIconForResolveInfo(mResolveInfo);
+ try {
+ return loadIconForResolveInfo(mResolveInfo);
+ } catch (Exception e) {
+ ComponentName componentName = mDisplayResolveInfo.getResolvedComponentName();
+ Log.e(TAG, "Failed to load app icon for " + componentName, e);
+ return loadIconPlaceholder();
+ }
}
@Override
@@ -957,207 +1037,9 @@ public class ResolverListAdapter extends BaseAdapter {
if (getOtherProfile() == mDisplayResolveInfo) {
mResolverListCommunicator.updateProfileViewButton();
} else if (!mDisplayResolveInfo.hasDisplayIcon()) {
- mDisplayResolveInfo.setDisplayIcon(d);
- mHolder.bindIcon(mDisplayResolveInfo);
- // Notify in case view is already bound to resolve the race conditions on
- // low end devices
+ mDisplayResolveInfo.getDisplayIconHolder().setDisplayIcon(d);
notifyDataSetChanged();
}
}
-
- public void setViewHolder(ViewHolder holder) {
- mHolder = holder;
- mHolder.bindIcon(mDisplayResolveInfo);
- }
- }
-
- /**
- * Loads the icon and label for the provided ResolveInfo.
- */
- @VisibleForTesting
- public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
- private final ResolveInfo mRi;
- public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) {
- super(ctx, iconDpi, ri.activityInfo);
- mRi = ri;
- }
-
- @Override
- Drawable getIconSubstituteInternal() {
- Drawable dr = null;
- try {
- // Do not use ResolveInfo#getIconResource() as it defaults to the app
- if (mRi.resolvePackageName != null && mRi.icon != 0) {
- dr = loadIconFromResource(
- mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon);
- }
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
- + "couldn't find resources for package", e);
- }
-
- // Fall back to ActivityInfo if no icon is found via ResolveInfo
- if (dr == null) dr = super.getIconSubstituteInternal();
-
- return dr;
- }
-
- @Override
- String getAppSubLabelInternal() {
- // Will default to app name if no intent filter or activity label set, make sure to
- // check if subLabel matches label before final display
- return mRi.loadLabel(mPm).toString();
- }
-
- @Override
- String getAppLabelForSubstitutePermission() {
- // Will default to app name if no activity label set
- return mRi.getComponentInfo().loadLabel(mPm).toString();
- }
- }
-
- /**
- * Loads the icon and label for the provided ActivityInfo.
- */
- @VisibleForTesting
- public static class ActivityInfoPresentationGetter extends
- TargetPresentationGetter {
- private final ActivityInfo mActivityInfo;
- public ActivityInfoPresentationGetter(Context ctx, int iconDpi,
- ActivityInfo activityInfo) {
- super(ctx, iconDpi, activityInfo.applicationInfo);
- mActivityInfo = activityInfo;
- }
-
- @Override
- Drawable getIconSubstituteInternal() {
- Drawable dr = null;
- try {
- // Do not use ActivityInfo#getIconResource() as it defaults to the app
- if (mActivityInfo.icon != 0) {
- dr = loadIconFromResource(
- mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
- mActivityInfo.icon);
- }
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
- + "couldn't find resources for package", e);
- }
-
- return dr;
- }
-
- @Override
- String getAppSubLabelInternal() {
- // Will default to app name if no activity label set, make sure to check if subLabel
- // matches label before final display
- return (String) mActivityInfo.loadLabel(mPm);
- }
-
- @Override
- String getAppLabelForSubstitutePermission() {
- return getAppSubLabelInternal();
- }
- }
-
- /**
- * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application
- * icon and label over any IntentFilter or Activity icon to increase user understanding, with an
- * exception for applications that hold the right permission. Always attempts to use available
- * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
- * Strings to strip creative formatting.
- */
- private abstract static class TargetPresentationGetter {
- @Nullable abstract Drawable getIconSubstituteInternal();
- @Nullable abstract String getAppSubLabelInternal();
- @Nullable abstract String getAppLabelForSubstitutePermission();
-
- private Context mCtx;
- private final int mIconDpi;
- private final boolean mHasSubstitutePermission;
- private final ApplicationInfo mAi;
-
- protected PackageManager mPm;
-
- TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) {
- mCtx = ctx;
- mPm = ctx.getPackageManager();
- mAi = ai;
- mIconDpi = iconDpi;
- mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
- android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
- mAi.packageName);
- }
-
- public Drawable getIcon(UserHandle userHandle) {
- return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle));
- }
-
- public Bitmap getIconBitmap(@Nullable UserHandle userHandle) {
- Drawable dr = null;
- if (mHasSubstitutePermission) {
- dr = getIconSubstituteInternal();
- }
-
- if (dr == null) {
- try {
- if (mAi.icon != 0) {
- dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon);
- }
- } catch (PackageManager.NameNotFoundException ignore) {
- }
- }
-
- // Fall back to ApplicationInfo#loadIcon if nothing has been loaded
- if (dr == null) {
- dr = mAi.loadIcon(mPm);
- }
-
- SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx);
- Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle);
- sif.recycle();
-
- return icon;
- }
-
- public String getLabel() {
- String label = null;
- // Apps with the substitute permission will always show the activity label as the
- // app label if provided
- if (mHasSubstitutePermission) {
- label = getAppLabelForSubstitutePermission();
- }
-
- if (label == null) {
- label = (String) mAi.loadLabel(mPm);
- }
-
- return label;
- }
-
- public String getSubLabel() {
- // Apps with the substitute permission will always show the resolve info label as the
- // sublabel if provided
- if (mHasSubstitutePermission){
- String appSubLabel = getAppSubLabelInternal();
- // Use the resolve info label as sublabel if it is set
- if(!TextUtils.isEmpty(appSubLabel)
- && !TextUtils.equals(appSubLabel, getLabel())){
- return appSubLabel;
- }
- return null;
- }
- return getAppSubLabelInternal();
- }
-
- protected String loadLabelFromResource(Resources res, int resId) {
- return res.getString(resId);
- }
-
- @Nullable
- protected Drawable loadIconFromResource(Resources res, int resId) {
- return res.getDrawableForDensity(resId, mIconDpi);
- }
-
}
}
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
index ff616ce0..bfffe0d8 100644
--- a/java/src/com/android/intentresolver/ResolverListController.java
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -32,7 +32,8 @@ import android.os.UserHandle;
import android.util.Log;
import com.android.intentresolver.chooser.DisplayResolveInfo;
-
+import com.android.intentresolver.model.AbstractResolverComparator;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
@@ -187,7 +188,6 @@ public class ResolverListController {
final ResolverActivity.ResolvedComponentInfo rci =
new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo);
rci.setPinned(isComponentPinned(name));
- rci.setFixedAtTop(isFixedAtTop(name));
into.add(rci);
}
}
@@ -202,14 +202,6 @@ public class ResolverListController {
return false;
}
- /**
- * Whether this component is fixed at top in the ranked apps list. Always false for Resolver;
- * overridden in Chooser.
- */
- public boolean isFixedAtTop(ComponentName name) {
- return false;
- }
-
// Filter out any activities that the launched uid does not have permission for.
// To preserve the inputList, optionally will return the original list if any modification has
// been made.
@@ -274,19 +266,6 @@ public class ResolverListController {
return listToReturn;
}
- private class ComputeCallback implements AbstractResolverComparator.AfterCompute {
-
- private CountDownLatch mFinishComputeSignal;
-
- public ComputeCallback(CountDownLatch finishComputeSignal) {
- mFinishComputeSignal = finishComputeSignal;
- }
-
- public void afterCompute () {
- mFinishComputeSignal.countDown();
- }
- }
-
private void compute(List<ResolverActivity.ResolvedComponentInfo> inputList)
throws InterruptedException {
if (mResolverComparator == null) {
@@ -294,8 +273,7 @@ public class ResolverListController {
return;
}
final CountDownLatch finishComputeSignal = new CountDownLatch(1);
- ComputeCallback callback = new ComputeCallback(finishComputeSignal);
- mResolverComparator.setCallBack(callback);
+ mResolverComparator.setCallBack(() -> finishComputeSignal.countDown());
mResolverComparator.compute(inputList);
finishComputeSignal.await();
isComputed = true;
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
index 56d326c1..65de9409 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -16,264 +16,99 @@
package com.android.intentresolver;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
-
-import android.annotation.Nullable;
-import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.UserHandle;
import android.view.LayoutInflater;
-import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
+import androidx.viewpager.widget.PagerAdapter;
+
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.PagerAdapter;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Optional;
+import java.util.function.Supplier;
/**
* A {@link PagerAdapter} which describes the work and personal profile intent resolver screens.
*/
@VisibleForTesting
-public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerAdapter {
-
- private final ResolverProfileDescriptor[] mItems;
- private final boolean mShouldShowNoCrossProfileIntentsEmptyState;
- private boolean mUseLayoutWithDefault;
+public class ResolverMultiProfilePagerAdapter extends
+ GenericMultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
+ private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
- ResolverMultiProfilePagerAdapter(Context context,
+ ResolverMultiProfilePagerAdapter(
+ Context context,
ResolverListAdapter adapter,
- UserHandle personalProfileUserHandle,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
UserHandle workProfileUserHandle) {
- super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle);
- mItems = new ResolverProfileDescriptor[] {
- createProfileDescriptor(adapter)
- };
- mShouldShowNoCrossProfileIntentsEmptyState = true;
+ this(
+ context,
+ ImmutableList.of(adapter),
+ emptyStateProvider,
+ quietModeManager,
+ /* defaultProfile= */ 0,
+ workProfileUserHandle,
+ new BottomPaddingOverrideSupplier());
}
ResolverMultiProfilePagerAdapter(Context context,
ResolverListAdapter personalAdapter,
ResolverListAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle) {
+ this(
+ context,
+ ImmutableList.of(personalAdapter, workAdapter),
+ emptyStateProvider,
+ quietModeManager,
+ defaultProfile,
+ workProfileUserHandle,
+ new BottomPaddingOverrideSupplier());
+ }
+
+ private ResolverMultiProfilePagerAdapter(
+ Context context,
+ ImmutableList<ResolverListAdapter> listAdapters,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
@Profile int defaultProfile,
- UserHandle personalProfileUserHandle,
UserHandle workProfileUserHandle,
- boolean shouldShowNoCrossProfileIntentsEmptyState) {
- super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
- workProfileUserHandle);
- mItems = new ResolverProfileDescriptor[] {
- createProfileDescriptor(personalAdapter),
- createProfileDescriptor(workAdapter)
- };
- mShouldShowNoCrossProfileIntentsEmptyState = shouldShowNoCrossProfileIntentsEmptyState;
- }
-
- private ResolverProfileDescriptor createProfileDescriptor(
- ResolverListAdapter adapter) {
- final LayoutInflater inflater = LayoutInflater.from(getContext());
- final ViewGroup rootView =
- (ViewGroup) inflater.inflate(R.layout.resolver_list_per_profile, null, false);
- return new ResolverProfileDescriptor(rootView, adapter);
- }
-
- ListView getListViewForIndex(int index) {
- return getItem(index).listView;
- }
-
- @Override
- ResolverProfileDescriptor getItem(int pageIndex) {
- return mItems[pageIndex];
- }
-
- @Override
- int getItemCount() {
- return mItems.length;
- }
-
- @Override
- void setupListAdapter(int pageIndex) {
- final ListView listView = getItem(pageIndex).listView;
- listView.setAdapter(getItem(pageIndex).resolverListAdapter);
- }
-
- @Override
- @VisibleForTesting
- public ResolverListAdapter getAdapterForIndex(int pageIndex) {
- return mItems[pageIndex].resolverListAdapter;
- }
-
- @Override
- public ViewGroup instantiateItem(ViewGroup container, int position) {
- setupListAdapter(position);
- return super.instantiateItem(container, position);
- }
-
- @Override
- @Nullable
- ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle) {
- if (getActiveListAdapter().getUserHandle().equals(userHandle)) {
- return getActiveListAdapter();
- } else if (getInactiveListAdapter() != null
- && getInactiveListAdapter().getUserHandle().equals(userHandle)) {
- return getInactiveListAdapter();
- }
- return null;
- }
-
- @Override
- @VisibleForTesting
- public ResolverListAdapter getActiveListAdapter() {
- return getAdapterForIndex(getCurrentPage());
- }
-
- @Override
- @VisibleForTesting
- public ResolverListAdapter getInactiveListAdapter() {
- if (getCount() == 1) {
- return null;
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+ super(
+ context,
+ listAdapter -> listAdapter,
+ (listView, bindAdapter) -> listView.setAdapter(bindAdapter),
+ listAdapters,
+ emptyStateProvider,
+ quietModeManager,
+ defaultProfile,
+ workProfileUserHandle,
+ () -> (ViewGroup) LayoutInflater.from(context).inflate(
+ R.layout.resolver_list_per_profile, null, false),
+ bottomPaddingOverrideSupplier);
+ mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
+ }
+
+ public void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+ mBottomPaddingOverrideSupplier.setUseLayoutWithDefault(useLayoutWithDefault);
+ }
+
+ private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
+ private boolean mUseLayoutWithDefault;
+
+ public void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+ mUseLayoutWithDefault = useLayoutWithDefault;
}
- return getAdapterForIndex(1 - getCurrentPage());
- }
-
- @Override
- public ResolverListAdapter getPersonalListAdapter() {
- return getAdapterForIndex(PROFILE_PERSONAL);
- }
-
- @Override
- @Nullable
- public ResolverListAdapter getWorkListAdapter() {
- return getAdapterForIndex(PROFILE_WORK);
- }
-
- @Override
- ResolverListAdapter getCurrentRootAdapter() {
- return getActiveListAdapter();
- }
-
- @Override
- ListView getActiveAdapterView() {
- return getListViewForIndex(getCurrentPage());
- }
-
- @Override
- @Nullable
- ViewGroup getInactiveAdapterView() {
- if (getCount() == 1) {
- return null;
- }
- return getListViewForIndex(1 - getCurrentPage());
- }
-
- @Override
- String getMetricsCategory() {
- return ResolverActivity.METRICS_CATEGORY_RESOLVER;
- }
-
- @Override
- boolean allowShowNoCrossProfileIntentsEmptyState() {
- return mShouldShowNoCrossProfileIntentsEmptyState;
- }
-
- @Override
- protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter,
- View.OnClickListener listener) {
- showEmptyState(activeListAdapter,
- getWorkAppPausedTitle(),
- /* subtitle = */ null,
- listener);
- }
-
- @Override
- protected void showNoPersonalToWorkIntentsEmptyState(ResolverListAdapter activeListAdapter) {
- showEmptyState(activeListAdapter,
- getCrossProfileBlockedTitle(),
- getCantAccessWorkMessage());
- }
-
- @Override
- protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) {
- showEmptyState(activeListAdapter,
- getCrossProfileBlockedTitle(),
- getCantAccessPersonalMessage());
- }
-
- @Override
- protected void showNoPersonalAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
- showEmptyState(listAdapter,
- getNoPersonalAppsAvailableMessage(),
- /* subtitle = */ null);
- }
-
- @Override
- protected void showNoWorkAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
- showEmptyState(listAdapter,
- getNoWorkAppsAvailableMessage(),
- /* subtitle= */ null);
- }
-
- private String getWorkAppPausedTitle() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_WORK_PAUSED_TITLE,
- () -> getContext().getString(R.string.resolver_turn_on_work_apps));
- }
-
- private String getCrossProfileBlockedTitle() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
- () -> getContext().getString(R.string.resolver_cross_profile_blocked));
- }
-
- private String getCantAccessWorkMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_CANT_ACCESS_WORK,
- () -> getContext().getString(
- R.string.resolver_cant_access_work_apps_explanation));
- }
-
- private String getCantAccessPersonalMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_CANT_ACCESS_PERSONAL,
- () -> getContext().getString(
- R.string.resolver_cant_access_personal_apps_explanation));
- }
-
- private String getNoWorkAppsAvailableMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_NO_WORK_APPS,
- () -> getContext().getString(
- R.string.resolver_no_work_apps_available));
- }
-
- private String getNoPersonalAppsAvailableMessage() {
- return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_NO_PERSONAL_APPS,
- () -> getContext().getString(
- R.string.resolver_no_personal_apps_available));
- }
-
- void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
- mUseLayoutWithDefault = useLayoutWithDefault;
- }
-
- @Override
- protected void setupContainerPadding(View container) {
- int bottom = mUseLayoutWithDefault ? container.getPaddingBottom() : 0;
- container.setPadding(container.getPaddingLeft(), container.getPaddingTop(),
- container.getPaddingRight(), bottom);
- }
- class ResolverProfileDescriptor extends ProfileDescriptor {
- private ResolverListAdapter resolverListAdapter;
- final ListView listView;
- ResolverProfileDescriptor(ViewGroup rootView, ResolverListAdapter adapter) {
- super(rootView);
- resolverListAdapter = adapter;
- listView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+ @Override
+ public Optional<Integer> get() {
+ return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0);
}
}
}
diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
index 1c234526..0804a2b8 100644
--- a/java/src/com/android/intentresolver/ResolverViewPager.java
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -21,7 +21,7 @@ import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
-import com.android.internal.widget.ViewPager;
+import androidx.viewpager.widget.ViewPager;
/**
* A {@link ViewPager} which wraps around its tallest child's height.
@@ -41,15 +41,6 @@ public class ResolverViewPager extends ViewPager {
super(context, attrs);
}
- public ResolverViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public ResolverViewPager(Context context, AttributeSet attrs,
- int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
new file mode 100644
index 00000000..645b9391
--- /dev/null
+++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
@@ -0,0 +1,193 @@
+/*
+ * 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.intentresolver;
+
+import android.annotation.Nullable;
+import android.app.prediction.AppTarget;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.service.chooser.ChooserTarget;
+import android.util.Log;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.SelectableTargetInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class ShortcutSelectionLogic {
+ private static final String TAG = "ShortcutSelectionLogic";
+ private static final boolean DEBUG = false;
+ private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
+ private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
+
+ private final int mMaxShortcutTargetsPerApp;
+ private final boolean mApplySharingAppLimits;
+
+ // Descending order
+ private final Comparator<ChooserTarget> mBaseTargetComparator =
+ (lhs, rhs) -> Float.compare(rhs.getScore(), lhs.getScore());
+
+ ShortcutSelectionLogic(
+ int maxShortcutTargetsPerApp,
+ boolean applySharingAppLimits) {
+ mMaxShortcutTargetsPerApp = maxShortcutTargetsPerApp;
+ mApplySharingAppLimits = applySharingAppLimits;
+ }
+
+ /**
+ * Evaluate targets for inclusion in the direct share area. May not be included
+ * if score is too low.
+ */
+ public boolean addServiceResults(
+ @Nullable DisplayResolveInfo origTarget,
+ float origTargetScore,
+ List<ChooserTarget> targets,
+ boolean isShortcutResult,
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
+ Map<ChooserTarget, AppTarget> directShareToAppTargets,
+ Context userContext,
+ Intent targetIntent,
+ Intent referrerFillInIntent,
+ int maxRankedTargets,
+ List<TargetInfo> serviceTargets) {
+ if (DEBUG) {
+ Log.d(TAG, "addServiceResults "
+ + (origTarget == null ? null : origTarget.getResolvedComponentName()) + ", "
+ + targets.size()
+ + " targets");
+ }
+ if (targets.size() == 0) {
+ return false;
+ }
+ Collections.sort(targets, mBaseTargetComparator);
+ final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
+ : MAX_CHOOSER_TARGETS_PER_APP;
+ final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets)
+ : targets.size();
+ float lastScore = 0;
+ boolean shouldNotify = false;
+ for (int i = 0, count = targetsLimit; i < count; i++) {
+ final ChooserTarget target = targets.get(i);
+ float targetScore = target.getScore();
+ if (mApplySharingAppLimits) {
+ targetScore *= origTargetScore;
+ if (i > 0 && targetScore >= lastScore) {
+ // Apply a decay so that the top app can't crowd out everything else.
+ // This incents ChooserTargetServices to define what's truly better.
+ targetScore = lastScore * 0.95f;
+ }
+ }
+ ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target)
+ : null;
+ if ((shortcutInfo != null) && shortcutInfo.isPinned()) {
+ targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST;
+ }
+ ResolveInfo backupResolveInfo;
+ Intent resolvedIntent;
+ if (origTarget == null) {
+ resolvedIntent = createResolvedIntentForCallerTarget(target, targetIntent);
+ backupResolveInfo = userContext.getPackageManager()
+ .resolveActivity(
+ resolvedIntent,
+ PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA));
+ } else {
+ resolvedIntent = origTarget.getResolvedIntent();
+ backupResolveInfo = null;
+ }
+ boolean isInserted = insertServiceTarget(
+ SelectableTargetInfo.newSelectableTargetInfo(
+ origTarget,
+ backupResolveInfo,
+ resolvedIntent,
+ target,
+ targetScore,
+ shortcutInfo,
+ directShareToAppTargets.get(target),
+ referrerFillInIntent),
+ maxRankedTargets,
+ serviceTargets);
+
+ shouldNotify |= isInserted;
+
+ if (DEBUG) {
+ Log.d(TAG, " => " + target + " score=" + targetScore
+ + " base=" + target.getScore()
+ + " lastScore=" + lastScore
+ + " baseScore=" + origTargetScore
+ + " applyAppLimit=" + mApplySharingAppLimits);
+ }
+
+ lastScore = targetScore;
+ }
+
+ return shouldNotify;
+ }
+
+ /**
+ * Creates a resolved intent for a caller-specified target.
+ * @param target, a caller-specified target.
+ * @param targetIntent, a target intent for the Chooser (see {@link Intent#EXTRA_INTENT}).
+ */
+ private static Intent createResolvedIntentForCallerTarget(
+ ChooserTarget target, Intent targetIntent) {
+ final Intent resolvedIntent = new Intent(targetIntent);
+ resolvedIntent.setComponent(target.getComponentName());
+ resolvedIntent.putExtras(target.getIntentExtras());
+ return resolvedIntent;
+ }
+
+ private boolean insertServiceTarget(
+ TargetInfo chooserTargetInfo,
+ int maxRankedTargets,
+ List<TargetInfo> serviceTargets) {
+
+ // Check for duplicates and abort if found
+ for (TargetInfo otherTargetInfo : serviceTargets) {
+ if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
+ return false;
+ }
+ }
+
+ int currentSize = serviceTargets.size();
+ final float newScore = chooserTargetInfo.getModifiedScore();
+ for (int i = 0; i < Math.min(currentSize, maxRankedTargets);
+ i++) {
+ final TargetInfo serviceTarget = serviceTargets.get(i);
+ if (serviceTarget == null) {
+ serviceTargets.set(i, chooserTargetInfo);
+ return true;
+ } else if (newScore > serviceTarget.getModifiedScore()) {
+ serviceTargets.add(i, chooserTargetInfo);
+ return true;
+ }
+ }
+
+ if (currentSize < maxRankedTargets) {
+ serviceTargets.add(chooserTargetInfo);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/java/src/com/android/intentresolver/SimpleIconFactory.java b/java/src/com/android/intentresolver/SimpleIconFactory.java
index b05b4f68..ec5179ac 100644
--- a/java/src/com/android/intentresolver/SimpleIconFactory.java
+++ b/java/src/com/android/intentresolver/SimpleIconFactory.java
@@ -50,6 +50,8 @@ import android.util.AttributeSet;
import android.util.Pools.SynchronizedPool;
import android.util.TypedValue;
+import com.android.internal.annotations.VisibleForTesting;
+
import org.xmlpull.v1.XmlPullParser;
import java.nio.ByteBuffer;
@@ -67,6 +69,7 @@ public class SimpleIconFactory {
private static final SynchronizedPool<SimpleIconFactory> sPool =
new SynchronizedPool<>(Runtime.getRuntime().availableProcessors());
+ private static boolean sPoolEnabled = true;
private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
private static final float BLUR_FACTOR = 1.5f / 48;
@@ -90,7 +93,7 @@ public class SimpleIconFactory {
*/
@Deprecated
public static SimpleIconFactory obtain(Context ctx) {
- SimpleIconFactory instance = sPool.acquire();
+ SimpleIconFactory instance = sPoolEnabled ? sPool.acquire() : null;
if (instance == null) {
final ActivityManager am = (ActivityManager) ctx.getSystemService(ACTIVITY_SERVICE);
final int iconDpi = (am == null) ? 0 : am.getLauncherLargeIconDensity();
@@ -104,6 +107,17 @@ public class SimpleIconFactory {
return instance;
}
+ /**
+ * Enables or disables SimpleIconFactory objects pooling. It is enabled in production, you
+ * could use this method in tests and disable the pooling to make the icon rendering more
+ * deterministic because some sizing parameters will not be cached. Please ensure that you
+ * reset this value back after finishing the test.
+ */
+ @VisibleForTesting
+ public static void setPoolEnabled(boolean poolEnabled) {
+ sPoolEnabled = poolEnabled;
+ }
+
private static int getAttrDimFromContext(Context ctx, @AttrRes int attrId, String errorMsg) {
final Resources res = ctx.getResources();
TypedValue outVal = new TypedValue();
diff --git a/java/src/com/android/intentresolver/TargetPresentationGetter.java b/java/src/com/android/intentresolver/TargetPresentationGetter.java
new file mode 100644
index 00000000..f8b36566
--- /dev/null
+++ b/java/src/com/android/intentresolver/TargetPresentationGetter.java
@@ -0,0 +1,267 @@
+/*
+ * 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.intentresolver;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application icon
+ * and label over any IntentFilter or Activity icon to increase user understanding, with an
+ * exception for applications that hold the right permission. Always attempts to use available
+ * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
+ * Strings to strip creative formatting.
+ *
+ * Use one of the {@link TargetPresentationGetter#Factory} methods to create an instance of the
+ * appropriate concrete type.
+ *
+ * TODO: once this component (and its tests) are merged, it should be possible to refactor and
+ * vastly simplify by precomputing conditional logic at initialization.
+ */
+public abstract class TargetPresentationGetter {
+ private static final String TAG = "ResolverListAdapter";
+
+ /** Helper to build appropriate type-specific {@link TargetPresentationGetter} instances. */
+ public static class Factory {
+ private final Context mContext;
+ private final int mIconDpi;
+
+ public Factory(Context context, int iconDpi) {
+ mContext = context;
+ mIconDpi = iconDpi;
+ }
+
+ /** Make a {@link TargetPresentationGetter} for an {@link ActivityInfo}. */
+ public TargetPresentationGetter makePresentationGetter(ActivityInfo activityInfo) {
+ return new ActivityInfoPresentationGetter(mContext, mIconDpi, activityInfo);
+ }
+
+ /** Make a {@link TargetPresentationGetter} for a {@link ResolveInfo}. */
+ public TargetPresentationGetter makePresentationGetter(ResolveInfo resolveInfo) {
+ return new ResolveInfoPresentationGetter(mContext, mIconDpi, resolveInfo);
+ }
+ }
+
+ @Nullable
+ protected abstract Drawable getIconSubstituteInternal();
+
+ @Nullable
+ protected abstract String getAppSubLabelInternal();
+
+ @Nullable
+ protected abstract String getAppLabelForSubstitutePermission();
+
+ private Context mContext;
+ private final int mIconDpi;
+ private final boolean mHasSubstitutePermission;
+ private final ApplicationInfo mAppInfo;
+
+ protected PackageManager mPm;
+
+ /**
+ * Retrieve the image that should be displayed as the icon when this target is presented to the
+ * specified {@code userHandle}.
+ */
+ public Drawable getIcon(UserHandle userHandle) {
+ return new BitmapDrawable(mContext.getResources(), getIconBitmap(userHandle));
+ }
+
+ /**
+ * Retrieve the image that should be displayed as the icon when this target is presented to the
+ * specified {@code userHandle}.
+ */
+ public Bitmap getIconBitmap(@Nullable UserHandle userHandle) {
+ Drawable drawable = null;
+ if (mHasSubstitutePermission) {
+ drawable = getIconSubstituteInternal();
+ }
+
+ if (drawable == null) {
+ try {
+ if (mAppInfo.icon != 0) {
+ drawable = loadIconFromResource(
+ mPm.getResourcesForApplication(mAppInfo), mAppInfo.icon);
+ }
+ } catch (PackageManager.NameNotFoundException ignore) { }
+ }
+
+ // Fall back to ApplicationInfo#loadIcon if nothing has been loaded
+ if (drawable == null) {
+ drawable = mAppInfo.loadIcon(mPm);
+ }
+
+ SimpleIconFactory iconFactory = SimpleIconFactory.obtain(mContext);
+ Bitmap icon = iconFactory.createUserBadgedIconBitmap(drawable, userHandle);
+ iconFactory.recycle();
+
+ return icon;
+ }
+
+ /** Get the label to display for the target. */
+ public String getLabel() {
+ String label = null;
+ // Apps with the substitute permission will always show the activity label as the app label
+ // if provided.
+ if (mHasSubstitutePermission) {
+ label = getAppLabelForSubstitutePermission();
+ }
+
+ if (label == null) {
+ label = (String) mAppInfo.loadLabel(mPm);
+ }
+
+ return label;
+ }
+
+ /**
+ * Get the sublabel to display for the target. Clients are responsible for deduping their
+ * presentation if this returns the same value as {@link #getLabel()}.
+ * TODO: this class should take responsibility for that deduping internally so it's an
+ * authoritative record of exactly the content that should be presented.
+ */
+ public String getSubLabel() {
+ // Apps with the substitute permission will always show the resolve info label as the
+ // sublabel if provided
+ if (mHasSubstitutePermission) {
+ String appSubLabel = getAppSubLabelInternal();
+ // Use the resolve info label as sublabel if it is set
+ if (!TextUtils.isEmpty(appSubLabel) && !TextUtils.equals(appSubLabel, getLabel())) {
+ return appSubLabel;
+ }
+ return null;
+ }
+ return getAppSubLabelInternal();
+ }
+
+ protected String loadLabelFromResource(Resources res, int resId) {
+ return res.getString(resId);
+ }
+
+ @Nullable
+ protected Drawable loadIconFromResource(Resources res, int resId) {
+ return res.getDrawableForDensity(resId, mIconDpi);
+ }
+
+ private TargetPresentationGetter(Context context, int iconDpi, ApplicationInfo appInfo) {
+ mContext = context;
+ mPm = context.getPackageManager();
+ mAppInfo = appInfo;
+ mIconDpi = iconDpi;
+ mHasSubstitutePermission = (PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
+ android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
+ mAppInfo.packageName));
+ }
+
+ /** Loads the icon and label for the provided ResolveInfo. */
+ private static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
+ private final ResolveInfo mResolveInfo;
+
+ ResolveInfoPresentationGetter(
+ Context context, int iconDpi, ResolveInfo resolveInfo) {
+ super(context, iconDpi, resolveInfo.activityInfo);
+ mResolveInfo = resolveInfo;
+ }
+
+ @Override
+ protected Drawable getIconSubstituteInternal() {
+ Drawable drawable = null;
+ try {
+ // Do not use ResolveInfo#getIconResource() as it defaults to the app
+ if (mResolveInfo.resolvePackageName != null && mResolveInfo.icon != 0) {
+ drawable = loadIconFromResource(
+ mPm.getResourcesForApplication(mResolveInfo.resolvePackageName),
+ mResolveInfo.icon);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+ + "couldn't find resources for package", e);
+ }
+
+ // Fall back to ActivityInfo if no icon is found via ResolveInfo
+ if (drawable == null) {
+ drawable = super.getIconSubstituteInternal();
+ }
+
+ return drawable;
+ }
+
+ @Override
+ protected String getAppSubLabelInternal() {
+ // Will default to app name if no intent filter or activity label set, make sure to
+ // check if subLabel matches label before final display
+ return mResolveInfo.loadLabel(mPm).toString();
+ }
+
+ @Override
+ protected String getAppLabelForSubstitutePermission() {
+ // Will default to app name if no activity label set
+ return mResolveInfo.getComponentInfo().loadLabel(mPm).toString();
+ }
+ }
+
+ /** Loads the icon and label for the provided {@link ActivityInfo}. */
+ private static class ActivityInfoPresentationGetter extends TargetPresentationGetter {
+ private final ActivityInfo mActivityInfo;
+
+ ActivityInfoPresentationGetter(
+ Context context, int iconDpi, ActivityInfo activityInfo) {
+ super(context, iconDpi, activityInfo.applicationInfo);
+ mActivityInfo = activityInfo;
+ }
+
+ @Override
+ protected Drawable getIconSubstituteInternal() {
+ Drawable drawable = null;
+ try {
+ // Do not use ActivityInfo#getIconResource() as it defaults to the app
+ if (mActivityInfo.icon != 0) {
+ drawable = loadIconFromResource(
+ mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
+ mActivityInfo.icon);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+ + "couldn't find resources for package", e);
+ }
+
+ return drawable;
+ }
+
+ @Override
+ protected String getAppSubLabelInternal() {
+ // Will default to app name if no activity label set, make sure to check if subLabel
+ // matches label before final display
+ return (String) mActivityInfo.loadLabel(mPm);
+ }
+
+ @Override
+ protected String getAppLabelForSubstitutePermission() {
+ return getAppSubLabelInternal();
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
new file mode 100644
index 00000000..b7c89907
--- /dev/null
+++ b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
@@ -0,0 +1,114 @@
+/*
+ * 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.intentresolver;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+import android.stats.devicepolicy.nano.DevicePolicyEnums;
+
+import com.android.internal.R;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
+
+/**
+ * Chooser/ResolverActivity empty state provider that returns empty state which is shown when
+ * work profile is paused and we need to show a button to enable it.
+ */
+public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
+
+ private final UserHandle mWorkProfileUserHandle;
+ private final QuietModeManager mQuietModeManager;
+ private final String mMetricsCategory;
+ private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+ private final Context mContext;
+
+ public WorkProfilePausedEmptyStateProvider(@NonNull Context context,
+ @Nullable UserHandle workProfileUserHandle,
+ @NonNull QuietModeManager quietModeManager,
+ @Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener,
+ @NonNull String metricsCategory) {
+ mContext = context;
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mQuietModeManager = quietModeManager;
+ mMetricsCategory = metricsCategory;
+ mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ if (!resolverListAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+ || !mQuietModeManager.isQuietModeEnabled(mWorkProfileUserHandle)
+ || resolverListAdapter.getCount() == 0) {
+ return null;
+ }
+
+ final String title = mContext.getSystemService(DevicePolicyManager.class)
+ .getResources().getString(RESOLVER_WORK_PAUSED_TITLE,
+ () -> mContext.getString(R.string.resolver_turn_on_work_apps));
+
+ return new WorkProfileOffEmptyState(title, (tab) -> {
+ tab.showSpinner();
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ mQuietModeManager.requestQuietModeEnabled(false, mWorkProfileUserHandle);
+ }, mMetricsCategory);
+ }
+
+ public static class WorkProfileOffEmptyState implements EmptyState {
+
+ private final String mTitle;
+ private final ClickListener mOnClick;
+ private final String mMetricsCategory;
+
+ public WorkProfileOffEmptyState(String title, @NonNull ClickListener onClick,
+ @NonNull String metricsCategory) {
+ mTitle = title;
+ mOnClick = onClick;
+ mMetricsCategory = metricsCategory;
+ }
+
+ @Nullable
+ @Override
+ public String getTitle() {
+ return mTitle;
+ }
+
+ @Nullable
+ @Override
+ public ClickListener getButtonClickListener() {
+ return mOnClick;
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED)
+ .setStrings(mMetricsCategory)
+ .write();
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
index 1c763071..8b9bfb32 100644
--- a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
@@ -16,38 +16,27 @@
package com.android.intentresolver.chooser;
-import android.service.chooser.ChooserTarget;
-import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.Arrays;
/**
* A TargetInfo for Direct Share. Includes a {@link ChooserTarget} representing the
* Direct Share deep link into an application.
*/
-public interface ChooserTargetInfo extends TargetInfo {
- float getModifiedScore();
+public abstract class ChooserTargetInfo implements TargetInfo {
- ChooserTarget getChooserTarget();
-
- /**
- * Do not label as 'equals', since this doesn't quite work
- * as intended with java 8.
- */
- default boolean isSimilar(ChooserTargetInfo other) {
- if (other == null) return false;
-
- ChooserTarget ct1 = getChooserTarget();
- ChooserTarget ct2 = other.getChooserTarget();
-
- // If either is null, there is not enough info to make an informed decision
- // about equality, so just exit
- if (ct1 == null || ct2 == null) return false;
+ @Override
+ public final boolean isChooserTargetInfo() {
+ return true;
+ }
- if (ct1.getComponentName().equals(ct2.getComponentName())
- && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
- && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) {
- return true;
+ @Override
+ public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
+ // TODO: consider making this the default behavior for all `TargetInfo` implementations
+ // (if it's reasonable for `DisplayResolveInfo.getDisplayResolveInfo()` to return `this`).
+ if (getDisplayResolveInfo() == null) {
+ return new ArrayList<>();
}
-
- return false;
+ return new ArrayList<>(Arrays.asList(getDisplayResolveInfo()));
}
}
diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
index e7ffe3c6..db5ae0b4 100644
--- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -20,84 +20,122 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.os.UserHandle;
import com.android.intentresolver.ResolverActivity;
-import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
+import com.android.intentresolver.TargetPresentationGetter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
* A TargetInfo plus additional information needed to render it (such as icon and label) and
* resolve it to an activity.
*/
-public class DisplayResolveInfo implements TargetInfo, Parcelable {
+public class DisplayResolveInfo implements TargetInfo {
private final ResolveInfo mResolveInfo;
private CharSequence mDisplayLabel;
- private Drawable mDisplayIcon;
private CharSequence mExtendedInfo;
private final Intent mResolvedIntent;
private final List<Intent> mSourceIntents = new ArrayList<>();
- private boolean mIsSuspended;
- private ResolveInfoPresentationGetter mResolveInfoPresentationGetter;
+ private final boolean mIsSuspended;
+ private TargetPresentationGetter mPresentationGetter;
private boolean mPinned = false;
-
- public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent,
- ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
- this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent,
- resolveInfoPresentationGetter);
- }
-
- public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel,
- CharSequence pInfo, @NonNull Intent resolvedIntent,
- @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ private final IconHolder mDisplayIconHolder = new SettableIconHolder();
+
+ /** Create a new {@code DisplayResolveInfo} instance. */
+ public static DisplayResolveInfo newDisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo resolveInfo,
+ @NonNull Intent resolvedIntent,
+ @Nullable TargetPresentationGetter presentationGetter) {
+ return newDisplayResolveInfo(
+ originalIntent,
+ resolveInfo,
+ /* displayLabel=*/ null,
+ /* extendedInfo=*/ null,
+ resolvedIntent,
+ presentationGetter);
+ }
+
+ /** Create a new {@code DisplayResolveInfo} instance. */
+ public static DisplayResolveInfo newDisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo resolveInfo,
+ CharSequence displayLabel,
+ CharSequence extendedInfo,
+ @NonNull Intent resolvedIntent,
+ @Nullable TargetPresentationGetter presentationGetter) {
+ return new DisplayResolveInfo(
+ originalIntent,
+ resolveInfo,
+ displayLabel,
+ extendedInfo,
+ resolvedIntent,
+ presentationGetter);
+ }
+
+ private DisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo resolveInfo,
+ CharSequence displayLabel,
+ CharSequence extendedInfo,
+ @NonNull Intent resolvedIntent,
+ @Nullable TargetPresentationGetter presentationGetter) {
mSourceIntents.add(originalIntent);
- mResolveInfo = pri;
- mDisplayLabel = pLabel;
- mExtendedInfo = pInfo;
- mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+ mResolveInfo = resolveInfo;
+ mDisplayLabel = displayLabel;
+ mExtendedInfo = extendedInfo;
+ mPresentationGetter = presentationGetter;
+
+ final ActivityInfo ai = mResolveInfo.activityInfo;
+ mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
final Intent intent = new Intent(resolvedIntent);
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
| Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
- final ActivityInfo ai = mResolveInfo.activityInfo;
intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
-
- mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
-
mResolvedIntent = intent;
}
- private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags,
- ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ private DisplayResolveInfo(
+ DisplayResolveInfo other,
+ Intent fillInIntent,
+ int flags,
+ TargetPresentationGetter presentationGetter) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
+ mIsSuspended = other.mIsSuspended;
mDisplayLabel = other.mDisplayLabel;
- mDisplayIcon = other.mDisplayIcon;
mExtendedInfo = other.mExtendedInfo;
mResolvedIntent = new Intent(other.mResolvedIntent);
mResolvedIntent.fillIn(fillInIntent, flags);
- mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+ mPresentationGetter = presentationGetter;
+
+ mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
- DisplayResolveInfo(DisplayResolveInfo other) {
+ protected DisplayResolveInfo(DisplayResolveInfo other) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
+ mIsSuspended = other.mIsSuspended;
mDisplayLabel = other.mDisplayLabel;
- mDisplayIcon = other.mDisplayIcon;
mExtendedInfo = other.mExtendedInfo;
mResolvedIntent = other.mResolvedIntent;
- mResolveInfoPresentationGetter = other.mResolveInfoPresentationGetter;
+ mPresentationGetter = other.mPresentationGetter;
+
+ mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
+ }
+
+ @Override
+ public final boolean isDisplayResolveInfo() {
+ return true;
}
public ResolveInfo getResolveInfo() {
@@ -105,9 +143,9 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
}
public CharSequence getDisplayLabel() {
- if (mDisplayLabel == null && mResolveInfoPresentationGetter != null) {
- mDisplayLabel = mResolveInfoPresentationGetter.getLabel();
- mExtendedInfo = mResolveInfoPresentationGetter.getSubLabel();
+ if (mDisplayLabel == null && mPresentationGetter != null) {
+ mDisplayLabel = mPresentationGetter.getLabel();
+ mExtendedInfo = mPresentationGetter.getSubLabel();
}
return mDisplayLabel;
}
@@ -124,13 +162,14 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
mExtendedInfo = extendedInfo;
}
- public Drawable getDisplayIcon(Context context) {
- return mDisplayIcon;
+ @Override
+ public IconHolder getDisplayIconHolder() {
+ return mDisplayIconHolder;
}
@Override
public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return new DisplayResolveInfo(this, fillInIntent, flags, mResolveInfoPresentationGetter);
+ return new DisplayResolveInfo(this, fillInIntent, flags, mPresentationGetter);
}
@Override
@@ -138,16 +177,13 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
return mSourceIntents;
}
- public void addAlternateSourceIntent(Intent alt) {
- mSourceIntents.add(alt);
- }
-
- public void setDisplayIcon(Drawable icon) {
- mDisplayIcon = icon;
+ @Override
+ public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
+ return new ArrayList<>(Arrays.asList(this));
}
- public boolean hasDisplayIcon() {
- return mDisplayIcon != null;
+ public void addAlternateSourceIntent(Intent alt) {
+ mSourceIntents.add(alt);
}
public CharSequence getExtendedInfo() {
@@ -172,14 +208,14 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
@Override
public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
- prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
+ TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
activity.startActivityAsCaller(mResolvedIntent, options, false, userId);
return true;
}
@Override
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
- prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
+ TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
activity.startActivityAsUser(mResolvedIntent, options, user);
return false;
}
@@ -196,48 +232,4 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
public void setPinned(boolean pinned) {
mPinned = pinned;
}
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeCharSequence(mDisplayLabel);
- dest.writeCharSequence(mExtendedInfo);
- dest.writeParcelable(mResolvedIntent, 0);
- dest.writeTypedList(mSourceIntents);
- dest.writeBoolean(mIsSuspended);
- dest.writeBoolean(mPinned);
- dest.writeParcelable(mResolveInfo, 0);
- }
-
- public static final Parcelable.Creator<DisplayResolveInfo> CREATOR =
- new Parcelable.Creator<DisplayResolveInfo>() {
- public DisplayResolveInfo createFromParcel(Parcel in) {
- return new DisplayResolveInfo(in);
- }
-
- public DisplayResolveInfo[] newArray(int size) {
- return new DisplayResolveInfo[size];
- }
- };
-
- private static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) {
- final int currentUserId = UserHandle.myUserId();
- if (targetUserId != currentUserId) {
- intent.fixUris(currentUserId);
- }
- }
-
- private DisplayResolveInfo(Parcel in) {
- mDisplayLabel = in.readCharSequence();
- mExtendedInfo = in.readCharSequence();
- mResolvedIntent = in.readParcelable(null /* ClassLoader */, android.content.Intent.class);
- in.readTypedList(mSourceIntents, Intent.CREATOR);
- mIsSuspended = in.readBoolean();
- mPinned = in.readBoolean();
- mResolveInfo = in.readParcelable(null /* ClassLoader */, android.content.pm.ResolveInfo.class);
- }
}
diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
index 5133d997..29f00a35 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.UserHandle;
import com.android.intentresolver.ResolverActivity;
import java.util.ArrayList;
+import java.util.List;
/**
* Represents a "stack" of chooser targets for various activities within the same component.
@@ -30,18 +31,31 @@ import java.util.ArrayList;
public class MultiDisplayResolveInfo extends DisplayResolveInfo {
ArrayList<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
- // We'll use this DRI for basic presentation info - eg icon, name.
- final DisplayResolveInfo mBaseInfo;
+
// Index of selected target
private int mSelected = -1;
/**
- * @param firstInfo A representative DRI to use for the main icon, title, etc for this Info.
+ * @param targetInfos A list of targets in this stack. The first item is treated as the
+ * "representative" that provides the main icon, title, etc.
*/
- public MultiDisplayResolveInfo(String packageName, DisplayResolveInfo firstInfo) {
- super(firstInfo);
- mBaseInfo = firstInfo;
- mTargetInfos.add(firstInfo);
+ public static MultiDisplayResolveInfo newMultiDisplayResolveInfo(
+ List<DisplayResolveInfo> targetInfos) {
+ return new MultiDisplayResolveInfo(targetInfos);
+ }
+
+ /**
+ * @param targetInfos A list of targets in this stack. The first item is treated as the
+ * "representative" that provides the main icon, title, etc.
+ */
+ private MultiDisplayResolveInfo(List<DisplayResolveInfo> targetInfos) {
+ super(targetInfos.get(0));
+ mTargetInfos = new ArrayList<>(targetInfos);
+ }
+
+ @Override
+ public final boolean isMultiDisplayResolveInfo() {
+ return true;
}
@Override
@@ -51,16 +65,12 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
}
/**
- * Add another DisplayResolveInfo to the list included for this target.
+ * List of all {@link DisplayResolveInfo}s included in this target.
+ * TODO: provide as a generic {@code List<DisplayResolveInfo>} once {@link ChooserActivity}
+ * stops requiring the signature to match that of the other "lists" it builds up.
*/
- public void addTarget(DisplayResolveInfo target) {
- mTargetInfos.add(target);
- }
-
- /**
- * List of all DisplayResolveInfos included in this target.
- */
- public ArrayList<DisplayResolveInfo> getTargets() {
+ @Override
+ public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
return mTargetInfos;
}
@@ -96,5 +106,4 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
return mTargetInfos.get(mSelected).startAsUser(activity, options, user);
}
-
}
diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
index 220870f2..d6333374 100644
--- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
@@ -18,12 +18,15 @@ package com.android.intentresolver.chooser;
import android.app.Activity;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
-import android.service.chooser.ChooserTarget;
+import com.android.intentresolver.R;
import com.android.intentresolver.ResolverActivity;
import java.util.List;
@@ -32,7 +35,55 @@ import java.util.List;
* Distinguish between targets that selectable by the user, vs those that are
* placeholders for the system while information is loading in an async manner.
*/
-public abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
+public abstract class NotSelectableTargetInfo extends ChooserTargetInfo {
+ /** Create a non-selectable {@link TargetInfo} with no content. */
+ public static TargetInfo newEmptyTargetInfo() {
+ return new NotSelectableTargetInfo() {
+ @Override
+ public boolean isEmptyTargetInfo() {
+ return true;
+ }
+ };
+ }
+
+ /**
+ * Create a non-selectable {@link TargetInfo} with placeholder content to be displayed
+ * unless/until it can be replaced by the result of a pending asynchronous load.
+ */
+ public static TargetInfo newPlaceHolderTargetInfo(Context context) {
+ return new NotSelectableTargetInfo() {
+ @Override
+ public boolean isPlaceHolderTargetInfo() {
+ return true;
+ }
+
+ @Override
+ public IconHolder getDisplayIconHolder() {
+ return new IconHolder() {
+ @Override
+ public Drawable getDisplayIcon() {
+ AnimatedVectorDrawable avd = (AnimatedVectorDrawable)
+ context.getDrawable(
+ R.drawable.chooser_direct_share_icon_placeholder);
+ avd.start(); // Start animation after generation.
+ return avd;
+ }
+
+ @Override
+ public void setDisplayIcon(Drawable icon) {}
+ };
+ }
+
+ @Override
+ public boolean hasDisplayIcon() {
+ return true;
+ }
+ };
+ }
+
+ public final boolean isNotSelectableTargetInfo() {
+ return true;
+ }
public Intent getResolvedIntent() {
return null;
@@ -78,10 +129,6 @@ public abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
return -0.1f;
}
- public ChooserTarget getChooserTarget() {
- return null;
- }
-
public boolean isSuspended() {
return false;
}
@@ -89,4 +136,17 @@ public abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
public boolean isPinned() {
return false;
}
+
+ @Override
+ public IconHolder getDisplayIconHolder() {
+ return new IconHolder() {
+ @Override
+ public Drawable getDisplayIcon() {
+ return null;
+ }
+
+ @Override
+ public void setDisplayIcon(Drawable icon) {}
+ };
+ }
}
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index 1610d0fd..3ab50175 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -18,31 +18,23 @@ package com.android.intentresolver.chooser;
import android.annotation.Nullable;
import android.app.Activity;
+import android.app.prediction.AppTarget;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.UserHandle;
+import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
import android.text.SpannableStringBuilder;
+import android.util.HashedStringCache;
import android.util.Log;
-import com.android.intentresolver.ChooserActivity;
import com.android.intentresolver.ResolverActivity;
-import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
-import com.android.intentresolver.SimpleIconFactory;
-
-import com.android.internal.annotations.GuardedBy;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import java.util.ArrayList;
import java.util.List;
@@ -51,237 +43,312 @@ import java.util.List;
* Live target, currently selectable by the user.
* @see NotSelectableTargetInfo
*/
-public final class SelectableTargetInfo implements ChooserTargetInfo {
+public final class SelectableTargetInfo extends ChooserTargetInfo {
private static final String TAG = "SelectableTargetInfo";
- private final Context mContext;
+ private interface TargetHashProvider {
+ HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context);
+ }
+
+ private interface TargetActivityStarter {
+ boolean start(Activity activity, Bundle options);
+ boolean startAsCaller(Activity activity, Bundle options, int userId);
+ boolean startAsUser(Activity activity, Bundle options, UserHandle user);
+ }
+
+ private static final String HASHED_STRING_CACHE_TAG = "ChooserActivity"; // For legacy reasons.
+ private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
+
+ private final int mMaxHashSaltDays = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
+ DEFAULT_SALT_EXPIRATION_DAYS);
+
+ @Nullable
private final DisplayResolveInfo mSourceInfo;
+ @Nullable
private final ResolveInfo mBackupResolveInfo;
- private final ChooserTarget mChooserTarget;
+ private final Intent mResolvedIntent;
private final String mDisplayLabel;
- private final PackageManager mPm;
- private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
- @GuardedBy("this")
- private ShortcutInfo mShortcutInfo;
- private Drawable mBadgeIcon = null;
- private CharSequence mBadgeContentDescription;
- @GuardedBy("this")
- private Drawable mDisplayIcon;
- private final Intent mFillInIntent;
+ @Nullable
+ private final AppTarget mAppTarget;
+ @Nullable
+ private final ShortcutInfo mShortcutInfo;
+
+ private final ComponentName mChooserTargetComponentName;
+ private final CharSequence mChooserTargetUnsanitizedTitle;
+ private final Icon mChooserTargetIcon;
+ private final Bundle mChooserTargetIntentExtras;
private final int mFillInFlags;
private final boolean mIsPinned;
private final float mModifiedScore;
- private boolean mIsSuspended = false;
+ private final boolean mIsSuspended;
+ private final ComponentName mResolvedComponentName;
+ private final Intent mBaseIntentToSend;
+ private final ResolveInfo mResolveInfo;
+ private final List<Intent> mAllSourceIntents;
+ private final IconHolder mDisplayIconHolder = new SettableIconHolder();
+ private final TargetHashProvider mHashProvider;
+ private final TargetActivityStarter mActivityStarter;
+
+ /**
+ * A refinement intent from the caller, if any (see
+ * {@link Intent#EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER})
+ */
+ private final Intent mFillInIntent;
+
+ /**
+ * An intent containing referrer URI (see {@link Activity#getReferrer()} (possibly {@code null})
+ * in its extended data under the key {@link Intent#EXTRA_REFERRER}.
+ */
+ private final Intent mReferrerFillInIntent;
- public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo,
+ /**
+ * Create a new {@link TargetInfo} instance representing a selectable target. Some target
+ * parameters are copied over from the (deprecated) legacy {@link ChooserTarget} structure.
+ *
+ * @deprecated Use the overload that doesn't call for a {@link ChooserTarget}.
+ */
+ @Deprecated
+ public static TargetInfo newSelectableTargetInfo(
+ @Nullable DisplayResolveInfo sourceInfo,
+ @Nullable ResolveInfo backupResolveInfo,
+ Intent resolvedIntent,
ChooserTarget chooserTarget,
- float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator,
- @Nullable ShortcutInfo shortcutInfo) {
- mContext = context;
+ float modifiedScore,
+ @Nullable ShortcutInfo shortcutInfo,
+ @Nullable AppTarget appTarget,
+ Intent referrerFillInIntent) {
+ return newSelectableTargetInfo(
+ sourceInfo,
+ backupResolveInfo,
+ resolvedIntent,
+ chooserTarget.getComponentName(),
+ chooserTarget.getTitle(),
+ chooserTarget.getIcon(),
+ chooserTarget.getIntentExtras(),
+ modifiedScore,
+ shortcutInfo,
+ appTarget,
+ referrerFillInIntent);
+ }
+
+ /**
+ * Create a new {@link TargetInfo} instance representing a selectable target. `chooserTarget*`
+ * parameters were historically retrieved from (now-deprecated) {@link ChooserTarget} structures
+ * even when the {@link TargetInfo} was a system (internal) synthesized target that never needed
+ * to be represented as a {@link ChooserTarget}. The values passed here are copied in directly
+ * as if they had been provided in the legacy representation.
+ *
+ * TODO: clarify semantics of how clients use the `getChooserTarget*()` methods; refactor/rename
+ * to avoid making reference to the legacy type; and reflect the improved semantics in the
+ * signature (and documentation) of this method.
+ */
+ public static TargetInfo newSelectableTargetInfo(
+ @Nullable DisplayResolveInfo sourceInfo,
+ @Nullable ResolveInfo backupResolveInfo,
+ Intent resolvedIntent,
+ ComponentName chooserTargetComponentName,
+ CharSequence chooserTargetUnsanitizedTitle,
+ Icon chooserTargetIcon,
+ @Nullable Bundle chooserTargetIntentExtras,
+ float modifiedScore,
+ @Nullable ShortcutInfo shortcutInfo,
+ @Nullable AppTarget appTarget,
+ Intent referrerFillInIntent) {
+ return new SelectableTargetInfo(
+ sourceInfo,
+ backupResolveInfo,
+ resolvedIntent,
+ chooserTargetComponentName,
+ chooserTargetUnsanitizedTitle,
+ chooserTargetIcon,
+ chooserTargetIntentExtras,
+ modifiedScore,
+ shortcutInfo,
+ appTarget,
+ referrerFillInIntent,
+ /* fillInIntent = */ null,
+ /* fillInFlags = */ 0);
+ }
+
+ private SelectableTargetInfo(
+ @Nullable DisplayResolveInfo sourceInfo,
+ @Nullable ResolveInfo backupResolveInfo,
+ Intent resolvedIntent,
+ ComponentName chooserTargetComponentName,
+ CharSequence chooserTargetUnsanitizedTitle,
+ Icon chooserTargetIcon,
+ Bundle chooserTargetIntentExtras,
+ float modifiedScore,
+ @Nullable ShortcutInfo shortcutInfo,
+ @Nullable AppTarget appTarget,
+ Intent referrerFillInIntent,
+ @Nullable Intent fillInIntent,
+ int fillInFlags) {
mSourceInfo = sourceInfo;
- mChooserTarget = chooserTarget;
+ mBackupResolveInfo = backupResolveInfo;
+ mResolvedIntent = resolvedIntent;
mModifiedScore = modifiedScore;
- mPm = mContext.getPackageManager();
- mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
mShortcutInfo = shortcutInfo;
- mIsPinned = shortcutInfo != null && shortcutInfo.isPinned();
- if (sourceInfo != null) {
- final ResolveInfo ri = sourceInfo.getResolveInfo();
- if (ri != null) {
- final ActivityInfo ai = ri.activityInfo;
- if (ai != null && ai.applicationInfo != null) {
- final PackageManager pm = mContext.getPackageManager();
- mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo);
- mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo);
- mIsSuspended =
- (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
- }
+ mAppTarget = appTarget;
+ mReferrerFillInIntent = referrerFillInIntent;
+ mFillInIntent = fillInIntent;
+ mFillInFlags = fillInFlags;
+ mChooserTargetComponentName = chooserTargetComponentName;
+ mChooserTargetUnsanitizedTitle = chooserTargetUnsanitizedTitle;
+ mChooserTargetIcon = chooserTargetIcon;
+ mChooserTargetIntentExtras = chooserTargetIntentExtras;
+
+ mIsPinned = (shortcutInfo != null) && shortcutInfo.isPinned();
+ mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle);
+ mIsSuspended = (mSourceInfo != null) && mSourceInfo.isSuspended();
+ mResolveInfo = (mSourceInfo != null) ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
+
+ mResolvedComponentName = getResolvedComponentName(mSourceInfo, mBackupResolveInfo);
+
+ mAllSourceIntents = getAllSourceIntents(sourceInfo);
+
+ mBaseIntentToSend = getBaseIntentToSend(
+ mResolvedIntent,
+ mFillInIntent,
+ mFillInFlags,
+ mReferrerFillInIntent);
+
+ mHashProvider = context -> {
+ final String plaintext =
+ getChooserTargetComponentName().getPackageName()
+ + mChooserTargetUnsanitizedTitle;
+ return HashedStringCache.getInstance().hashString(
+ context,
+ HASHED_STRING_CACHE_TAG,
+ plaintext,
+ mMaxHashSaltDays);
+ };
+
+ mActivityStarter = new TargetActivityStarter() {
+ @Override
+ public boolean start(Activity activity, Bundle options) {
+ throw new RuntimeException("ChooserTargets should be started as caller.");
}
- }
- if (sourceInfo != null) {
- mBackupResolveInfo = null;
- } else {
- mBackupResolveInfo =
- mContext.getPackageManager().resolveActivity(getResolvedIntent(), 0);
- }
+ @Override
+ public boolean startAsCaller(Activity activity, Bundle options, int userId) {
+ final Intent intent = mBaseIntentToSend;
+ if (intent == null) {
+ return false;
+ }
+ intent.setComponent(getChooserTargetComponentName());
+ intent.putExtras(mChooserTargetIntentExtras);
+ TargetInfo.prepareIntentForCrossProfileLaunch(intent, userId);
+
+ // Important: we will ignore the target security checks in ActivityManager if and
+ // only if the ChooserTarget's target package is the same package where we got the
+ // ChooserTargetService that provided it. This lets a ChooserTargetService provide
+ // a non-exported or permission-guarded target for the user to pick.
+ //
+ // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
+ // so we'll obey the caller's normal security checks.
+ final boolean ignoreTargetSecurity = (mSourceInfo != null)
+ && mSourceInfo.getResolvedComponentName().getPackageName()
+ .equals(getChooserTargetComponentName().getPackageName());
+ activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
+ return true;
+ }
- mFillInIntent = null;
- mFillInFlags = 0;
+ @Override
+ public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ throw new RuntimeException("ChooserTargets should be started as caller.");
+ }
+ };
+ }
- mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle());
+ private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) {
+ this(
+ other.mSourceInfo,
+ other.mBackupResolveInfo,
+ other.mResolvedIntent,
+ other.mChooserTargetComponentName,
+ other.mChooserTargetUnsanitizedTitle,
+ other.mChooserTargetIcon,
+ other.mChooserTargetIntentExtras,
+ other.mModifiedScore,
+ other.mShortcutInfo,
+ other.mAppTarget,
+ other.mReferrerFillInIntent,
+ fillInIntent,
+ flags);
}
- private SelectableTargetInfo(SelectableTargetInfo other,
- Intent fillInIntent, int flags) {
- mContext = other.mContext;
- mPm = other.mPm;
- mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator;
- mSourceInfo = other.mSourceInfo;
- mBackupResolveInfo = other.mBackupResolveInfo;
- mChooserTarget = other.mChooserTarget;
- mBadgeIcon = other.mBadgeIcon;
- mBadgeContentDescription = other.mBadgeContentDescription;
- synchronized (other) {
- mShortcutInfo = other.mShortcutInfo;
- mDisplayIcon = other.mDisplayIcon;
- }
- mFillInIntent = fillInIntent;
- mFillInFlags = flags;
- mModifiedScore = other.mModifiedScore;
- mIsPinned = other.mIsPinned;
+ @Override
+ public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+ return new SelectableTargetInfo(this, fillInIntent, flags);
+ }
- mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle());
+ @Override
+ public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
+ return mHashProvider.getHashedTargetIdForMetrics(context);
}
- private String sanitizeDisplayLabel(CharSequence label) {
- SpannableStringBuilder sb = new SpannableStringBuilder(label);
- sb.clearSpans();
- return sb.toString();
+ @Override
+ public boolean isSelectableTargetInfo() {
+ return true;
}
+ @Override
public boolean isSuspended() {
return mIsSuspended;
}
+ @Override
+ @Nullable
public DisplayResolveInfo getDisplayResolveInfo() {
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,
- @Nullable ShortcutInfo shortcutInfo) {
- Drawable directShareIcon = null;
-
- // First get the target drawable and associated activity info
- final Icon icon = target.getIcon();
- if (icon != null) {
- directShareIcon = icon.loadDrawable(mContext);
- } else if (shortcutInfo != null) {
- LauncherApps launcherApps = (LauncherApps) mContext.getSystemService(
- Context.LAUNCHER_APPS_SERVICE);
- directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0);
- }
-
- if (directShareIcon == null) return null;
-
- ActivityInfo info = null;
- try {
- info = mPm.getActivityInfo(target.getComponentName(), 0);
- } catch (PackageManager.NameNotFoundException error) {
- Log.e(TAG, "Could not find activity associated with ChooserTarget");
- }
-
- if (info == null) return null;
-
- // Now fetch app icon and raster with no badging even in work profile
- Bitmap appIcon = mSelectableTargetInfoCommunicator.makePresentationGetter(info)
- .getIconBitmap(null);
-
- // Raster target drawable with appIcon as a badge
- SimpleIconFactory sif = SimpleIconFactory.obtain(mContext);
- Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
- sif.recycle();
-
- return new BitmapDrawable(mContext.getResources(), directShareBadgedIcon);
- }
-
+ @Override
public float getModifiedScore() {
return mModifiedScore;
}
@Override
public Intent getResolvedIntent() {
- if (mSourceInfo != null) {
- return mSourceInfo.getResolvedIntent();
- }
-
- final Intent targetIntent = new Intent(mSelectableTargetInfoCommunicator.getTargetIntent());
- targetIntent.setComponent(mChooserTarget.getComponentName());
- targetIntent.putExtras(mChooserTarget.getIntentExtras());
- return targetIntent;
+ return mResolvedIntent;
}
@Override
public ComponentName getResolvedComponentName() {
- if (mSourceInfo != null) {
- return mSourceInfo.getResolvedComponentName();
- } else if (mBackupResolveInfo != null) {
- return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
- mBackupResolveInfo.activityInfo.name);
- }
- return null;
+ return mResolvedComponentName;
}
- private Intent getBaseIntentToSend() {
- Intent result = getResolvedIntent();
- if (result == null) {
- Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
- } else {
- result = new Intent(result);
- if (mFillInIntent != null) {
- result.fillIn(mFillInIntent, mFillInFlags);
- }
- result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0);
- }
- return result;
+ @Override
+ public ComponentName getChooserTargetComponentName() {
+ return mChooserTargetComponentName;
+ }
+
+ @Nullable
+ public Icon getChooserTargetIcon() {
+ return mChooserTargetIcon;
}
@Override
public boolean start(Activity activity, Bundle options) {
- throw new RuntimeException("ChooserTargets should be started as caller.");
+ return mActivityStarter.start(activity, options);
}
@Override
public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
- final Intent intent = getBaseIntentToSend();
- if (intent == null) {
- return false;
- }
- intent.setComponent(mChooserTarget.getComponentName());
- intent.putExtras(mChooserTarget.getIntentExtras());
-
- // Important: we will ignore the target security checks in ActivityManager
- // if and only if the ChooserTarget's target package is the same package
- // where we got the ChooserTargetService that provided it. This lets a
- // ChooserTargetService provide a non-exported or permission-guarded target
- // to the chooser for the user to pick.
- //
- // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
- // so we'll obey the caller's normal security checks.
- final boolean ignoreTargetSecurity = mSourceInfo != null
- && mSourceInfo.getResolvedComponentName().getPackageName()
- .equals(mChooserTarget.getComponentName().getPackageName());
- activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
- return true;
+ return mActivityStarter.startAsCaller(activity, options, userId);
}
@Override
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
- throw new RuntimeException("ChooserTargets should be started as caller.");
+ return mActivityStarter.startAsUser(activity, options, user);
}
@Override
public ResolveInfo getResolveInfo() {
- return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
+ return mResolveInfo;
}
@Override
@@ -296,27 +363,25 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
}
@Override
- public synchronized Drawable getDisplayIcon(Context context) {
- return mDisplayIcon;
+ public IconHolder getDisplayIconHolder() {
+ return mDisplayIconHolder;
}
- public ChooserTarget getChooserTarget() {
- return mChooserTarget;
+ @Override
+ @Nullable
+ public ShortcutInfo getDirectShareShortcutInfo() {
+ return mShortcutInfo;
}
@Override
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return new SelectableTargetInfo(this, fillInIntent, flags);
+ @Nullable
+ public AppTarget getDirectShareAppTarget() {
+ return mAppTarget;
}
@Override
public List<Intent> getAllSourceIntents() {
- final List<Intent> results = new ArrayList<>();
- if (mSourceInfo != null) {
- // We only queried the service for the first one in our sourceinfo.
- results.add(mSourceInfo.getAllSourceIntents().get(0));
- }
- return results;
+ return mAllSourceIntents;
}
@Override
@@ -324,16 +389,49 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
return mIsPinned;
}
- /**
- * Necessary methods to communicate between {@link SelectableTargetInfo}
- * and {@link ResolverActivity} or {@link ChooserActivity}.
- */
- public interface SelectableTargetInfoCommunicator {
+ private static String sanitizeDisplayLabel(CharSequence label) {
+ SpannableStringBuilder sb = new SpannableStringBuilder(label);
+ sb.clearSpans();
+ return sb.toString();
+ }
- ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info);
+ private static List<Intent> getAllSourceIntents(@Nullable DisplayResolveInfo sourceInfo) {
+ final List<Intent> results = new ArrayList<>();
+ if (sourceInfo != null) {
+ // We only queried the service for the first one in our sourceinfo.
+ results.add(sourceInfo.getAllSourceIntents().get(0));
+ }
+ return results;
+ }
- Intent getTargetIntent();
+ private static ComponentName getResolvedComponentName(
+ @Nullable DisplayResolveInfo sourceInfo, ResolveInfo backupResolveInfo) {
+ if (sourceInfo != null) {
+ return sourceInfo.getResolvedComponentName();
+ } else if (backupResolveInfo != null) {
+ return new ComponentName(
+ backupResolveInfo.activityInfo.packageName,
+ backupResolveInfo.activityInfo.name);
+ }
+ return null;
+ }
- Intent getReferrerFillInIntent();
+ @Nullable
+ private static Intent getBaseIntentToSend(
+ @Nullable Intent resolvedIntent,
+ Intent fillInIntent,
+ int fillInFlags,
+ Intent referrerFillInIntent) {
+ Intent result = resolvedIntent;
+ if (result == null) {
+ Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
+ } else {
+ result = new Intent(result);
+ if (fillInIntent != null) {
+ result.fillIn(fillInIntent, fillInFlags);
+ }
+ result.fillIn(referrerFillInIntent, 0);
+ }
+ return result;
}
}
diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java
index fabb26c2..72dd1b0b 100644
--- a/java/src/com/android/intentresolver/chooser/TargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -17,23 +17,67 @@
package com.android.intentresolver.chooser;
+import android.annotation.Nullable;
import android.app.Activity;
+import android.app.prediction.AppTarget;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+import android.util.HashedStringCache;
import com.android.intentresolver.ResolverActivity;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
/**
* A single target as represented in the chooser.
*/
public interface TargetInfo {
+
+ /**
+ * Container for a {@link TargetInfo}'s (potentially) mutable icon state. This is provided to
+ * encapsulate the state so that the {@link TargetInfo} itself can be "immutable" (in some
+ * sense) as long as it always returns the same {@link IconHolder} instance.
+ *
+ * TODO: move "stateful" responsibilities out to clients; for more info see the Javadoc comment
+ * on {@link #getDisplayIconHolder()}.
+ */
+ interface IconHolder {
+ /** @return the icon (if it's already loaded, or statically available), or null. */
+ @Nullable
+ Drawable getDisplayIcon();
+
+ /**
+ * @param icon the icon to return on subsequent calls to {@link #getDisplayIcon()}.
+ * Implementations may discard this request as a no-op if they don't support setting.
+ */
+ void setDisplayIcon(Drawable icon);
+ }
+
+ /** A simple mutable-container implementation of {@link IconHolder}. */
+ final class SettableIconHolder implements IconHolder {
+ @Nullable
+ private Drawable mDisplayIcon;
+
+ @Nullable
+ public Drawable getDisplayIcon() {
+ return mDisplayIcon;
+ }
+
+ public void setDisplayIcon(Drawable icon) {
+ mDisplayIcon = icon;
+ }
+ }
+
/**
* Get the resolved intent that represents this target. Note that this may not be the
* intent that will be launched by calling one of the <code>start</code> methods provided;
@@ -46,13 +90,34 @@ public interface TargetInfo {
/**
* Get the resolved component name that represents this target. Note that this may not
* be the component that will be directly launched by calling one of the <code>start</code>
- * methods provided; this is the component that will be credited with the launch.
+ * methods provided; this is the component that will be credited with the launch. This may be
+ * null if the target was specified by a caller-provided {@link ChooserTarget} that we failed to
+ * resolve to a component on the system.
*
* @return the resolved ComponentName for this target
*/
+ @Nullable
ComponentName getResolvedComponentName();
/**
+ * If this target was historically built from a (now-deprecated) {@link ChooserTarget} record,
+ * get the {@link ComponentName} that would've been provided by that record.
+ *
+ * TODO: for (historical) {@link ChooserTargetInfo} targets, this differs from the result of
+ * {@link #getResolvedComponentName()} only for caller-provided targets that we fail to resolve;
+ * then this returns the name of the component that was requested, and the other returns null.
+ * At the time of writing, this method is only called in contexts where the client knows that
+ * the target was a historical {@link ChooserTargetInfo}. Thus this method could be removed and
+ * all clients consolidated on the other, if we have some alternate mechanism of tracking this
+ * discrepancy; or if we know that the distinction won't apply in the conditions when we call
+ * this method; or if we determine that tracking the distinction isn't a requirement for us.
+ */
+ @Nullable
+ default ComponentName getChooserTargetComponentName() {
+ return null;
+ }
+
+ /**
* Start the activity referenced by this target.
*
* @param activity calling Activity performing the launch
@@ -106,12 +171,23 @@ public interface TargetInfo {
CharSequence getExtendedInfo();
/**
- * @return The drawable that should be used to represent this target including badge
- * @param context
+ * @return the {@link IconHolder} for the icon used to represent this target, including badge.
+ *
+ * TODO: while the {@link TargetInfo} may be immutable in always returning the same instance of
+ * {@link IconHolder} here, the holder itself is mutable state, and could become a problem if we
+ * ever rely on {@link TargetInfo} immutability elsewhere. Ideally, the {@link TargetInfo}
+ * should provide an immutable "spec" that tells clients <em>how</em> to load the appropriate
+ * icon, while leaving the load itself to some external component.
*/
- Drawable getDisplayIcon(Context context);
+ IconHolder getDisplayIconHolder();
/**
+ * @return true if display icon is available.
+ */
+ default boolean hasDisplayIcon() {
+ return getDisplayIconHolder().getDisplayIcon() != null;
+ }
+ /**
* Clone this target with the given fill-in information.
*/
TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
@@ -122,6 +198,28 @@ public interface TargetInfo {
List<Intent> getAllSourceIntents();
/**
+ * @return the one or more {@link DisplayResolveInfo}s that this target represents in the UI.
+ *
+ * TODO: clarify the semantics of the {@link DisplayResolveInfo} branch of {@link TargetInfo}'s
+ * class hierarchy. Why is it that {@link MultiDisplayResolveInfo} can stand in for some
+ * "virtual" {@link DisplayResolveInfo} targets that aren't individually represented in the UI,
+ * but OTOH a {@link ChooserTargetInfo} (which doesn't inherit from {@link DisplayResolveInfo})
+ * can't provide its own UI treatment, and instead needs us to reach into its composed-in
+ * info via {@link #getDisplayResolveInfo()}? It seems like {@link DisplayResolveInfo} may be
+ * required to populate views in our UI, while {@link ChooserTargetInfo} may carry some other
+ * metadata. For non-{@link ChooserTargetInfo} targets (e.g. in {@link ResolverActivity}) the
+ * "naked" {@link DisplayResolveInfo} might also be taken to provide some of this metadata, but
+ * this presents a denormalization hazard since the "UI info" ({@link DisplayResolveInfo}) that
+ * represents a {@link ChooserTargetInfo} might provide different values than its enclosing
+ * {@link ChooserTargetInfo} (as they both implement {@link TargetInfo}). We could try to
+ * address this by splitting {@link DisplayResolveInfo} into two types; one (which implements
+ * the same {@link TargetInfo} interface as {@link ChooserTargetInfo}) provides the previously-
+ * implicit "metadata", and the other provides only the UI treatment for a target of any type
+ * (taking over the respective methods that previously belonged to {@link TargetInfo}).
+ */
+ ArrayList<DisplayResolveInfo> getAllDisplayTargets();
+
+ /**
* @return true if this target cannot be selected by the user
*/
boolean isSuspended();
@@ -130,4 +228,220 @@ public interface TargetInfo {
* @return true if this target should be pinned to the front by the request of the user
*/
boolean isPinned();
+
+ /**
+ * Determine whether two targets represent "similar" content that could be de-duped.
+ * Note an earlier version of this code cautioned maintainers,
+ * "do not label as 'equals', since this doesn't quite work as intended with java 8."
+ * This seems to refer to the rule that interfaces can't provide defaults that conflict with the
+ * definitions of "real" methods in {@code java.lang.Object}, and (if desired) it could be
+ * presumably resolved by converting {@code TargetInfo} from an interface to an abstract class.
+ */
+ default boolean isSimilar(TargetInfo other) {
+ if (other == null) {
+ return false;
+ }
+
+ // TODO: audit usage and try to reconcile a behavior that doesn't depend on the legacy
+ // subclass type. Note that the `isSimilar()` method was pulled up from the legacy
+ // `ChooserTargetInfo`, so no legacy behavior currently depends on calling `isSimilar()` on
+ // an instance where `isChooserTargetInfo()` would return false (although technically it may
+ // have been possible for the `other` target to be of a different type). Thus we have
+ // flexibility in defining the similarity conditions between pairs of non "chooser" targets.
+ if (isChooserTargetInfo()) {
+ return other.isChooserTargetInfo()
+ && Objects.equals(
+ getChooserTargetComponentName(), other.getChooserTargetComponentName())
+ && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
+ && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo());
+ } else {
+ return !other.isChooserTargetInfo() && Objects.equals(this, other);
+ }
+ }
+
+ /**
+ * @return the target score, including any Chooser-specific modifications that may have been
+ * applied (either overriding by special-case for "non-selectable" targets, or by twiddling the
+ * scores of "selectable" targets in {@link ChooserListAdapter}). Higher scores are "better."
+ * Targets that aren't intended for ranking/scoring should return a negative value.
+ */
+ default float getModifiedScore() {
+ return -0.1f;
+ }
+
+ /**
+ * @return the {@link ShortcutManager} data for any shortcut associated with this target.
+ */
+ @Nullable
+ default ShortcutInfo getDirectShareShortcutInfo() {
+ return null;
+ }
+
+ /**
+ * @return the ID of the shortcut represented by this target, or null if the target didn't come
+ * from a {@link ShortcutManager} shortcut.
+ */
+ @Nullable
+ default String getDirectShareShortcutId() {
+ ShortcutInfo shortcut = getDirectShareShortcutInfo();
+ if (shortcut == null) {
+ return null;
+ }
+ return shortcut.getId();
+ }
+
+ /**
+ * @return the {@link AppTarget} metadata if this target was sourced from App Prediction
+ * service, or null otherwise.
+ */
+ @Nullable
+ default AppTarget getDirectShareAppTarget() {
+ return null;
+ }
+
+ /**
+ * Get more info about this target in the form of a {@link DisplayResolveInfo}, if available.
+ * TODO: this seems to return non-null only for ChooserTargetInfo subclasses. Determine the
+ * meaning of a TargetInfo (ChooserTargetInfo) embedding another kind of TargetInfo
+ * (DisplayResolveInfo) in this way, and - at least - improve this documentation; OTOH this
+ * probably indicates an opportunity to simplify or better separate these APIs. (For example,
+ * targets that <em>don't</em> descend from ChooserTargetInfo instead descend directly from
+ * DisplayResolveInfo; should they return `this`? Do we always use DisplayResolveInfo to
+ * represent visual properties, and then either assume some implicit metadata properties *or*
+ * embed that visual representation within a ChooserTargetInfo to carry additional metadata? If
+ * that's the case, maybe we could decouple by saying that all TargetInfos compose-in their
+ * visual representation [as a DisplayResolveInfo, now the root of its own class hierarchy] and
+ * then add a new TargetInfo type that explicitly represents the "implicit metadata" that we
+ * previously assumed for "naked DisplayResolveInfo targets" that weren't wrapped as
+ * ChooserTargetInfos. Or does all this complexity disappear once we stop relying on the
+ * deprecated ChooserTarget type?)
+ */
+ @Nullable
+ default DisplayResolveInfo getDisplayResolveInfo() {
+ return null;
+ }
+
+ /**
+ * @return true if this target represents a legacy {@code ChooserTargetInfo}. These objects were
+ * historically documented as representing "[a] TargetInfo for Direct Share." However, not all
+ * of these targets are actually *valid* for direct share; e.g. some represent "empty" items
+ * (although perhaps only for display in the Direct Share UI?). In even earlier versions, these
+ * targets may also have been results from peers in the (now-deprecated/unsupported)
+ * {@code ChooserTargetService} ecosystem; even though we no longer use these services, we're
+ * still shoehorning other target data into the deprecated {@link ChooserTarget} structure for
+ * compatibility with some internal APIs.
+ * TODO: refactor to clarify the semantics of any target for which this method returns true
+ * (e.g., are they characterized by their application in the Direct Share UI?), and to remove
+ * the scaffolding that adapts to and from the {@link ChooserTarget} structure. Eventually, we
+ * expect to remove this method (and others that strictly indicate legacy subclass roles) in
+ * favor of a more semantic design that expresses the purpose and distinctions in those roles.
+ */
+ default boolean isChooserTargetInfo() {
+ return false;
+ }
+
+ /**
+ * @return true if this target represents a legacy {@code DisplayResolveInfo}. These objects
+ * were historically documented as an augmented "TargetInfo plus additional information needed
+ * to render it (such as icon and label) and resolve it to an activity." That description in no
+ * way distinguishes from the base {@code TargetInfo} API. At the time of writing, these objects
+ * are most-clearly defined by their opposite; this returns true for exactly those instances of
+ * {@code TargetInfo} where {@link #isChooserTargetInfo()} returns false (these conditions are
+ * complementary because they correspond to the immediate {@code TargetInfo} child types that
+ * historically partitioned all concrete {@code TargetInfo} implementations). These may(?)
+ * represent any target displayed somewhere other than the Direct Share UI.
+ */
+ default boolean isDisplayResolveInfo() {
+ return false;
+ }
+
+ /**
+ * @return true if this target represents a legacy {@code MultiDisplayResolveInfo}. These
+ * objects were historically documented as representing "a 'stack' of chooser targets for
+ * various activities within the same component." For historical reasons this currently can
+ * return true only if {@link #isDisplayResolveInfo()} returns true (because the legacy classes
+ * shared an inheritance relationship), but new code should avoid relying on that relationship
+ * since these APIs are "in transition."
+ */
+ default boolean isMultiDisplayResolveInfo() {
+ return false;
+ }
+
+ /**
+ * @return true if this target represents a legacy {@code SelectableTargetInfo}. Note that this
+ * is defined for legacy compatibility and may not conform to other notions of a "selectable"
+ * target. For historical reasons, this method and {@link #isNotSelectableTargetInfo()} only
+ * partition the {@code TargetInfo} instances for which {@link #isChooserTargetInfo()} returns
+ * true; otherwise <em>both</em> methods return false.
+ * TODO: define selectability for targets not historically from {@code ChooserTargetInfo},
+ * then attempt to replace this with a new method like {@code TargetInfo#isSelectable()} that
+ * actually partitions <em>all</em> target types (after updating client usage as needed).
+ */
+ default boolean isSelectableTargetInfo() {
+ return false;
+ }
+
+ /**
+ * @return true if this target represents a legacy {@code NotSelectableTargetInfo} (i.e., a
+ * target where {@link #isChooserTargetInfo()} is true but {@link #isSelectableTargetInfo()} is
+ * false). For more information on how this divides the space of targets, see the Javadoc for
+ * {@link #isSelectableTargetInfo()}.
+ */
+ default boolean isNotSelectableTargetInfo() {
+ return false;
+ }
+
+ /**
+ * @return true if this target represents a legacy {@code ChooserActivity#EmptyTargetInfo}. Note
+ * that this is defined for legacy compatibility and may not conform to other notions of an
+ * "empty" target.
+ */
+ default boolean isEmptyTargetInfo() {
+ return false;
+ }
+
+ /**
+ * @return true if this target represents a legacy {@code ChooserActivity#PlaceHolderTargetInfo}
+ * (defined only for compatibility with historic use in {@link ChooserListAdapter}). For
+ * historic reasons (owing to a legacy subclass relationship) this can return true only if
+ * {@link #isNotSelectableTargetInfo()} also returns true.
+ */
+ default boolean isPlaceHolderTargetInfo() {
+ return false;
+ }
+
+ /**
+ * @return true if this target should be logged with the "direct_share" metrics category in
+ * {@link ResolverActivity#maybeLogCrossProfileTargetLaunch()}. This is defined for legacy
+ * compatibility and is <em>not</em> likely to be a good indicator of whether this is actually a
+ * "direct share" target (e.g. because it historically also applies to "empty" and "placeholder"
+ * targets).
+ */
+ default boolean isInDirectShareMetricsCategory() {
+ return isChooserTargetInfo();
+ }
+
+ /**
+ * @param context caller's context, to provide the {@link SharedPreferences} for use by the
+ * {@link HashedStringCache}.
+ * @return a hashed ID that should be logged along with our target-selection metrics, or null.
+ * The contents of the plaintext are defined for historical reasons, "the package name + target
+ * name to answer the question if most users share to mostly the same person
+ * or to a bunch of different people." Clients should consider this as opaque data for logging
+ * only; they should not rely on any particular semantics about the value.
+ */
+ default HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
+ return null;
+ }
+
+ /**
+ * Fix the URIs in {@code intent} if cross-profile sharing is required. This should be called
+ * before launching the intent as another user.
+ */
+ static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) {
+ final int currentUserId = UserHandle.myUserId();
+ if (targetUserId != currentUserId) {
+ intent.fixUris(currentUserId);
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
new file mode 100644
index 00000000..1cf59316
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2008 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.grid;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Space;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter.ViewHolder;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.android.collect.Lists;
+
+/**
+ * Adapter for all types of items and targets in ShareSheet.
+ * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
+ * row level by this adapter but not on the item level. Individual targets within the row are
+ * handled by {@link ChooserListAdapter}
+ */
+@VisibleForTesting
+public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ /**
+ * The transition time between placeholders for direct share to a message
+ * indicating that none are available.
+ */
+ public static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200;
+
+ /**
+ * Injectable interface for any considerations that should be delegated to other components
+ * in the {@link ChooserActivity}.
+ * TODO: determine whether any of these methods return parameters that can safely be
+ * precomputed; whether any should be converted to `ChooserGridAdapter` setters to be
+ * invoked by external callbacks; and whether any reflect requirements that should be moved
+ * out of `ChooserGridAdapter` altogether.
+ */
+ public interface ChooserActivityDelegate {
+ /** @return whether we're showing a tabbed (multi-profile) UI. */
+ boolean shouldShowTabs();
+
+ /**
+ * @return a content preview {@link View} that's appropriate for the caller's share
+ * content, constructed for display in the provided {@code parent} group.
+ */
+ View buildContentPreview(ViewGroup parent);
+
+ /** Notify the client that the item with the selected {@code itemIndex} was selected. */
+ void onTargetSelected(int itemIndex);
+
+ /**
+ * Notify the client that the item with the selected {@code itemIndex} was
+ * long-pressed.
+ */
+ void onTargetLongPressed(int itemIndex);
+
+ /**
+ * Notify the client that the provided {@code View} should be configured as the new
+ * "profile view" button. Callers should attach their own click listeners to implement
+ * behaviors on this view.
+ */
+ void updateProfileViewButton(View newButtonFromProfileRow);
+
+ /**
+ * @return the number of "valid" targets in the active list adapter.
+ * TODO: define "valid."
+ */
+ int getValidTargetCount();
+
+ /**
+ * Request that the client update our {@code directShareGroup} to match their desired
+ * state for the "expansion" UI.
+ */
+ void updateDirectShareExpansion(DirectShareViewHolder directShareGroup);
+
+ /**
+ * Request that the client handle a scroll event that should be taken as expanding the
+ * provided {@code directShareGroup}. Note that this currently never happens due to a
+ * hard-coded condition in {@link #canExpandDirectShare()}.
+ */
+ void handleScrollToExpandDirectShare(
+ DirectShareViewHolder directShareGroup, int y, int oldy);
+ }
+
+ private static final int VIEW_TYPE_DIRECT_SHARE = 0;
+ private static final int VIEW_TYPE_NORMAL = 1;
+ private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
+ private static final int VIEW_TYPE_PROFILE = 3;
+ private static final int VIEW_TYPE_AZ_LABEL = 4;
+ private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
+ private static final int VIEW_TYPE_FOOTER = 6;
+
+ private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
+
+ private final ChooserActivityDelegate mChooserActivityDelegate;
+ private final ChooserListAdapter mChooserListAdapter;
+ private final LayoutInflater mLayoutInflater;
+
+ private final int mMaxTargetsPerRow;
+ private final boolean mShouldShowContentPreview;
+ private final int mChooserWidthPixels;
+ private final int mChooserRowTextOptionTranslatePixelSize;
+ private final boolean mShowAzLabelIfPoss;
+
+ private DirectShareViewHolder mDirectShareViewHolder;
+ private int mChooserTargetWidth = 0;
+
+ private int mFooterHeight = 0;
+
+ public ChooserGridAdapter(
+ Context context,
+ ChooserActivityDelegate chooserActivityDelegate,
+ ChooserListAdapter wrappedAdapter,
+ boolean shouldShowContentPreview,
+ int maxTargetsPerRow,
+ int numSheetExpansions) {
+ super();
+
+ mChooserActivityDelegate = chooserActivityDelegate;
+
+ mChooserListAdapter = wrappedAdapter;
+ mLayoutInflater = LayoutInflater.from(context);
+
+ mShouldShowContentPreview = shouldShowContentPreview;
+ mMaxTargetsPerRow = maxTargetsPerRow;
+
+ mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);
+ mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(
+ R.dimen.chooser_row_text_option_translate);
+
+ mShowAzLabelIfPoss = numSheetExpansions < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL;
+
+ wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ notifyDataSetChanged();
+ }
+ });
+ }
+
+ public void setFooterHeight(int height) {
+ mFooterHeight = height;
+ }
+
+ /**
+ * Calculate the chooser target width to maximize space per item
+ *
+ * @param width The new row width to use for recalculation
+ * @return true if the view width has changed
+ */
+ public boolean calculateChooserTargetWidth(int width) {
+ if (width == 0) {
+ return false;
+ }
+
+ // Limit width to the maximum width of the chooser activity
+ int maxWidth = mChooserWidthPixels;
+ width = Math.min(maxWidth, width);
+
+ int newWidth = width / mMaxTargetsPerRow;
+ if (newWidth != mChooserTargetWidth) {
+ mChooserTargetWidth = newWidth;
+ return true;
+ }
+
+ return false;
+ }
+
+ public int getRowCount() {
+ return (int) (
+ getSystemRowCount()
+ + getProfileRowCount()
+ + getServiceTargetRowCount()
+ + getCallerAndRankedTargetRowCount()
+ + getAzLabelRowCount()
+ + Math.ceil(
+ (float) mChooserListAdapter.getAlphaTargetCount()
+ / mMaxTargetsPerRow)
+ );
+ }
+
+ /**
+ * Whether the "system" row of targets is displayed.
+ * This area includes the content preview (if present) and action row.
+ */
+ public int getSystemRowCount() {
+ // For the tabbed case we show the sticky content preview above the tabs,
+ // please refer to shouldShowStickyContentPreview
+ if (mChooserActivityDelegate.shouldShowTabs()) {
+ return 0;
+ }
+
+ if (!mShouldShowContentPreview) {
+ return 0;
+ }
+
+ if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
+ return 0;
+ }
+
+ return 1;
+ }
+
+ public int getProfileRowCount() {
+ if (mChooserActivityDelegate.shouldShowTabs()) {
+ return 0;
+ }
+ return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
+ }
+
+ public int getFooterRowCount() {
+ return 1;
+ }
+
+ public int getCallerAndRankedTargetRowCount() {
+ return (int) Math.ceil(
+ ((float) mChooserListAdapter.getCallerTargetCount()
+ + mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow);
+ }
+
+ // There can be at most one row in the listview, that is internally
+ // a ViewGroup with 2 rows
+ public int getServiceTargetRowCount() {
+ if (mShouldShowContentPreview && !ActivityManager.isLowRamDeviceStatic()) {
+ return 1;
+ }
+ return 0;
+ }
+
+ public int getAzLabelRowCount() {
+ // Only show a label if the a-z list is showing
+ return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0;
+ }
+
+ @Override
+ public int getItemCount() {
+ return (int) (
+ getSystemRowCount()
+ + getProfileRowCount()
+ + getServiceTargetRowCount()
+ + getCallerAndRankedTargetRowCount()
+ + getAzLabelRowCount()
+ + mChooserListAdapter.getAlphaTargetCount()
+ + getFooterRowCount()
+ );
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_CONTENT_PREVIEW:
+ return new ItemViewHolder(
+ mChooserActivityDelegate.buildContentPreview(parent),
+ viewType,
+ null,
+ null);
+ case VIEW_TYPE_PROFILE:
+ return new ItemViewHolder(
+ createProfileView(parent),
+ viewType,
+ null,
+ null);
+ case VIEW_TYPE_AZ_LABEL:
+ return new ItemViewHolder(
+ createAzLabelView(parent),
+ viewType,
+ null,
+ null);
+ case VIEW_TYPE_NORMAL:
+ return new ItemViewHolder(
+ mChooserListAdapter.createView(parent),
+ viewType,
+ mChooserActivityDelegate::onTargetSelected,
+ mChooserActivityDelegate::onTargetLongPressed);
+ case VIEW_TYPE_DIRECT_SHARE:
+ case VIEW_TYPE_CALLER_AND_RANK:
+ return createItemGroupViewHolder(viewType, parent);
+ case VIEW_TYPE_FOOTER:
+ Space sp = new Space(parent.getContext());
+ sp.setLayoutParams(new RecyclerView.LayoutParams(
+ LayoutParams.MATCH_PARENT, mFooterHeight));
+ return new FooterViewHolder(sp, viewType);
+ default:
+ // Since we catch all possible viewTypes above, no chance this is being called.
+ return null;
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ int viewType = ((ViewHolderBase) holder).getViewType();
+ switch (viewType) {
+ case VIEW_TYPE_DIRECT_SHARE:
+ case VIEW_TYPE_CALLER_AND_RANK:
+ bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder);
+ break;
+ case VIEW_TYPE_NORMAL:
+ bindItemViewHolder(position, (ItemViewHolder) holder);
+ break;
+ default:
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ int count;
+
+ int countSum = (count = getSystemRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW;
+
+ countSum += (count = getProfileRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE;
+
+ countSum += (count = getServiceTargetRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
+
+ countSum += (count = getCallerAndRankedTargetRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK;
+
+ countSum += (count = getAzLabelRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL;
+
+ if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER;
+
+ return VIEW_TYPE_NORMAL;
+ }
+
+ public int getTargetType(int position) {
+ return mChooserListAdapter.getPositionTargetType(getListPosition(position));
+ }
+
+ private View createProfileView(ViewGroup parent) {
+ View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false);
+ mChooserActivityDelegate.updateProfileViewButton(profileRow);
+ return profileRow;
+ }
+
+ private View createAzLabelView(ViewGroup parent) {
+ return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
+ }
+
+ private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) {
+ final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth, MeasureSpec.EXACTLY);
+ int columnCount = holder.getColumnCount();
+
+ final boolean isDirectShare = holder instanceof DirectShareViewHolder;
+
+ for (int i = 0; i < columnCount; i++) {
+ final View v = mChooserListAdapter.createView(holder.getRowByIndex(i));
+ final int column = i;
+ v.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mChooserActivityDelegate.onTargetSelected(holder.getItemIndex(column));
+ }
+ });
+
+ // Show menu for both direct share and app share targets after long click.
+ v.setOnLongClickListener(v1 -> {
+ mChooserActivityDelegate.onTargetLongPressed(holder.getItemIndex(column));
+ return true;
+ });
+
+ holder.addView(i, v);
+
+ // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
+ // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be
+ // done before measuring.
+ if (isDirectShare) {
+ final ViewHolder vh = (ViewHolder) v.getTag();
+ vh.text.setLines(2);
+ vh.text.setHorizontallyScrolling(false);
+ vh.text2.setVisibility(View.GONE);
+ }
+
+ // Force height to be a given so we don't have visual disruption during scaling.
+ v.measure(exactSpec, spec);
+ setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight());
+ }
+
+ final ViewGroup viewGroup = holder.getViewGroup();
+
+ // Pre-measure and fix height so we can scale later.
+ holder.measure();
+ setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight());
+
+ if (isDirectShare) {
+ DirectShareViewHolder dsvh = (DirectShareViewHolder) holder;
+ setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
+ setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
+ }
+
+ viewGroup.setTag(holder);
+ return holder;
+ }
+
+ private void setViewBounds(View view, int widthPx, int heightPx) {
+ LayoutParams lp = view.getLayoutParams();
+ if (lp == null) {
+ lp = new LayoutParams(widthPx, heightPx);
+ view.setLayoutParams(lp);
+ } else {
+ lp.height = heightPx;
+ lp.width = widthPx;
+ }
+ }
+
+ ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) {
+ if (viewType == VIEW_TYPE_DIRECT_SHARE) {
+ ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate(
+ R.layout.chooser_row_direct_share, parent, false);
+ ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(
+ R.layout.chooser_row, parentGroup, false);
+ ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(
+ R.layout.chooser_row, parentGroup, false);
+ parentGroup.addView(row1);
+ parentGroup.addView(row2);
+
+ mDirectShareViewHolder = new DirectShareViewHolder(parentGroup,
+ Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType,
+ mChooserActivityDelegate::getValidTargetCount);
+ loadViewsIntoGroup(mDirectShareViewHolder);
+
+ return mDirectShareViewHolder;
+ } else {
+ ViewGroup row = (ViewGroup) mLayoutInflater.inflate(
+ R.layout.chooser_row, parent, false);
+ ItemGroupViewHolder holder =
+ new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType);
+ loadViewsIntoGroup(holder);
+
+ return holder;
+ }
+ }
+
+ /**
+ * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from
+ * showing on top of the AZ list if the AZ label is visible. All other types are placed into
+ * their own row as determined by their target type, and dividers are added in the list to
+ * separate each type.
+ */
+ int getRowType(int rowPosition) {
+ // Merge caller and ranked standard into a single row
+ int positionType = mChooserListAdapter.getPositionTargetType(rowPosition);
+ if (positionType == ChooserListAdapter.TARGET_CALLER) {
+ return ChooserListAdapter.TARGET_STANDARD;
+ }
+
+ // If an A-Z label is shown, prevent a separator from appearing by making the A-Z
+ // row type the same as the suggestion row type
+ if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) {
+ return ChooserListAdapter.TARGET_STANDARD;
+ }
+
+ return positionType;
+ }
+
+ void bindItemViewHolder(int position, ItemViewHolder holder) {
+ View v = holder.itemView;
+ int listPosition = getListPosition(position);
+ holder.setListPosition(listPosition);
+ mChooserListAdapter.bindView(listPosition, v);
+ }
+
+ void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) {
+ final ViewGroup viewGroup = (ViewGroup) holder.itemView;
+ int start = getListPosition(position);
+ int startType = getRowType(start);
+
+ int columnCount = holder.getColumnCount();
+ int end = start + columnCount - 1;
+ while (getRowType(end) != startType && end >= start) {
+ end--;
+ }
+
+ if (end == start && mChooserListAdapter.getItem(start).isEmptyTargetInfo()) {
+ final TextView textView = viewGroup.findViewById(
+ com.android.internal.R.id.chooser_row_text_option);
+
+ if (textView.getVisibility() != View.VISIBLE) {
+ textView.setAlpha(0.0f);
+ textView.setVisibility(View.VISIBLE);
+ textView.setText(R.string.chooser_no_direct_share_targets);
+
+ ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f);
+ fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+
+ textView.setTranslationY(mChooserRowTextOptionTranslatePixelSize);
+ ValueAnimator translateAnim =
+ ObjectAnimator.ofFloat(textView, "translationY", 0.0f);
+ translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+
+ AnimatorSet animSet = new AnimatorSet();
+ animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+ animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+ animSet.playTogether(fadeAnim, translateAnim);
+ animSet.start();
+ }
+ }
+
+ for (int i = 0; i < columnCount; i++) {
+ final View v = holder.getView(i);
+
+ if (start + i <= end) {
+ holder.setViewVisibility(i, View.VISIBLE);
+ holder.setItemIndex(i, start + i);
+ mChooserListAdapter.bindView(holder.getItemIndex(i), v);
+ } else {
+ holder.setViewVisibility(i, View.INVISIBLE);
+ }
+ }
+ }
+
+ int getListPosition(int position) {
+ position -= getSystemRowCount() + getProfileRowCount();
+
+ final int serviceCount = mChooserListAdapter.getServiceTargetCount();
+ final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow);
+ if (position < serviceRows) {
+ return position * mMaxTargetsPerRow;
+ }
+
+ position -= serviceRows;
+
+ final int callerAndRankedCount =
+ mChooserListAdapter.getCallerTargetCount()
+ + mChooserListAdapter.getRankedTargetCount();
+ final int callerAndRankedRows = getCallerAndRankedTargetRowCount();
+ if (position < callerAndRankedRows) {
+ return serviceCount + position * mMaxTargetsPerRow;
+ }
+
+ position -= getAzLabelRowCount() + callerAndRankedRows;
+
+ return callerAndRankedCount + serviceCount + position;
+ }
+
+ public void handleScroll(View v, int y, int oldy) {
+ boolean canExpandDirectShare = canExpandDirectShare();
+ if (mDirectShareViewHolder != null && canExpandDirectShare) {
+ mChooserActivityDelegate.handleScrollToExpandDirectShare(
+ mDirectShareViewHolder, y, oldy);
+ }
+ }
+
+ /** Only expand direct share area if there is a minimum number of targets. */
+ private boolean canExpandDirectShare() {
+ // Do not enable until we have confirmed more apps are using sharing shortcuts
+ // Check git history for enablement logic
+ return false;
+ }
+
+ public ChooserListAdapter getListAdapter() {
+ return mChooserListAdapter;
+ }
+
+ public boolean shouldCellSpan(int position) {
+ return getItemViewType(position) == VIEW_TYPE_NORMAL;
+ }
+
+ public void updateDirectShareExpansion() {
+ if (mDirectShareViewHolder == null || !canExpandDirectShare()) {
+ return;
+ }
+ mChooserActivityDelegate.updateDirectShareExpansion(mDirectShareViewHolder);
+ }
+}
diff --git a/java/src/com/android/intentresolver/grid/DirectShareViewHolder.java b/java/src/com/android/intentresolver/grid/DirectShareViewHolder.java
new file mode 100644
index 00000000..316c9f07
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/DirectShareViewHolder.java
@@ -0,0 +1,197 @@
+/*
+ * 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.intentresolver.grid;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.intentresolver.ChooserActivity;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+
+/** Holder for direct share targets in the {@link ChooserGridAdapter}. */
+public class DirectShareViewHolder extends ItemGroupViewHolder {
+ private final ViewGroup mParent;
+ private final List<ViewGroup> mRows;
+ private int mCellCountPerRow;
+
+ private boolean mHideDirectShareExpansion = false;
+ private int mDirectShareMinHeight = 0;
+ private int mDirectShareCurrHeight = 0;
+ private int mDirectShareMaxHeight = 0;
+
+ private final boolean[] mCellVisibility;
+
+ private final Supplier<Integer> mDeferredTargetCountSupplier;
+
+ public DirectShareViewHolder(
+ ViewGroup parent,
+ List<ViewGroup> rows,
+ int cellCountPerRow,
+ int viewType,
+ Supplier<Integer> deferredTargetCountSupplier) {
+ super(rows.size() * cellCountPerRow, parent, viewType);
+
+ this.mParent = parent;
+ this.mRows = rows;
+ this.mCellCountPerRow = cellCountPerRow;
+ this.mCellVisibility = new boolean[rows.size() * cellCountPerRow];
+ Arrays.fill(mCellVisibility, true);
+ this.mDeferredTargetCountSupplier = deferredTargetCountSupplier;
+ }
+
+ public ViewGroup addView(int index, View v) {
+ ViewGroup row = getRowByIndex(index);
+ row.addView(v);
+ mCells[index] = v;
+
+ return row;
+ }
+
+ public ViewGroup getViewGroup() {
+ return mParent;
+ }
+
+ public ViewGroup getRowByIndex(int index) {
+ return mRows.get(index / mCellCountPerRow);
+ }
+
+ public ViewGroup getRow(int rowNumber) {
+ return mRows.get(rowNumber);
+ }
+
+ public void measure() {
+ final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ getRow(0).measure(spec, spec);
+ getRow(1).measure(spec, spec);
+
+ mDirectShareMinHeight = getRow(0).getMeasuredHeight();
+ mDirectShareCurrHeight = (mDirectShareCurrHeight > 0)
+ ? mDirectShareCurrHeight : mDirectShareMinHeight;
+ mDirectShareMaxHeight = 2 * mDirectShareMinHeight;
+ }
+
+ public int getMeasuredRowHeight() {
+ return mDirectShareCurrHeight;
+ }
+
+ public int getMinRowHeight() {
+ return mDirectShareMinHeight;
+ }
+
+ public void setViewVisibility(int i, int visibility) {
+ final View v = getView(i);
+ if (visibility == View.VISIBLE) {
+ mCellVisibility[i] = true;
+ v.setVisibility(visibility);
+ v.setAlpha(1.0f);
+ } else if (visibility == View.INVISIBLE && mCellVisibility[i]) {
+ mCellVisibility[i] = false;
+
+ ValueAnimator fadeAnim = ObjectAnimator.ofFloat(v, "alpha", 1.0f, 0f);
+ fadeAnim.setDuration(ChooserGridAdapter.NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+ fadeAnim.setInterpolator(new AccelerateInterpolator(1.0f));
+ fadeAnim.addListener(new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animation) {
+ v.setVisibility(View.INVISIBLE);
+ }
+ });
+ fadeAnim.start();
+ }
+ }
+
+ public void handleScroll(RecyclerView view, int y, int oldy, int maxTargetsPerRow) {
+ // only exit early if fully collapsed, otherwise onListRebuilt() with shifting
+ // targets can lock us into an expanded mode
+ boolean notExpanded = mDirectShareCurrHeight == mDirectShareMinHeight;
+ if (notExpanded) {
+ if (mHideDirectShareExpansion) {
+ return;
+ }
+
+ // only expand if we have more than maxTargetsPerRow, and delay that decision
+ // until they start to scroll
+ final int validTargets = this.mDeferredTargetCountSupplier.get();
+ if (validTargets <= maxTargetsPerRow) {
+ mHideDirectShareExpansion = true;
+ return;
+ }
+ }
+
+ int yDiff = (int) ((oldy - y) * ChooserActivity.DIRECT_SHARE_EXPANSION_RATE);
+
+ int prevHeight = mDirectShareCurrHeight;
+ int newHeight = Math.min(prevHeight + yDiff, mDirectShareMaxHeight);
+ newHeight = Math.max(newHeight, mDirectShareMinHeight);
+ yDiff = newHeight - prevHeight;
+
+ updateDirectShareRowHeight(view, yDiff, newHeight);
+ }
+
+ public void expand(RecyclerView view) {
+ updateDirectShareRowHeight(
+ view, mDirectShareMaxHeight - mDirectShareCurrHeight, mDirectShareMaxHeight);
+ }
+
+ public void collapse(RecyclerView view) {
+ updateDirectShareRowHeight(
+ view, mDirectShareMinHeight - mDirectShareCurrHeight, mDirectShareMinHeight);
+ }
+
+ private void updateDirectShareRowHeight(RecyclerView view, int yDiff, int newHeight) {
+ if (view == null || view.getChildCount() == 0 || yDiff == 0) {
+ return;
+ }
+
+ // locate the item to expand, and offset the rows below that one
+ boolean foundExpansion = false;
+ for (int i = 0; i < view.getChildCount(); i++) {
+ View child = view.getChildAt(i);
+
+ if (foundExpansion) {
+ child.offsetTopAndBottom(yDiff);
+ } else {
+ if (child.getTag() != null && child.getTag() instanceof DirectShareViewHolder) {
+ int widthSpec = MeasureSpec.makeMeasureSpec(child.getWidth(),
+ MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(newHeight,
+ MeasureSpec.EXACTLY);
+ child.measure(widthSpec, heightSpec);
+ child.getLayoutParams().height = child.getMeasuredHeight();
+ child.layout(child.getLeft(), child.getTop(), child.getRight(),
+ child.getTop() + child.getMeasuredHeight());
+
+ foundExpansion = true;
+ }
+ }
+ }
+
+ if (foundExpansion) {
+ mDirectShareCurrHeight = newHeight;
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserFlags.java b/java/src/com/android/intentresolver/grid/FooterViewHolder.java
index 67f9046f..0c94e3ed 100644
--- a/java/src/com/android/intentresolver/ChooserFlags.java
+++ b/java/src/com/android/intentresolver/grid/FooterViewHolder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,20 +14,15 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.grid;
-import android.app.prediction.AppPredictionManager;
+import android.view.View;
/**
- * Common flags for {@link ChooserListAdapter} and {@link ChooserActivity}.
+ * A footer on the list, to support scrolling behavior below the navbar.
*/
-public class ChooserFlags {
-
- /**
- * Whether to use {@link AppPredictionManager} to query for direct share targets (as opposed to
- * talking directly to {@link android.content.pm.ShortcutManager}.
- */
- // TODO(b/123089490): Replace with system flag
- static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true;
+public final class FooterViewHolder extends ViewHolderBase {
+ public FooterViewHolder(View itemView, int viewType) {
+ super(itemView, viewType);
+ }
}
-
diff --git a/java/src/com/android/intentresolver/grid/ItemGroupViewHolder.java b/java/src/com/android/intentresolver/grid/ItemGroupViewHolder.java
new file mode 100644
index 00000000..5470506b
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/ItemGroupViewHolder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.intentresolver.grid;
+
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+
+/**
+ * Used to bind types for group of items including:
+ * {@link ChooserGridAdapter#VIEW_TYPE_DIRECT_SHARE},
+ * and {@link ChooserGridAdapter#VIEW_TYPE_CALLER_AND_RANK}.
+ */
+public abstract class ItemGroupViewHolder extends ViewHolderBase {
+ protected int mMeasuredRowHeight;
+ private int[] mItemIndices;
+ protected final View[] mCells;
+ private final int mColumnCount;
+
+ public ItemGroupViewHolder(int cellCount, View itemView, int viewType) {
+ super(itemView, viewType);
+ this.mCells = new View[cellCount];
+ this.mItemIndices = new int[cellCount];
+ this.mColumnCount = cellCount;
+ }
+
+ public abstract ViewGroup addView(int index, View v);
+
+ public abstract ViewGroup getViewGroup();
+
+ public abstract ViewGroup getRowByIndex(int index);
+
+ public abstract ViewGroup getRow(int rowNumber);
+
+ public abstract void setViewVisibility(int i, int visibility);
+
+ public int getColumnCount() {
+ return mColumnCount;
+ }
+
+ public void measure() {
+ final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ getViewGroup().measure(spec, spec);
+ mMeasuredRowHeight = getViewGroup().getMeasuredHeight();
+ }
+
+ public int getMeasuredRowHeight() {
+ return mMeasuredRowHeight;
+ }
+
+ public void setItemIndex(int itemIndex, int listIndex) {
+ mItemIndices[itemIndex] = listIndex;
+ }
+
+ public int getItemIndex(int itemIndex) {
+ return mItemIndices[itemIndex];
+ }
+
+ public View getView(int index) {
+ return mCells[index];
+ }
+}
diff --git a/java/src/com/android/intentresolver/grid/ItemViewHolder.java b/java/src/com/android/intentresolver/grid/ItemViewHolder.java
new file mode 100644
index 00000000..2ec56b1b
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/ItemViewHolder.java
@@ -0,0 +1,63 @@
+/*
+ * 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.intentresolver.grid;
+
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ResolverListAdapter;
+
+import java.util.function.Consumer;
+
+/**
+ * Used to bind types of individual item including
+ * {@link ChooserGridAdapter#VIEW_TYPE_NORMAL},
+ * {@link ChooserGridAdapter#VIEW_TYPE_CONTENT_PREVIEW},
+ * {@link ChooserGridAdapter#VIEW_TYPE_PROFILE},
+ * and {@link ChooserGridAdapter#VIEW_TYPE_AZ_LABEL}.
+ */
+public final class ItemViewHolder extends ViewHolderBase {
+ private final ResolverListAdapter.ViewHolder mWrappedViewHolder;
+
+ private int mListPosition = ChooserListAdapter.NO_POSITION;
+
+ public ItemViewHolder(
+ View itemView,
+ int viewType,
+ @Nullable Consumer<Integer> onClick,
+ @Nullable Consumer<Integer> onLongClick) {
+ super(itemView, viewType);
+ mWrappedViewHolder = new ResolverListAdapter.ViewHolder(itemView);
+
+ if (onClick != null) {
+ itemView.setOnClickListener(v -> onClick.accept(mListPosition));
+ }
+
+ if (onLongClick != null) {
+ itemView.setOnLongClickListener(v -> {
+ onLongClick.accept(mListPosition);
+ return true;
+ });
+ }
+ }
+
+ public void setListPosition(int listPosition) {
+ mListPosition = listPosition;
+ }
+}
diff --git a/java/src/com/android/intentresolver/grid/SingleRowViewHolder.java b/java/src/com/android/intentresolver/grid/SingleRowViewHolder.java
new file mode 100644
index 00000000..a72da7aa
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/SingleRowViewHolder.java
@@ -0,0 +1,73 @@
+/*
+ * 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.intentresolver.grid;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/** Holder for a group of items displayed in a single row of the {@link ChooserGridAdapter}. */
+public final class SingleRowViewHolder extends ItemGroupViewHolder {
+ private final ViewGroup mRow;
+
+ public SingleRowViewHolder(ViewGroup row, int cellCount, int viewType) {
+ super(cellCount, row, viewType);
+
+ this.mRow = row;
+ }
+
+ /** Get the group of all views in this holder. */
+ public ViewGroup getViewGroup() {
+ return mRow;
+ }
+
+ /**
+ * Get the group of views for the row containing the specified cell index.
+ * TODO: unclear if that's what this `index` meant. It doesn't matter for our "single row"
+ * holders, and it doesn't look like this is an override from some other interface; maybe we can
+ * just remove?
+ */
+ public ViewGroup getRowByIndex(int index) {
+ return mRow;
+ }
+
+ /** Get the group of views for the specified {@code rowNumber}, if any. */
+ public ViewGroup getRow(int rowNumber) {
+ if (rowNumber == 0) {
+ return mRow;
+ }
+ return null;
+ }
+
+ /**
+ * @param index the index of the cell to add the view into.
+ * @param v the view to add into the cell.
+ */
+ public ViewGroup addView(int index, View v) {
+ mRow.addView(v);
+ mCells[index] = v;
+
+ return mRow;
+ }
+
+ /**
+ * @param i the index of the cell containing the view to modify.
+ * @param visibility the new visibility to set on the view with the specified index.
+ */
+ public void setViewVisibility(int i, int visibility) {
+ getView(i).setVisibility(visibility);
+ }
+}
diff --git a/java/src/com/android/intentresolver/grid/ViewHolderBase.java b/java/src/com/android/intentresolver/grid/ViewHolderBase.java
new file mode 100644
index 00000000..78e9104a
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/ViewHolderBase.java
@@ -0,0 +1,35 @@
+/*
+ * 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.intentresolver.grid;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/** Base class for all {@link RecyclerView.ViewHolder} types in the {@link ChooserGridAdapter}. */
+public abstract class ViewHolderBase extends RecyclerView.ViewHolder {
+ private int mViewType;
+
+ ViewHolderBase(View itemView, int viewType) {
+ super(itemView);
+ this.mViewType = viewType;
+ }
+
+ public int getViewType() {
+ return mViewType;
+ }
+}
diff --git a/java/src/com/android/intentresolver/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index 6f802876..271c6f98 100644
--- a/java/src/com/android/intentresolver/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.model;
import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
@@ -29,6 +29,8 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
+import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.ResolverActivity;
import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import java.text.Collator;
@@ -47,7 +49,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
private static final boolean DEBUG = true;
private static final String TAG = "AbstractResolverComp";
- protected AfterCompute mAfterCompute;
+ protected Runnable mAfterCompute;
protected final PackageManager mPm;
protected final UsageStatsManager mUsm;
protected String[] mAnnotations;
@@ -129,15 +131,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
}
- /**
- * Callback to be called when {@link #compute(List)} finishes. This signals to stop waiting.
- */
- interface AfterCompute {
-
- void afterCompute();
- }
-
- void setCallBack(AfterCompute afterCompute) {
+ public void setCallBack(Runnable afterCompute) {
mAfterCompute = afterCompute;
}
@@ -150,9 +144,9 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
protected final void afterCompute() {
- final AfterCompute afterCompute = mAfterCompute;
+ final Runnable afterCompute = mAfterCompute;
if (afterCompute != null) {
- afterCompute.afterCompute();
+ afterCompute.run();
}
}
@@ -161,11 +155,6 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
final ResolveInfo lhs = lhsp.getResolveInfoAt(0);
final ResolveInfo rhs = rhsp.getResolveInfoAt(0);
- final boolean lFixedAtTop = lhsp.isFixedAtTop();
- final boolean rFixedAtTop = rhsp.isFixedAtTop();
- if (lFixedAtTop && !rFixedAtTop) return -1;
- if (!lFixedAtTop && rFixedAtTop) return 1;
-
// We want to put the one targeted to another user at the end of the dialog.
if (lhs.targetUserId != UserHandle.USER_CURRENT) {
return rhs.targetUserId != UserHandle.USER_CURRENT ? 0 : 1;
@@ -214,7 +203,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* ResolvedComponentInfo} by {@link ComponentName}. {@link #beforeCompute()} will be called
* before doing any computing.
*/
- final void compute(List<ResolvedComponentInfo> targets) {
+ public final void compute(List<ResolvedComponentInfo> targets) {
beforeCompute();
doCompute(targets);
}
@@ -226,7 +215,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo}
* when {@link #compute(List)} was called before this.
*/
- abstract float getScore(ComponentName name);
+ public abstract float getScore(ComponentName name);
/** Handles result message sent to mHandler. */
abstract void handleResultMessage(Message message);
@@ -234,7 +223,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
/**
* Reports to UsageStats what was chosen.
*/
- final void updateChooserCounts(String packageName, int userId, String action) {
+ public final void updateChooserCounts(String packageName, int userId, String action) {
if (mUsm != null) {
mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action);
}
@@ -248,7 +237,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
*
* @param componentName the component that the user clicked
*/
- void updateModel(ComponentName componentName) {
+ public void updateModel(ComponentName componentName) {
}
/** Called before {@link #doCompute(List)}. Sets up 500ms timeout. */
@@ -266,7 +255,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* this call needs to happen at a different time during destroy, the method should be
* overridden.
*/
- void destroy() {
+ public void destroy() {
mHandler.removeMessages(RANKER_SERVICE_RESULT);
mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
afterCompute();
diff --git a/java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index 9b9fc1c0..c6bb2b85 100644
--- a/java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.model;
import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
@@ -31,6 +31,7 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
+import com.android.intentresolver.ChooserActivityLogger;
import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import java.util.ArrayList;
@@ -45,7 +46,7 @@ import java.util.concurrent.Executors;
* disabled by returning an empty sorted target list, {@link AppPredictionServiceResolverComparator}
* will fallback to using a {@link ResolverRankerServiceResolverComparator}.
*/
-class AppPredictionServiceResolverComparator extends AbstractResolverComparator {
+public class AppPredictionServiceResolverComparator extends AbstractResolverComparator {
private static final String TAG = "APSResolverComparator";
@@ -62,7 +63,7 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
private ResolverRankerServiceResolverComparator mResolverRankerService;
private AppPredictionServiceComparatorModel mComparatorModel;
- AppPredictionServiceResolverComparator(
+ public AppPredictionServiceResolverComparator(
Context context,
Intent intent,
String referrerPackage,
@@ -166,17 +167,17 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
}
@Override
- float getScore(ComponentName name) {
+ public float getScore(ComponentName name) {
return mComparatorModel.getScore(name);
}
@Override
- void updateModel(ComponentName componentName) {
+ public void updateModel(ComponentName componentName) {
mComparatorModel.notifyOnTargetSelected(componentName);
}
@Override
- void destroy() {
+ public void destroy() {
if (mResolverRankerService != null) {
mResolverRankerService.destroy();
mResolverRankerService = null;
diff --git a/java/src/com/android/intentresolver/ResolverComparatorModel.java b/java/src/com/android/intentresolver/model/ResolverComparatorModel.java
index 79160c84..3616a853 100644
--- a/java/src/com/android/intentresolver/ResolverComparatorModel.java
+++ b/java/src/com/android/intentresolver/model/ResolverComparatorModel.java
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.model;
import android.content.ComponentName;
import android.content.pm.ResolveInfo;
import java.util.Comparator;
-import java.util.List;
/**
* A ranking model for resolver targets, providing ordering and (optionally) numerical scoring.
diff --git a/java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
index be3e6f18..4382f109 100644
--- a/java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
@@ -15,7 +15,7 @@
*/
-package com.android.intentresolver;
+package com.android.intentresolver.model;
import android.app.usage.UsageStats;
import android.content.ComponentName;
@@ -37,8 +37,8 @@ import android.service.resolver.ResolverRankerService;
import android.service.resolver.ResolverTarget;
import android.util.Log;
+import com.android.intentresolver.ChooserActivityLogger;
import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
-
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -54,7 +54,7 @@ import java.util.concurrent.TimeUnit;
/**
* Ranks and compares packages based on usage stats and uses the {@link ResolverRankerService}.
*/
-class ResolverRankerServiceResolverComparator extends AbstractResolverComparator {
+public class ResolverRankerServiceResolverComparator extends AbstractResolverComparator {
private static final String TAG = "RRSResolverComparator";
private static final boolean DEBUG = false;
@@ -87,7 +87,7 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
private ResolverRankerServiceComparatorModel mComparatorModel;
public ResolverRankerServiceResolverComparator(Context context, Intent intent,
- String referrerPackage, AfterCompute afterCompute,
+ String referrerPackage, Runnable afterCompute,
ChooserActivityLogger chooserActivityLogger) {
super(context, intent);
mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
@@ -191,9 +191,9 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
if (mAction == null) {
Log.d(TAG, "Action type is null");
} else {
- Log.d(TAG, "Chooser Count of " + mAction + ":" +
- target.name.getPackageName() + " is " +
- Float.toString(chooserScore));
+ Log.d(TAG, "Chooser Count of " + mAction + ":"
+ + target.name.getPackageName() + " is "
+ + Float.toString(chooserScore));
}
}
resolverTarget.setChooserScore(chooserScore);
@@ -333,7 +333,7 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
private class ResolverRankerServiceConnection implements ServiceConnection {
private final CountDownLatch mConnectSignal;
- public ResolverRankerServiceConnection(CountDownLatch connectSignal) {
+ ResolverRankerServiceConnection(CountDownLatch connectSignal) {
mConnectSignal = connectSignal;
}
@@ -424,8 +424,10 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
// adds select prob as the default values, according to a pre-trained Logistic Regression model.
private void addDefaultSelectProbability(ResolverTarget target) {
- float sum = 2.5543f * target.getLaunchScore() + 2.8412f * target.getTimeSpentScore() +
- 0.269f * target.getRecencyScore() + 4.2222f * target.getChooserScore();
+ float sum = (2.5543f * target.getLaunchScore())
+ + (2.8412f * target.getTimeSpentScore())
+ + (0.269f * target.getRecencyScore())
+ + (4.2222f * target.getChooserScore());
target.setSelectProbability((float) (1.0 / (1.0 + Math.exp(1.6568f - sum))));
}
@@ -440,8 +442,8 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
static boolean isPersistentProcess(ResolvedComponentInfo rci) {
if (rci != null && rci.getCount() > 0) {
- return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags &
- ApplicationInfo.FLAG_PERSISTENT) != 0;
+ int flags = rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags;
+ return (flags & ApplicationInfo.FLAG_PERSISTENT) != 0;
}
return false;
}
diff --git a/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt b/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt
new file mode 100644
index 00000000..82f40b91
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.intentresolver.shortcuts
+
+import android.app.prediction.AppPredictionContext
+import android.app.prediction.AppPredictionManager
+import android.app.prediction.AppPredictor
+import android.content.Context
+import android.content.IntentFilter
+import android.os.Bundle
+import android.os.UserHandle
+
+// TODO(b/123088566) Share these in a better way.
+private const val APP_PREDICTION_SHARE_UI_SURFACE = "share"
+private const val APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20
+private const val APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter"
+private const val SHARED_TEXT_KEY = "shared_text"
+
+/**
+ * A factory to create an AppPredictor instance for a profile, if available.
+ * @param context, application context
+ * @param sharedText, a shared text associated with the Chooser's target intent
+ * (see [android.content.Intent.EXTRA_TEXT]).
+ * Will be mapped to app predictor's "shared_text" parameter.
+ * @param targetIntentFilter, an IntentFilter to match direct share targets against.
+ * Will be mapped app predictor's "intent_filter" parameter.
+ */
+class AppPredictorFactory(
+ private val context: Context,
+ private val sharedText: String?,
+ private val targetIntentFilter: IntentFilter?
+) {
+ private val mIsComponentAvailable =
+ context.packageManager.appPredictionServicePackageName != null
+
+ /**
+ * Creates an AppPredictor instance for a profile or `null` if app predictor is not available.
+ */
+ fun create(userHandle: UserHandle): AppPredictor? {
+ if (!mIsComponentAvailable) return null
+ val contextAsUser = context.createContextAsUser(userHandle, 0 /* flags */)
+ val extras = Bundle().apply {
+ putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, targetIntentFilter)
+ putString(SHARED_TEXT_KEY, sharedText)
+ }
+ val appPredictionContext = AppPredictionContext.Builder(contextAsUser)
+ .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
+ .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
+ .setExtras(extras)
+ .build()
+ return contextAsUser.getSystemService(AppPredictionManager::class.java)
+ ?.createAppPredictionSession(appPredictionContext)
+ }
+}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java
new file mode 100644
index 00000000..1cfa2c8d
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java
@@ -0,0 +1,426 @@
+/*
+ * 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.intentresolver.shortcuts;
+
+import android.app.ActivityManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.os.AsyncTask;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager.
+ * <p>
+ * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut
+ * updates. The shortcut loading is triggered by the {@link #queryShortcuts(DisplayResolveInfo[])},
+ * the processing will happen on the {@link #mBackgroundExecutor} and the result is delivered
+ * through the {@link #mCallback} on the {@link #mCallbackExecutor}, the main thread.
+ * </p>
+ * <p>
+ * The current version does not improve on the legacy in a way that it does not guarantee that
+ * each invocation of the {@link #queryShortcuts(DisplayResolveInfo[])} will be matched by an
+ * invocation of the callback (there are early terminations of the flow). Also, the fetched
+ * shortcuts would be matched against the last known input, i.e. two invocations of
+ * {@link #queryShortcuts(DisplayResolveInfo[])} may result in two callbacks where shortcuts are
+ * processed against the latest input.
+ * </p>
+ */
+public class ShortcutLoader {
+ private static final String TAG = "ChooserActivity";
+
+ private static final Request NO_REQUEST = new Request(new DisplayResolveInfo[0]);
+
+ private final Context mContext;
+ @Nullable
+ private final AppPredictorProxy mAppPredictor;
+ private final UserHandle mUserHandle;
+ @Nullable
+ private final IntentFilter mTargetIntentFilter;
+ private final Executor mBackgroundExecutor;
+ private final Executor mCallbackExecutor;
+ private final boolean mIsPersonalProfile;
+ private final ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter =
+ new ShortcutToChooserTargetConverter();
+ private final UserManager mUserManager;
+ private final AtomicReference<Consumer<Result>> mCallback = new AtomicReference<>();
+ private final AtomicReference<Request> mActiveRequest = new AtomicReference<>(NO_REQUEST);
+
+ @Nullable
+ private final AppPredictor.Callback mAppPredictorCallback;
+
+ @MainThread
+ public ShortcutLoader(
+ Context context,
+ @Nullable AppPredictor appPredictor,
+ UserHandle userHandle,
+ @Nullable IntentFilter targetIntentFilter,
+ Consumer<Result> callback) {
+ this(
+ context,
+ appPredictor == null ? null : new AppPredictorProxy(appPredictor),
+ userHandle,
+ userHandle.equals(UserHandle.of(ActivityManager.getCurrentUser())),
+ targetIntentFilter,
+ AsyncTask.SERIAL_EXECUTOR,
+ context.getMainExecutor(),
+ callback);
+ }
+
+ @VisibleForTesting
+ ShortcutLoader(
+ Context context,
+ @Nullable AppPredictorProxy appPredictor,
+ UserHandle userHandle,
+ boolean isPersonalProfile,
+ @Nullable IntentFilter targetIntentFilter,
+ Executor backgroundExecutor,
+ Executor callbackExecutor,
+ Consumer<Result> callback) {
+ mContext = context;
+ mAppPredictor = appPredictor;
+ mUserHandle = userHandle;
+ mTargetIntentFilter = targetIntentFilter;
+ mBackgroundExecutor = backgroundExecutor;
+ mCallbackExecutor = callbackExecutor;
+ mCallback.set(callback);
+ mIsPersonalProfile = isPersonalProfile;
+ mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+
+ if (mAppPredictor != null) {
+ mAppPredictorCallback = createAppPredictorCallback();
+ mAppPredictor.registerPredictionUpdates(mCallbackExecutor, mAppPredictorCallback);
+ } else {
+ mAppPredictorCallback = null;
+ }
+ }
+
+ /**
+ * Unsubscribe from app predictor if one was provided.
+ */
+ @MainThread
+ public void destroy() {
+ if (mCallback.getAndSet(null) != null) {
+ if (mAppPredictor != null) {
+ mAppPredictor.unregisterPredictionUpdates(mAppPredictorCallback);
+ }
+ }
+ }
+
+ private boolean isDestroyed() {
+ return mCallback.get() == null;
+ }
+
+ /**
+ * Set new resolved targets. This will trigger shortcut loading.
+ * @param appTargets a collection of application targets a loaded set of shortcuts will be
+ * grouped against
+ */
+ @MainThread
+ public void queryShortcuts(DisplayResolveInfo[] appTargets) {
+ if (isDestroyed()) {
+ return;
+ }
+ mActiveRequest.set(new Request(appTargets));
+ mBackgroundExecutor.execute(this::loadShortcuts);
+ }
+
+ @WorkerThread
+ private void loadShortcuts() {
+ // no need to query direct share for work profile when its locked or disabled
+ if (!shouldQueryDirectShareTargets()) {
+ return;
+ }
+ Log.d(TAG, "querying direct share targets");
+ queryDirectShareTargets(false);
+ }
+
+ @WorkerThread
+ private void queryDirectShareTargets(boolean skipAppPredictionService) {
+ if (isDestroyed()) {
+ return;
+ }
+ if (!skipAppPredictionService && mAppPredictor != null) {
+ mAppPredictor.requestPredictionUpdate();
+ return;
+ }
+ // Default to just querying ShortcutManager if AppPredictor not present.
+ if (mTargetIntentFilter == null) {
+ return;
+ }
+
+ Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */);
+ ShortcutManager sm = (ShortcutManager) selectedProfileContext
+ .getSystemService(Context.SHORTCUT_SERVICE);
+ List<ShortcutManager.ShareShortcutInfo> shortcuts =
+ sm.getShareTargets(mTargetIntentFilter);
+ sendShareShortcutInfoList(shortcuts, false, null);
+ }
+
+ private AppPredictor.Callback createAppPredictorCallback() {
+ return appPredictorTargets -> {
+ if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) {
+ // APS may be disabled, so try querying targets ourselves.
+ queryDirectShareTargets(true);
+ return;
+ }
+
+ final List<ShortcutManager.ShareShortcutInfo> shortcuts = new ArrayList<>();
+ List<AppTarget> shortcutResults = new ArrayList<>();
+ for (AppTarget appTarget : appPredictorTargets) {
+ if (appTarget.getShortcutInfo() == null) {
+ continue;
+ }
+ shortcutResults.add(appTarget);
+ }
+ appPredictorTargets = shortcutResults;
+ for (AppTarget appTarget : appPredictorTargets) {
+ shortcuts.add(new ShortcutManager.ShareShortcutInfo(
+ appTarget.getShortcutInfo(),
+ new ComponentName(appTarget.getPackageName(), appTarget.getClassName())));
+ }
+ sendShareShortcutInfoList(shortcuts, true, appPredictorTargets);
+ };
+ }
+
+ @WorkerThread
+ private void sendShareShortcutInfoList(
+ List<ShortcutManager.ShareShortcutInfo> shortcuts,
+ boolean isFromAppPredictor,
+ @Nullable List<AppTarget> appPredictorTargets) {
+ if (appPredictorTargets != null && appPredictorTargets.size() != shortcuts.size()) {
+ throw new RuntimeException("resultList and appTargets must have the same size."
+ + " resultList.size()=" + shortcuts.size()
+ + " appTargets.size()=" + appPredictorTargets.size());
+ }
+ Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */);
+ for (int i = shortcuts.size() - 1; i >= 0; i--) {
+ final String packageName = shortcuts.get(i).getTargetComponent().getPackageName();
+ if (!isPackageEnabled(selectedProfileContext, packageName)) {
+ shortcuts.remove(i);
+ if (appPredictorTargets != null) {
+ appPredictorTargets.remove(i);
+ }
+ }
+ }
+
+ HashMap<ChooserTarget, AppTarget> directShareAppTargetCache = new HashMap<>();
+ HashMap<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache = new HashMap<>();
+ // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path
+ // for direct share targets. After ShareSheet is refactored we should use the
+ // ShareShortcutInfos directly.
+ final DisplayResolveInfo[] appTargets = mActiveRequest.get().appTargets;
+ List<ShortcutResultInfo> resultRecords = new ArrayList<>();
+ for (DisplayResolveInfo displayResolveInfo : appTargets) {
+ List<ShortcutManager.ShareShortcutInfo> matchingShortcuts =
+ filterShortcutsByTargetComponentName(
+ shortcuts, displayResolveInfo.getResolvedComponentName());
+ if (matchingShortcuts.isEmpty()) {
+ continue;
+ }
+
+ List<ChooserTarget> chooserTargets = mShortcutToChooserTargetConverter
+ .convertToChooserTarget(
+ matchingShortcuts,
+ shortcuts,
+ appPredictorTargets,
+ directShareAppTargetCache,
+ directShareShortcutInfoCache);
+
+ ShortcutResultInfo resultRecord =
+ new ShortcutResultInfo(displayResolveInfo, chooserTargets);
+ resultRecords.add(resultRecord);
+ }
+
+ postReport(
+ new Result(
+ isFromAppPredictor,
+ appTargets,
+ resultRecords.toArray(new ShortcutResultInfo[0]),
+ directShareAppTargetCache,
+ directShareShortcutInfoCache));
+ }
+
+ private void postReport(Result result) {
+ mCallbackExecutor.execute(() -> report(result));
+ }
+
+ @MainThread
+ private void report(Result result) {
+ Consumer<Result> callback = mCallback.get();
+ if (callback != null) {
+ callback.accept(result);
+ }
+ }
+
+ /**
+ * Returns {@code false} if {@code userHandle} is the work profile and it's either
+ * in quiet mode or not running.
+ */
+ private boolean shouldQueryDirectShareTargets() {
+ return mIsPersonalProfile || isProfileActive();
+ }
+
+ @VisibleForTesting
+ protected boolean isProfileActive() {
+ return mUserManager.isUserRunning(mUserHandle)
+ && mUserManager.isUserUnlocked(mUserHandle)
+ && !mUserManager.isQuietModeEnabled(mUserHandle);
+ }
+
+ private static boolean isPackageEnabled(Context context, String packageName) {
+ if (TextUtils.isEmpty(packageName)) {
+ return false;
+ }
+ ApplicationInfo appInfo;
+ try {
+ appInfo = context.getPackageManager().getApplicationInfo(
+ packageName,
+ ApplicationInfoFlags.of(PackageManager.GET_META_DATA));
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+
+ return appInfo != null && appInfo.enabled
+ && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0;
+ }
+
+ private static List<ShortcutManager.ShareShortcutInfo> filterShortcutsByTargetComponentName(
+ List<ShortcutManager.ShareShortcutInfo> allShortcuts, ComponentName requiredTarget) {
+ List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = new ArrayList<>();
+ for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) {
+ if (requiredTarget.equals(shortcut.getTargetComponent())) {
+ matchingShortcuts.add(shortcut);
+ }
+ }
+ return matchingShortcuts;
+ }
+
+ private static class Request {
+ public final DisplayResolveInfo[] appTargets;
+
+ Request(DisplayResolveInfo[] targets) {
+ appTargets = targets;
+ }
+ }
+
+ /**
+ * Resolved shortcuts with corresponding app targets.
+ */
+ public static class Result {
+ public final boolean isFromAppPredictor;
+ /**
+ * Input app targets (see {@link ShortcutLoader#queryShortcuts(DisplayResolveInfo[])} the
+ * shortcuts were process against.
+ */
+ public final DisplayResolveInfo[] appTargets;
+ /**
+ * Shortcuts grouped by app target.
+ */
+ public final ShortcutResultInfo[] shortcutsByApp;
+ public final Map<ChooserTarget, AppTarget> directShareAppTargetCache;
+ public final Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache;
+
+ @VisibleForTesting
+ public Result(
+ boolean isFromAppPredictor,
+ DisplayResolveInfo[] appTargets,
+ ShortcutResultInfo[] shortcutsByApp,
+ Map<ChooserTarget, AppTarget> directShareAppTargetCache,
+ Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache) {
+ this.isFromAppPredictor = isFromAppPredictor;
+ this.appTargets = appTargets;
+ this.shortcutsByApp = shortcutsByApp;
+ this.directShareAppTargetCache = directShareAppTargetCache;
+ this.directShareShortcutInfoCache = directShareShortcutInfoCache;
+ }
+ }
+
+ /**
+ * Shortcuts grouped by app.
+ */
+ public static class ShortcutResultInfo {
+ public final DisplayResolveInfo appTarget;
+ public final List<ChooserTarget> shortcuts;
+
+ public ShortcutResultInfo(DisplayResolveInfo appTarget, List<ChooserTarget> shortcuts) {
+ this.appTarget = appTarget;
+ this.shortcuts = shortcuts;
+ }
+ }
+
+ /**
+ * A wrapper around AppPredictor to facilitate unit-testing.
+ */
+ @VisibleForTesting
+ public static class AppPredictorProxy {
+ private final AppPredictor mAppPredictor;
+
+ AppPredictorProxy(AppPredictor appPredictor) {
+ mAppPredictor = appPredictor;
+ }
+
+ /**
+ * {@link AppPredictor#registerPredictionUpdates}
+ */
+ public void registerPredictionUpdates(
+ Executor callbackExecutor, AppPredictor.Callback callback) {
+ mAppPredictor.registerPredictionUpdates(callbackExecutor, callback);
+ }
+
+ /**
+ * {@link AppPredictor#unregisterPredictionUpdates}
+ */
+ public void unregisterPredictionUpdates(AppPredictor.Callback callback) {
+ mAppPredictor.unregisterPredictionUpdates(callback);
+ }
+
+ /**
+ * {@link AppPredictor#requestPredictionUpdate}
+ */
+ public void requestPredictionUpdate() {
+ mAppPredictor.requestPredictionUpdate();
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
new file mode 100644
index 00000000..a37d6558
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
@@ -0,0 +1,109 @@
+/*
+ * 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.intentresolver.shortcuts;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.prediction.AppTarget;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.os.Bundle;
+import android.service.chooser.ChooserTarget;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class ShortcutToChooserTargetConverter {
+
+ /**
+ * Converts a list of ShareShortcutInfos to ChooserTargets.
+ * @param matchingShortcuts List of shortcuts, all from the same package, that match the current
+ * share intent filter.
+ * @param allShortcuts List of all the shortcuts from all the packages on the device that are
+ * returned for the current sharing action.
+ * @param allAppTargets List of AppTargets. Null if the results are not from prediction service.
+ * @param directShareAppTargetCache An optional map to store mapping for the new ChooserTarget
+ * instances back to original allAppTargets.
+ * @param directShareShortcutInfoCache An optional map to store mapping from the new
+ * ChooserTarget instances back to the original matchingShortcuts' {@code getShortcutInfo()}
+ * @return A list of ChooserTargets sorted by score in descending order.
+ */
+ @NonNull
+ public List<ChooserTarget> convertToChooserTarget(
+ @NonNull List<ShortcutManager.ShareShortcutInfo> matchingShortcuts,
+ @NonNull List<ShortcutManager.ShareShortcutInfo> allShortcuts,
+ @Nullable List<AppTarget> allAppTargets,
+ @Nullable Map<ChooserTarget, AppTarget> directShareAppTargetCache,
+ @Nullable Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache) {
+ // If |appTargets| is not null, results are from AppPredictionService and already sorted.
+ final boolean isFromAppPredictor = allAppTargets != null;
+ // A set of distinct scores for the matched shortcuts. We use index of a rank in the sorted
+ // list instead of the actual rank value when converting a rank to a score.
+ List<Integer> scoreList = new ArrayList<>();
+ if (!isFromAppPredictor) {
+ for (int i = 0; i < matchingShortcuts.size(); i++) {
+ int shortcutRank = matchingShortcuts.get(i).getShortcutInfo().getRank();
+ if (!scoreList.contains(shortcutRank)) {
+ scoreList.add(shortcutRank);
+ }
+ }
+ Collections.sort(scoreList);
+ }
+
+ List<ChooserTarget> chooserTargetList = new ArrayList<>(matchingShortcuts.size());
+ for (int i = 0; i < matchingShortcuts.size(); i++) {
+ ShortcutInfo shortcutInfo = matchingShortcuts.get(i).getShortcutInfo();
+ int indexInAllShortcuts = allShortcuts.indexOf(matchingShortcuts.get(i));
+
+ float score;
+ if (isFromAppPredictor) {
+ // Incoming results are ordered. Create a score based on index in the original list.
+ score = Math.max(1.0f - (0.01f * indexInAllShortcuts), 0.0f);
+ } else {
+ // Create a score based on the rank of the shortcut.
+ int rankIndex = scoreList.indexOf(shortcutInfo.getRank());
+ score = Math.max(1.0f - (0.01f * rankIndex), 0.0f);
+ }
+
+ Bundle extras = new Bundle();
+ extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId());
+
+ ChooserTarget chooserTarget = new ChooserTarget(
+ shortcutInfo.getLabel(),
+ null, // Icon will be loaded later if this target is selected to be shown.
+ score, matchingShortcuts.get(i).getTargetComponent().clone(), extras);
+
+ chooserTargetList.add(chooserTarget);
+ if (directShareAppTargetCache != null && allAppTargets != null) {
+ directShareAppTargetCache.put(chooserTarget,
+ allAppTargets.get(indexInAllShortcuts));
+ }
+ if (directShareShortcutInfoCache != null) {
+ directShareShortcutInfoCache.put(chooserTarget, shortcutInfo);
+ }
+ }
+ // Sort ChooserTargets by score in descending order
+ Comparator<ChooserTarget> byScore =
+ (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore());
+ Collections.sort(chooserTargetList, byScore);
+ return chooserTargetList;
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ActionRow.kt b/java/src/com/android/intentresolver/widget/ActionRow.kt
new file mode 100644
index 00000000..6764d3ae
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ActionRow.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.intentresolver.widget
+
+import android.content.res.Resources.ID_NULL
+import android.graphics.drawable.Drawable
+
+interface ActionRow {
+ fun setActions(actions: List<Action>)
+
+ class Action @JvmOverloads constructor(
+ // TODO: apparently, IDs set to this field are used in unit tests only; evaluate whether we
+ // get rid of them
+ val id: Int = ID_NULL,
+ val label: CharSequence?,
+ val icon: Drawable?,
+ val onClicked: Runnable,
+ )
+}
diff --git a/java/src/com/android/intentresolver/widget/ChooserActionRow.kt b/java/src/com/android/intentresolver/widget/ChooserActionRow.kt
new file mode 100644
index 00000000..a4656bb5
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ChooserActionRow.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.intentresolver.widget
+
+import android.annotation.LayoutRes
+import android.content.Context
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.Button
+import android.widget.LinearLayout
+import com.android.intentresolver.R
+import com.android.intentresolver.widget.ActionRow.Action
+
+class ChooserActionRow : LinearLayout, ActionRow {
+ constructor(context: Context) : this(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int
+ ) : this(context, attrs, defStyleAttr, 0)
+
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes) {
+ orientation = HORIZONTAL
+ }
+
+ @LayoutRes
+ private val itemLayout = R.layout.chooser_action_button
+ private val itemMargin =
+ context.resources.getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2
+ private var actions: List<Action> = emptyList()
+
+ override fun onRestoreInstanceState(state: Parcelable?) {
+ super.onRestoreInstanceState(state)
+ setActions(actions)
+ }
+
+ override fun setActions(actions: List<Action>) {
+ removeAllViews()
+ this.actions = ArrayList(actions)
+ for (action in actions) {
+ addAction(action)
+ }
+ }
+
+ private fun addAction(action: Action) {
+ val b = LayoutInflater.from(context).inflate(itemLayout, null) as Button
+ if (action.icon != null) {
+ val size = resources
+ .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size)
+ action.icon.setBounds(0, 0, size, size)
+ b.setCompoundDrawablesRelative(action.icon, null, null, null)
+ }
+ b.text = action.label ?: ""
+ b.setOnClickListener {
+ action.onClicked.run()
+ }
+ b.id = action.id
+ addView(b)
+ }
+
+ override fun generateDefaultLayoutParams(): LayoutParams =
+ super.generateDefaultLayoutParams().apply {
+ setMarginsRelative(itemMargin, 0, itemMargin, 0)
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
new file mode 100644
index 00000000..a37ef954
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.intentresolver.widget
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.animation.DecelerateInterpolator
+import android.widget.RelativeLayout
+import androidx.core.view.isVisible
+import com.android.intentresolver.R
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import java.util.function.Consumer
+import com.android.internal.R as IntR
+
+typealias ImageLoader = suspend (Uri) -> Bitmap?
+
+private const val IMAGE_FADE_IN_MILLIS = 150L
+
+class ImagePreviewView : RelativeLayout {
+
+ constructor(context: Context) : this(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int
+ ) : this(context, attrs, defStyleAttr, 0)
+
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ private val coroutineScope = MainScope()
+ private lateinit var mainImage: RoundedRectImageView
+ private lateinit var secondLargeImage: RoundedRectImageView
+ private lateinit var secondSmallImage: RoundedRectImageView
+ private lateinit var thirdImage: RoundedRectImageView
+
+ private var loadImageJob: Job? = null
+ private var onTransitionViewReadyCallback: Consumer<Boolean>? = null
+
+ override fun onFinishInflate() {
+ LayoutInflater.from(context).inflate(R.layout.image_preview_view, this, true)
+ mainImage = requireViewById(IntR.id.content_preview_image_1_large)
+ secondLargeImage = requireViewById(IntR.id.content_preview_image_2_large)
+ secondSmallImage = requireViewById(IntR.id.content_preview_image_2_small)
+ thirdImage = requireViewById(IntR.id.content_preview_image_3_small)
+ }
+
+ /**
+ * Specifies a transition animation target name and a readiness callback. The callback will be
+ * invoked once when the view preparation is done i.e. either when an image is loaded into it
+ * and it is laid out (and it is ready to be draw) or image loading has failed.
+ * Should be called before [setImages].
+ * @param name, transition name
+ * @param onViewReady, a callback that will be invoked with `true` if the view is ready to
+ * receive transition animation (the image was loaded successfully) and with `false` otherwise.
+ */
+ fun setSharedElementTransitionTarget(name: String, onViewReady: Consumer<Boolean>) {
+ mainImage.transitionName = name
+ onTransitionViewReadyCallback = onViewReady
+ }
+
+ fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ loadImageJob?.cancel()
+ loadImageJob = coroutineScope.launch {
+ when (uris.size) {
+ 0 -> hideAllViews()
+ 1 -> showOneImage(uris, imageLoader)
+ 2 -> showTwoImages(uris, imageLoader)
+ else -> showThreeImages(uris, imageLoader)
+ }
+ }
+ }
+
+ private fun hideAllViews() {
+ mainImage.isVisible = false
+ secondLargeImage.isVisible = false
+ secondSmallImage.isVisible = false
+ thirdImage.isVisible = false
+ invokeTransitionViewReadyCallback(runTransitionAnimation = false)
+ }
+
+ private suspend fun showOneImage(uris: List<Uri>, imageLoader: ImageLoader) {
+ secondLargeImage.isVisible = false
+ secondSmallImage.isVisible = false
+ thirdImage.isVisible = false
+ showImages(uris, imageLoader, mainImage)
+ }
+
+ private suspend fun showTwoImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ secondSmallImage.isVisible = false
+ thirdImage.isVisible = false
+ showImages(uris, imageLoader, mainImage, secondLargeImage)
+ }
+
+ private suspend fun showThreeImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ secondLargeImage.isVisible = false
+ showImages(uris, imageLoader, mainImage, secondSmallImage, thirdImage)
+ thirdImage.setExtraImageCount(uris.size - 3)
+ }
+
+ private suspend fun showImages(
+ uris: List<Uri>, imageLoader: ImageLoader, vararg views: RoundedRectImageView
+ ) = coroutineScope {
+ for (i in views.indices) {
+ launch {
+ loadImageIntoView(views[i], uris[i], imageLoader)
+ }
+ }
+ }
+
+ private suspend fun loadImageIntoView(
+ view: RoundedRectImageView, uri: Uri, imageLoader: ImageLoader
+ ) {
+ val bitmap = runCatching {
+ imageLoader(uri)
+ }.getOrDefault(null)
+ if (bitmap == null) {
+ view.isVisible = false
+ if (view === mainImage) {
+ invokeTransitionViewReadyCallback(runTransitionAnimation = false)
+ }
+ } else {
+ view.isVisible = true
+ view.setImageBitmap(bitmap)
+
+ view.alpha = 0f
+ ObjectAnimator.ofFloat(view, "alpha", 0.0f, 1.0f).apply {
+ interpolator = DecelerateInterpolator(1.0f)
+ duration = IMAGE_FADE_IN_MILLIS
+ start()
+ }
+ if (view === mainImage && onTransitionViewReadyCallback != null) {
+ setupPreDrawListener(mainImage)
+ }
+ }
+ }
+
+ private fun setupPreDrawListener(view: View) {
+ view.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ view.viewTreeObserver.removeOnPreDrawListener(this)
+ invokeTransitionViewReadyCallback(runTransitionAnimation = true)
+ return true
+ }
+ }
+ )
+ }
+
+ private fun invokeTransitionViewReadyCallback(runTransitionAnimation: Boolean) {
+ onTransitionViewReadyCallback?.accept(runTransitionAnimation)
+ onTransitionViewReadyCallback = null
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
new file mode 100644
index 00000000..f5e20510
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
@@ -0,0 +1,1280 @@
+/*
+ * Copyright (C) 2014 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.widget;
+
+import static android.content.res.Resources.ID_NULL;
+
+import android.annotation.IdRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.metrics.LogMaker;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.AnimationUtils;
+import android.widget.AbsListView;
+import android.widget.OverScroller;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.intentresolver.R;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+public class ResolverDrawerLayout extends ViewGroup {
+ private static final String TAG = "ResolverDrawerLayout";
+ private MetricsLogger mMetricsLogger;
+
+ /**
+ * Max width of the whole drawer layout
+ */
+ private final int mMaxWidth;
+
+ /**
+ * Max total visible height of views not marked always-show when in the closed/initial state
+ */
+ private int mMaxCollapsedHeight;
+
+ /**
+ * Max total visible height of views not marked always-show when in the closed/initial state
+ * when a default option is present
+ */
+ private int mMaxCollapsedHeightSmall;
+
+ /**
+ * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
+ * inferred by {@code mMaxCollapsedHeight}.
+ */
+ private final boolean mIsMaxCollapsedHeightSmallExplicit;
+
+ private boolean mSmallCollapsed;
+
+ /**
+ * Move views down from the top by this much in px
+ */
+ private float mCollapseOffset;
+
+ /**
+ * Track fractions of pixels from drag calculations. Without this, the view offsets get
+ * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
+ */
+ private float mDragRemainder = 0.0f;
+ private int mHeightUsed;
+ private int mCollapsibleHeight;
+ private int mAlwaysShowHeight;
+
+ /**
+ * The height in pixels of reserved space added to the top of the collapsed UI;
+ * e.g. chooser targets
+ */
+ private int mCollapsibleHeightReserved;
+
+ private int mTopOffset;
+ private boolean mShowAtTop;
+ @IdRes
+ private int mIgnoreOffsetTopLimitViewId = ID_NULL;
+
+ private boolean mIsDragging;
+ private boolean mOpenOnClick;
+ private boolean mOpenOnLayout;
+ private boolean mDismissOnScrollerFinished;
+ private final int mTouchSlop;
+ private final float mMinFlingVelocity;
+ private final OverScroller mScroller;
+ private final VelocityTracker mVelocityTracker;
+
+ private Drawable mScrollIndicatorDrawable;
+
+ private OnDismissedListener mOnDismissedListener;
+ private RunOnDismissedListener mRunOnDismissedListener;
+ private OnCollapsedChangedListener mOnCollapsedChangedListener;
+
+ private boolean mDismissLocked;
+
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private float mLastTouchY;
+ private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+ private final Rect mTempRect = new Rect();
+
+ private AbsListView mNestedListChild;
+ private RecyclerView mNestedRecyclerChild;
+
+ private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
+ new ViewTreeObserver.OnTouchModeChangeListener() {
+ @Override
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
+ smoothScrollTo(0, 0);
+ }
+ }
+ };
+
+ public ResolverDrawerLayout(Context context) {
+ this(context, null);
+ }
+
+ public ResolverDrawerLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
+ defStyleAttr, 0);
+ mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_android_maxWidth, -1);
+ mMaxCollapsedHeight = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
+ mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
+ mMaxCollapsedHeight);
+ mIsMaxCollapsedHeightSmallExplicit =
+ a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
+ mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
+ if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) {
+ mIgnoreOffsetTopLimitViewId = a.getResourceId(
+ R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
+ }
+ a.recycle();
+
+ mScrollIndicatorDrawable = mContext.getDrawable(
+ com.android.internal.R.drawable.scroll_indicator_material);
+
+ mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
+ android.R.interpolator.decelerate_quint));
+ mVelocityTracker = VelocityTracker.obtain();
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+
+ setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ /**
+ * Dynamically set the max collapsed height. Note this also updates the small collapsed
+ * height if it wasn't specified explicitly.
+ */
+ public void setMaxCollapsedHeight(int heightInPixels) {
+ if (heightInPixels == mMaxCollapsedHeight) {
+ return;
+ }
+ mMaxCollapsedHeight = heightInPixels;
+ if (!mIsMaxCollapsedHeightSmallExplicit) {
+ mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
+ }
+ requestLayout();
+ }
+
+ public void setSmallCollapsed(boolean smallCollapsed) {
+ if (mSmallCollapsed != smallCollapsed) {
+ mSmallCollapsed = smallCollapsed;
+ requestLayout();
+ }
+ }
+
+ public boolean isSmallCollapsed() {
+ return mSmallCollapsed;
+ }
+
+ public boolean isCollapsed() {
+ return mCollapseOffset > 0;
+ }
+
+ public void setShowAtTop(boolean showOnTop) {
+ if (mShowAtTop != showOnTop) {
+ mShowAtTop = showOnTop;
+ requestLayout();
+ }
+ }
+
+ public boolean getShowAtTop() {
+ return mShowAtTop;
+ }
+
+ public void setCollapsed(boolean collapsed) {
+ if (!isLaidOut()) {
+ mOpenOnLayout = !collapsed;
+ } else {
+ smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
+ }
+ }
+
+ public void setCollapsibleHeightReserved(int heightPixels) {
+ final int oldReserved = mCollapsibleHeightReserved;
+ mCollapsibleHeightReserved = heightPixels;
+ if (oldReserved != mCollapsibleHeightReserved) {
+ requestLayout();
+ }
+
+ final int dReserved = mCollapsibleHeightReserved - oldReserved;
+ if (dReserved != 0 && mIsDragging) {
+ mLastTouchY -= dReserved;
+ }
+
+ final int oldCollapsibleHeight = updateCollapsibleHeight();
+ if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
+ return;
+ }
+
+ invalidate();
+ }
+
+ public void setDismissLocked(boolean locked) {
+ mDismissLocked = locked;
+ }
+
+ private boolean isMoving() {
+ return mIsDragging || !mScroller.isFinished();
+ }
+
+ private boolean isDragging() {
+ return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
+ }
+
+ private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
+ if (oldCollapsibleHeight == mCollapsibleHeight) {
+ return false;
+ }
+
+ if (getShowAtTop()) {
+ // Keep the drawer fully open.
+ setCollapseOffset(0);
+ return false;
+ }
+
+ if (isLaidOut()) {
+ final boolean isCollapsedOld = mCollapseOffset != 0;
+ if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
+ && mCollapseOffset == oldCollapsibleHeight)) {
+ // Stay closed even at the new height.
+ setCollapseOffset(mCollapsibleHeight);
+ } else {
+ setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight));
+ }
+ final boolean isCollapsedNew = mCollapseOffset != 0;
+ if (isCollapsedOld != isCollapsedNew) {
+ onCollapsedChanged(isCollapsedNew);
+ }
+ } else {
+ // Start out collapsed at first unless we restored state for otherwise
+ setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight);
+ }
+ return true;
+ }
+
+ private void setCollapseOffset(float collapseOffset) {
+ if (mCollapseOffset != collapseOffset) {
+ mCollapseOffset = collapseOffset;
+ requestLayout();
+ }
+ }
+
+ private int getMaxCollapsedHeight() {
+ return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
+ + mCollapsibleHeightReserved;
+ }
+
+ public void setOnDismissedListener(OnDismissedListener listener) {
+ mOnDismissedListener = listener;
+ }
+
+ private boolean isDismissable() {
+ return mOnDismissedListener != null && !mDismissLocked;
+ }
+
+ public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
+ mOnCollapsedChangedListener = listener;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mVelocityTracker.clear();
+ }
+
+ mVelocityTracker.addMovement(ev);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialTouchX = x;
+ mInitialTouchY = mLastTouchY = y;
+ mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ final float dy = y - mInitialTouchY;
+ if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
+ (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+ mActivePointerId = ev.getPointerId(0);
+ mIsDragging = true;
+ mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+ Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ resetTouch();
+ }
+ break;
+ }
+
+ if (mIsDragging) {
+ abortAnimation();
+ }
+ return mIsDragging || mOpenOnClick;
+ }
+
+ private boolean isNestedListChildScrolled() {
+ return mNestedListChild != null
+ && mNestedListChild.getChildCount() > 0
+ && (mNestedListChild.getFirstVisiblePosition() > 0
+ || mNestedListChild.getChildAt(0).getTop() < 0);
+ }
+
+ private boolean isNestedRecyclerChildScrolled() {
+ if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
+ final RecyclerView.ViewHolder vh =
+ mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
+ return vh == null || vh.itemView.getTop() < 0;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ mVelocityTracker.addMovement(ev);
+
+ boolean handled = false;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialTouchX = x;
+ mInitialTouchY = mLastTouchY = y;
+ mActivePointerId = ev.getPointerId(0);
+ final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
+ handled = isDismissable() || mCollapsibleHeight > 0;
+ mIsDragging = hitView && handled;
+ abortAnimation();
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ int index = ev.findPointerIndex(mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
+ index = 0;
+ mActivePointerId = ev.getPointerId(0);
+ mInitialTouchX = ev.getX();
+ mInitialTouchY = mLastTouchY = ev.getY();
+ }
+ final float x = ev.getX(index);
+ final float y = ev.getY(index);
+ if (!mIsDragging) {
+ final float dy = y - mInitialTouchY;
+ if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
+ handled = mIsDragging = true;
+ mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+ Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+ }
+ }
+ if (mIsDragging) {
+ final float dy = y - mLastTouchY;
+ if (dy > 0 && isNestedListChildScrolled()) {
+ mNestedListChild.smoothScrollBy((int) -dy, 0);
+ } else if (dy > 0 && isNestedRecyclerChildScrolled()) {
+ mNestedRecyclerChild.scrollBy(0, (int) -dy);
+ } else {
+ performDrag(dy);
+ }
+ }
+ mLastTouchY = y;
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int pointerIndex = ev.getActionIndex();
+ mActivePointerId = ev.getPointerId(pointerIndex);
+ mInitialTouchX = ev.getX(pointerIndex);
+ mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP: {
+ final boolean wasDragging = mIsDragging;
+ mIsDragging = false;
+ if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
+ findChildUnder(ev.getX(), ev.getY()) == null) {
+ if (isDismissable()) {
+ dispatchOnDismissed();
+ resetTouch();
+ return true;
+ }
+ }
+ if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
+ Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
+ smoothScrollTo(0, 0);
+ return true;
+ }
+ mVelocityTracker.computeCurrentVelocity(1000);
+ final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
+ if (Math.abs(yvel) > mMinFlingVelocity) {
+ if (getShowAtTop()) {
+ if (isDismissable() && yvel < 0) {
+ abortAnimation();
+ dismiss();
+ } else {
+ smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ }
+ } else {
+ if (isDismissable()
+ && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mHeightUsed, yvel);
+ mDismissOnScrollerFinished = true;
+ } else {
+ scrollNestedScrollableChildBackToTop();
+ smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ }
+ }
+ }else {
+ smoothScrollTo(
+ mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ resetTouch();
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ if (mIsDragging) {
+ smoothScrollTo(
+ mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ resetTouch();
+ return true;
+ }
+ }
+
+ return handled;
+ }
+
+ /**
+ * Scroll nested scrollable child back to top if it has been scrolled.
+ */
+ public void scrollNestedScrollableChildBackToTop() {
+ if (isNestedListChildScrolled()) {
+ mNestedListChild.smoothScrollToPosition(0);
+ } else if (isNestedRecyclerChildScrolled()) {
+ mNestedRecyclerChild.smoothScrollToPosition(0);
+ }
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mInitialTouchX = ev.getX(newPointerIndex);
+ mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ }
+ }
+
+ private void resetTouch() {
+ mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+ mIsDragging = false;
+ mOpenOnClick = false;
+ mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
+ mVelocityTracker.clear();
+ }
+
+ private void dismiss() {
+ mRunOnDismissedListener = new RunOnDismissedListener();
+ post(mRunOnDismissedListener);
+ }
+
+ @Override
+ public void computeScroll() {
+ super.computeScroll();
+ if (mScroller.computeScrollOffset()) {
+ final boolean keepGoing = !mScroller.isFinished();
+ performDrag(mScroller.getCurrY() - mCollapseOffset);
+ if (keepGoing) {
+ postInvalidateOnAnimation();
+ } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
+ dismiss();
+ }
+ }
+ }
+
+ private void abortAnimation() {
+ mScroller.abortAnimation();
+ mRunOnDismissedListener = null;
+ mDismissOnScrollerFinished = false;
+ }
+
+ private float performDrag(float dy) {
+ if (getShowAtTop()) {
+ return 0;
+ }
+
+ final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mHeightUsed));
+ if (newPos != mCollapseOffset) {
+ dy = newPos - mCollapseOffset;
+
+ mDragRemainder += dy - (int) dy;
+ if (mDragRemainder >= 1.0f) {
+ mDragRemainder -= 1.0f;
+ dy += 1.0f;
+ } else if (mDragRemainder <= -1.0f) {
+ mDragRemainder += 1.0f;
+ dy -= 1.0f;
+ }
+
+ boolean isIgnoreOffsetLimitSet = false;
+ int ignoreOffsetLimit = 0;
+ View ignoreOffsetLimitView = findIgnoreOffsetLimitView();
+ if (ignoreOffsetLimitView != null) {
+ LayoutParams lp = (LayoutParams) ignoreOffsetLimitView.getLayoutParams();
+ ignoreOffsetLimit = ignoreOffsetLimitView.getBottom() + lp.bottomMargin;
+ isIgnoreOffsetLimitSet = true;
+ }
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == View.GONE) {
+ continue;
+ }
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.ignoreOffset) {
+ child.offsetTopAndBottom((int) dy);
+ } else if (isIgnoreOffsetLimitSet) {
+ int top = child.getTop();
+ int targetTop = Math.max(
+ (int) (ignoreOffsetLimit + lp.topMargin + dy),
+ lp.mFixedTop);
+ if (top != targetTop) {
+ child.offsetTopAndBottom(targetTop - top);
+ }
+ ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
+ }
+ }
+ final boolean isCollapsedOld = mCollapseOffset != 0;
+ mCollapseOffset = newPos;
+ mTopOffset += dy;
+ final boolean isCollapsedNew = newPos != 0;
+ if (isCollapsedOld != isCollapsedNew) {
+ onCollapsedChanged(isCollapsedNew);
+ getMetricsLogger().write(
+ new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED)
+ .setSubtype(isCollapsedNew ? 1 : 0));
+ }
+ onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
+ postInvalidateOnAnimation();
+ return dy;
+ }
+ return 0;
+ }
+
+ private void onCollapsedChanged(boolean isCollapsed) {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+
+ if (mScrollIndicatorDrawable != null) {
+ setWillNotDraw(!isCollapsed);
+ }
+
+ if (mOnCollapsedChangedListener != null) {
+ mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
+ }
+ }
+
+ void dispatchOnDismissed() {
+ if (mOnDismissedListener != null) {
+ mOnDismissedListener.onDismissed();
+ }
+ if (mRunOnDismissedListener != null) {
+ removeCallbacks(mRunOnDismissedListener);
+ mRunOnDismissedListener = null;
+ }
+ }
+
+ private void smoothScrollTo(int yOffset, float velocity) {
+ abortAnimation();
+ final int sy = (int) mCollapseOffset;
+ int dy = yOffset - sy;
+ if (dy == 0) {
+ return;
+ }
+
+ final int height = getHeight();
+ final int halfHeight = height / 2;
+ final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
+ final float distance = halfHeight + halfHeight *
+ distanceInfluenceForSnapDuration(distanceRatio);
+
+ int duration = 0;
+ velocity = Math.abs(velocity);
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ final float pageDelta = (float) Math.abs(dy) / height;
+ duration = (int) ((pageDelta + 1) * 100);
+ }
+ duration = Math.min(duration, 300);
+
+ mScroller.startScroll(0, sy, 0, dy, duration);
+ postInvalidateOnAnimation();
+ }
+
+ private float distanceInfluenceForSnapDuration(float f) {
+ f -= 0.5f; // center the values about 0.
+ f *= 0.3f * Math.PI / 2.0f;
+ return (float) Math.sin(f);
+ }
+
+ /**
+ * Note: this method doesn't take Z into account for overlapping views
+ * since it is only used in contexts where this doesn't affect the outcome.
+ */
+ private View findChildUnder(float x, float y) {
+ return findChildUnder(this, x, y);
+ }
+
+ private static View findChildUnder(ViewGroup parent, float x, float y) {
+ final int childCount = parent.getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = parent.getChildAt(i);
+ if (isChildUnder(child, x, y)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private View findListChildUnder(float x, float y) {
+ View v = findChildUnder(x, y);
+ while (v != null) {
+ x -= v.getX();
+ y -= v.getY();
+ if (v instanceof AbsListView) {
+ // One more after this.
+ return findChildUnder((ViewGroup) v, x, y);
+ }
+ v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
+ }
+ return v;
+ }
+
+ /**
+ * This only checks clipping along the bottom edge.
+ */
+ private boolean isListChildUnderClipped(float x, float y) {
+ final View listChild = findListChildUnder(x, y);
+ return listChild != null && isDescendantClipped(listChild);
+ }
+
+ private boolean isDescendantClipped(View child) {
+ mTempRect.set(0, 0, child.getWidth(), child.getHeight());
+ offsetDescendantRectToMyCoords(child, mTempRect);
+ View directChild;
+ if (child.getParent() == this) {
+ directChild = child;
+ } else {
+ View v = child;
+ ViewParent p = child.getParent();
+ while (p != this) {
+ v = (View) p;
+ p = v.getParent();
+ }
+ directChild = v;
+ }
+
+ // ResolverDrawerLayout lays out vertically in child order;
+ // the next view and forward is what to check against.
+ int clipEdge = getHeight() - getPaddingBottom();
+ final int childCount = getChildCount();
+ for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
+ final View nextChild = getChildAt(i);
+ if (nextChild.getVisibility() == GONE) {
+ continue;
+ }
+ clipEdge = Math.min(clipEdge, nextChild.getTop());
+ }
+ return mTempRect.bottom > clipEdge;
+ }
+
+ private static boolean isChildUnder(View child, float x, float y) {
+ final float left = child.getX();
+ final float top = child.getY();
+ final float right = left + child.getWidth();
+ final float bottom = top + child.getHeight();
+ return x >= left && y >= top && x < right && y < bottom;
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ super.requestChildFocus(child, focused);
+ if (!isInTouchMode() && isDescendantClipped(focused)) {
+ smoothScrollTo(0, 0);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
+ abortAnimation();
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
+ if (target instanceof AbsListView) {
+ mNestedListChild = (AbsListView) target;
+ }
+ if (target instanceof RecyclerView) {
+ mNestedRecyclerChild = (RecyclerView) target;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int axes) {
+ super.onNestedScrollAccepted(child, target, axes);
+ }
+
+ @Override
+ public void onStopNestedScroll(View child) {
+ super.onStopNestedScroll(child);
+ if (mScroller.isFinished()) {
+ smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ if (dyUnconsumed < 0) {
+ performDrag(-dyUnconsumed);
+ }
+ }
+
+ @Override
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+ if (dy > 0) {
+ consumed[1] = (int) -performDrag(-dy);
+ }
+ }
+
+ @Override
+ public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
+ smoothScrollTo(0, velocityY);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
+ if (getShowAtTop()) {
+ if (isDismissable() && velocityY > 0) {
+ abortAnimation();
+ dismiss();
+ } else {
+ smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
+ }
+ } else {
+ if (isDismissable()
+ && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mHeightUsed, velocityY);
+ mDismissOnScrollerFinished = true;
+ } else {
+ smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean performAccessibilityActionCommon(int action) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ case AccessibilityNodeInfo.ACTION_EXPAND:
+ case com.android.internal.R.id.accessibilityActionScrollDown:
+ if (mCollapseOffset != 0) {
+ smoothScrollTo(0, 0);
+ return true;
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_COLLAPSE:
+ if (mCollapseOffset < mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight, 0);
+ return true;
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_DISMISS:
+ if ((mCollapseOffset < mHeightUsed) && isDismissable()) {
+ smoothScrollTo(mHeightUsed, 0);
+ mDismissOnScrollerFinished = true;
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+ if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
+ return true;
+ }
+
+ return performAccessibilityActionCommon(action);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ // Since we support scrolling, make this ViewGroup look like a
+ // ScrollView. This is kind of a hack until we have support for
+ // specifying auto-scroll behavior.
+ return android.widget.ScrollView.class.getName();
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ if (isEnabled()) {
+ if (mCollapseOffset != 0) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityAction.ACTION_EXPAND);
+ info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
+ info.setScrollable(true);
+ }
+ if ((mCollapseOffset < mHeightUsed)
+ && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
+ info.setScrollable(true);
+ }
+ if (mCollapseOffset < mCollapsibleHeight) {
+ info.addAction(AccessibilityAction.ACTION_COLLAPSE);
+ }
+ if (mCollapseOffset < mHeightUsed && isDismissable()) {
+ info.addAction(AccessibilityAction.ACTION_DISMISS);
+ }
+ }
+
+ // This view should never get accessibility focus, but it's interactive
+ // via nested scrolling, so we can't hide it completely.
+ info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
+ // This view should never get accessibility focus.
+ return false;
+ }
+
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+
+ return performAccessibilityActionCommon(action);
+ }
+
+ @Override
+ public void onDrawForeground(Canvas canvas) {
+ if (mScrollIndicatorDrawable != null) {
+ mScrollIndicatorDrawable.draw(canvas);
+ }
+
+ super.onDrawForeground(canvas);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int widthSize = sourceWidth;
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ // Single-use layout; just ignore the mode and use available space.
+ // Clamp to maxWidth.
+ if (mMaxWidth >= 0) {
+ widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight());
+ }
+
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
+
+ // Currently we allot more height than is really needed so that the entirety of the
+ // sheet may be pulled up.
+ // TODO: Restrict the height here to be the right value.
+ int heightUsed = 0;
+
+ // Measure always-show children first.
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.alwaysShow && child.getVisibility() != GONE) {
+ if (lp.maxHeight != -1) {
+ final int remainingHeight = heightSize - heightUsed;
+ measureChildWithMargins(child, widthSpec, 0,
+ MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
+ lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
+ } else {
+ measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
+ }
+ heightUsed += child.getMeasuredHeight();
+ }
+ }
+
+ mAlwaysShowHeight = heightUsed;
+
+ // And now the rest.
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.alwaysShow && child.getVisibility() != GONE) {
+ if (lp.maxHeight != -1) {
+ final int remainingHeight = heightSize - heightUsed;
+ measureChildWithMargins(child, widthSpec, 0,
+ MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
+ lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
+ } else {
+ measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
+ }
+ heightUsed += child.getMeasuredHeight();
+ }
+ }
+
+ mHeightUsed = heightUsed;
+ int oldCollapsibleHeight = updateCollapsibleHeight();
+ updateCollapseOffset(oldCollapsibleHeight, !isDragging());
+
+ if (getShowAtTop()) {
+ mTopOffset = 0;
+ } else {
+ mTopOffset = Math.max(0, heightSize - mHeightUsed) + (int) mCollapseOffset;
+ }
+
+ setMeasuredDimension(sourceWidth, heightSize);
+ }
+
+ private int updateCollapsibleHeight() {
+ final int oldCollapsibleHeight = mCollapsibleHeight;
+ mCollapsibleHeight = Math.max(0, mHeightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
+ return oldCollapsibleHeight;
+ }
+
+ /**
+ * @return The space reserved by views with 'alwaysShow=true'
+ */
+ public int getAlwaysShowHeight() {
+ return mAlwaysShowHeight;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int width = getWidth();
+
+ View indicatorHost = null;
+
+ int ypos = mTopOffset;
+ final int leftEdge = getPaddingLeft();
+ final int rightEdge = width - getPaddingRight();
+ final int widthAvailable = rightEdge - leftEdge;
+
+ boolean isIgnoreOffsetLimitSet = false;
+ int ignoreOffsetLimit = 0;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.hasNestedScrollIndicator) {
+ indicatorHost = child;
+ }
+
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) {
+ if (mIgnoreOffsetTopLimitViewId == child.getId()) {
+ ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
+ isIgnoreOffsetLimitSet = true;
+ }
+ }
+
+ int top = ypos + lp.topMargin;
+ if (lp.ignoreOffset) {
+ if (!isDragging()) {
+ lp.mFixedTop = (int) (top - mCollapseOffset);
+ }
+ if (isIgnoreOffsetLimitSet) {
+ top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset));
+ ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin;
+ } else {
+ top -= mCollapseOffset;
+ }
+ }
+ final int bottom = top + child.getMeasuredHeight();
+
+ final int childWidth = child.getMeasuredWidth();
+ final int left = leftEdge + (widthAvailable - childWidth) / 2;
+ final int right = left + childWidth;
+
+ child.layout(left, top, right, bottom);
+
+ ypos = bottom + lp.bottomMargin;
+ }
+
+ if (mScrollIndicatorDrawable != null) {
+ if (indicatorHost != null) {
+ final int left = indicatorHost.getLeft();
+ final int right = indicatorHost.getRight();
+ final int bottom = indicatorHost.getTop();
+ final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
+ mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
+ setWillNotDraw(!isCollapsed());
+ } else {
+ mScrollIndicatorDrawable = null;
+ setWillNotDraw(true);
+ }
+ }
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ if (p instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) p);
+ } else if (p instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) p);
+ }
+ return new LayoutParams(p);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final SavedState ss = new SavedState(super.onSaveInstanceState());
+ ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
+ ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mOpenOnLayout = ss.open;
+ mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
+ }
+
+ private View findIgnoreOffsetLimitView() {
+ if (mIgnoreOffsetTopLimitViewId == ID_NULL) {
+ return null;
+ }
+ View v = findViewById(mIgnoreOffsetTopLimitViewId);
+ if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) {
+ return v;
+ }
+ return null;
+ }
+
+ public static class LayoutParams extends MarginLayoutParams {
+ public boolean alwaysShow;
+ public boolean ignoreOffset;
+ public boolean hasNestedScrollIndicator;
+ public int maxHeight;
+ int mFixedTop;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ final TypedArray a = c.obtainStyledAttributes(attrs,
+ R.styleable.ResolverDrawerLayout_LayoutParams);
+ alwaysShow = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
+ false);
+ ignoreOffset = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
+ false);
+ hasNestedScrollIndicator = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
+ false);
+ maxHeight = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1);
+ a.recycle();
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ this.alwaysShow = source.alwaysShow;
+ this.ignoreOffset = source.ignoreOffset;
+ this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
+ this.maxHeight = source.maxHeight;
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean open;
+ private int mCollapsibleHeightReserved;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ open = in.readInt() != 0;
+ mCollapsibleHeightReserved = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(open ? 1 : 0);
+ out.writeInt(mCollapsibleHeightReserved);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ /**
+ * Listener for sheet dismissed events.
+ */
+ public interface OnDismissedListener {
+ /**
+ * Callback when the sheet is dismissed by the user.
+ */
+ void onDismissed();
+ }
+
+ /**
+ * Listener for sheet collapsed / expanded events.
+ */
+ public interface OnCollapsedChangedListener {
+ /**
+ * Callback when the sheet is either fully expanded or collapsed.
+ * @param isCollapsed true when collapsed, false when expanded.
+ */
+ void onCollapsedChanged(boolean isCollapsed);
+ }
+
+ private class RunOnDismissedListener implements Runnable {
+ @Override
+ public void run() {
+ dispatchOnDismissed();
+ }
+ }
+
+ private MetricsLogger getMetricsLogger() {
+ if (mMetricsLogger == null) {
+ mMetricsLogger = new MetricsLogger();
+ }
+ return mMetricsLogger;
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/RoundedRectImageView.java b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java
new file mode 100644
index 00000000..8538041b
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2008 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.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import com.android.intentresolver.R;
+
+/**
+ * {@link ImageView} that rounds the corners around the presented image while obeying view padding.
+ */
+public class RoundedRectImageView extends ImageView {
+ private int mRadius = 0;
+ private Path mPath = new Path();
+ private Paint mOverlayPaint = new Paint(0);
+ private Paint mRoundRectPaint = new Paint(0);
+ private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private String mExtraImageCount = null;
+
+ public RoundedRectImageView(Context context) {
+ super(context);
+ }
+
+ public RoundedRectImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public RoundedRectImageView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius);
+
+ mOverlayPaint.setColor(0x99000000);
+ mOverlayPaint.setStyle(Paint.Style.FILL);
+
+ mRoundRectPaint.setColor(context.getResources().getColor(R.color.chooser_row_divider));
+ mRoundRectPaint.setStyle(Paint.Style.STROKE);
+ mRoundRectPaint.setStrokeWidth(context.getResources()
+ .getDimensionPixelSize(R.dimen.chooser_preview_image_border));
+
+ mTextPaint.setColor(Color.WHITE);
+ mTextPaint.setTextSize(context.getResources()
+ .getDimensionPixelSize(R.dimen.chooser_preview_image_font_size));
+ mTextPaint.setTextAlign(Paint.Align.CENTER);
+ }
+
+ private void updatePath(int width, int height) {
+ mPath.reset();
+
+ int imageWidth = width - getPaddingRight() - getPaddingLeft();
+ int imageHeight = height - getPaddingBottom() - getPaddingTop();
+ mPath.addRoundRect(getPaddingLeft(), getPaddingTop(), imageWidth, imageHeight, mRadius,
+ mRadius, Path.Direction.CW);
+ }
+
+ /**
+ * Sets the corner radius on all corners
+ *
+ * param radius 0 for no radius, &gt; 0 for a visible corner radius
+ */
+ public void setRadius(int radius) {
+ mRadius = radius;
+ updatePath(getWidth(), getHeight());
+ }
+
+ /**
+ * Display an overlay with extra image count on 3rd image
+ */
+ public void setExtraImageCount(int count) {
+ if (count > 0) {
+ this.mExtraImageCount = "+" + count;
+ } else {
+ this.mExtraImageCount = null;
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
+ updatePath(width, height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mRadius != 0) {
+ canvas.clipPath(mPath);
+ }
+
+ super.onDraw(canvas);
+
+ int x = getPaddingLeft();
+ int y = getPaddingRight();
+ int width = getWidth() - getPaddingRight() - getPaddingLeft();
+ int height = getHeight() - getPaddingBottom() - getPaddingTop();
+ if (mExtraImageCount != null) {
+ canvas.drawRect(x, y, width, height, mOverlayPaint);
+
+ int xPos = canvas.getWidth() / 2;
+ int yPos = (int) ((canvas.getHeight() / 2.0f)
+ - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f));
+
+ canvas.drawText(mExtraImageCount, xPos, yPos, mTextPaint);
+ }
+
+ canvas.drawRoundRect(x, y, width, height, mRadius, mRadius, mRoundRectPaint);
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt
new file mode 100644
index 00000000..a941b97a
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.intentresolver.widget
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.intentresolver.R
+
+class ScrollableActionRow : RecyclerView, ActionRow {
+ constructor(context: Context) : this(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr) {
+ layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ adapter = Adapter(context)
+ }
+
+ private val actionsAdapter get() = adapter as Adapter
+
+ override fun setActions(actions: List<ActionRow.Action>) {
+ actionsAdapter.setActions(actions)
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ super.onLayout(changed, l, t, r, b)
+ setOverScrollMode(
+ if (areAllChildrenVisible) View.OVER_SCROLL_NEVER else View.OVER_SCROLL_ALWAYS
+ )
+ }
+
+ private val areAllChildrenVisible: Boolean
+ get() {
+ val count = getChildCount()
+ if (count == 0) return true
+ val first = getChildAt(0)
+ val last = getChildAt(count - 1)
+ return getChildAdapterPosition(first) == 0
+ && getChildAdapterPosition(last) == actionsAdapter.itemCount - 1
+ && isFullyVisible(first)
+ && isFullyVisible(last)
+ }
+
+ private fun isFullyVisible(view: View): Boolean =
+ view.left >= paddingLeft && view.right <= width - paddingRight
+
+ private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() {
+ private val iconSize: Int =
+ context.resources.getDimensionPixelSize(R.dimen.chooser_action_view_icon_size)
+ private val itemLayout = R.layout.chooser_action_view
+ private var actions: List<ActionRow.Action> = emptyList()
+
+ override fun onCreateViewHolder(parent: ViewGroup, type: Int): ViewHolder =
+ ViewHolder(
+ LayoutInflater.from(context).inflate(itemLayout, null) as TextView,
+ iconSize
+ )
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(actions[position])
+ }
+
+ override fun getItemCount() = actions.size
+
+ override fun onViewRecycled(holder: ViewHolder) {
+ holder.unbind()
+ }
+
+ override fun onFailedToRecycleView(holder: ViewHolder): Boolean {
+ holder.unbind()
+ return super.onFailedToRecycleView(holder)
+ }
+
+ fun setActions(actions: List<ActionRow.Action>) {
+ this.actions = ArrayList(actions)
+ notifyDataSetChanged()
+ }
+ }
+
+ private class ViewHolder(
+ private val view: TextView, private val iconSize: Int
+ ) : RecyclerView.ViewHolder(view) {
+
+ fun bind(action: ActionRow.Action) {
+ if (action.icon != null) {
+ action.icon.setBounds(0, 0, iconSize, iconSize)
+ // some drawables (edit) does not gets tinted when set to the top of the text
+ // with TextView#setCompoundDrawableRelative
+ view.setCompoundDrawablesRelative(null, action.icon, null, null)
+ }
+ view.text = action.label ?: ""
+ view.setOnClickListener {
+ action.onClicked.run()
+ }
+ view.id = action.id
+ }
+
+ fun unbind() {
+ view.setOnClickListener(null)
+ }
+
+ private fun tintIcon(drawable: Drawable, view: TextView) {
+ val tintList = view.compoundDrawableTintList ?: return
+ drawable.setTintList(tintList)
+ view.compoundDrawableTintMode?.let { drawable.setTintMode(it) }
+ view.compoundDrawableTintBlendMode?.let { drawable.setTintBlendMode(it) }
+ }
+ }
+}