diff options
Diffstat (limited to 'java/src')
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, > 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, > 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) } +        } +    } +}  |