summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java17
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java195
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityReEnabler.kt39
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java12
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java125
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java17
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java2
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java46
-rw-r--r--java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt51
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt128
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt17
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java53
-rw-r--r--java/src/com/android/intentresolver/flags/Flags.kt7
-rw-r--r--java/src/com/android/intentresolver/grid/ChooserGridAdapter.java6
-rw-r--r--java/src/com/android/intentresolver/icons/LoadIconTask.java4
-rw-r--r--java/src/com/android/intentresolver/logging/EventLog.java (renamed from java/src/com/android/intentresolver/ChooserActivityLogger.java)11
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java16
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java8
-rw-r--r--java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java10
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt3
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt261
21 files changed, 522 insertions, 506 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index 06c7e8d7..a54e8c62 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -37,6 +37,7 @@ import android.view.View;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.widget.ActionRow;
import com.android.internal.annotations.VisibleForTesting;
@@ -97,7 +98,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
private final @Nullable ChooserAction mModifyShareAction;
private final Consumer<Boolean> mExcludeSharedTextAction;
private final Consumer</* @Nullable */ Integer> mFinishCallback;
- private final ChooserActivityLogger mLogger;
+ private final EventLog mLogger;
/**
* @param context
@@ -116,7 +117,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Context context,
ChooserRequestParameters chooserRequest,
ChooserIntegratedDeviceComponents integratedDeviceComponents,
- ChooserActivityLogger logger,
+ EventLog logger,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
@@ -152,7 +153,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
List<ChooserAction> customActions,
@Nullable ChooserAction modifyShareAction,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
- ChooserActivityLogger logger,
+ EventLog logger,
Consumer</* @Nullable */ Integer> finishCallback) {
mContext = context;
mCopyButtonRunnable = copyButtonRunnable;
@@ -208,7 +209,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
mModifyShareAction,
mFinishCallback,
() -> {
- mLogger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE);
+ mLogger.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
});
}
@@ -232,7 +233,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Intent targetIntent,
String referrerPackageName,
Consumer<Integer> finishCallback,
- ChooserActivityLogger logger) {
+ EventLog logger) {
final ClipData clipData;
try {
clipData = extractTextToCopy(targetIntent);
@@ -248,7 +249,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
- logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
+ logger.logActionSelected(EventLog.SELECTION_TYPE_COPY);
finishCallback.accept(Activity.RESULT_OK);
};
}
@@ -327,10 +328,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
TargetInfo editSharingTarget,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
- ChooserActivityLogger logger) {
+ EventLog logger) {
return () -> {
// Log share completion via edit.
- logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_EDIT);
+ logger.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
View firstImageView = null;
try {
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 63ac6435..b27f054e 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -27,7 +27,6 @@ import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_S
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
import android.annotation.IntDef;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
@@ -66,9 +65,6 @@ import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
-import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
-import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import androidx.annotation.MainThread;
@@ -92,6 +88,7 @@ import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.icons.DefaultTargetDataLoader;
import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.measurements.Tracer;
import com.android.intentresolver.model.AbstractResolverComparator;
import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
@@ -191,7 +188,7 @@ public class ChooserActivity extends ResolverActivity implements
private boolean mShouldDisplayLandscape;
// statsd logger wrapper
- protected ChooserActivityLogger mChooserActivityLogger;
+ protected EventLog mEventLog;
private long mChooserShownTime;
protected boolean mIsSuccessfullySelected;
@@ -224,6 +221,13 @@ public class ChooserActivity extends ResolverActivity implements
private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
private boolean mExcludeSharedText = false;
+ /**
+ * When we intend to finish the activity with a shared element transition, we can't immediately
+ * finish() when the transition is invoked, as the receiving end may not be able to start the
+ * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop
+ * in order to wait for the transition to begin.
+ */
+ private boolean mFinishWhenStopped = false;
public ChooserActivity() {}
@@ -233,7 +237,7 @@ public class ChooserActivity extends ResolverActivity implements
final long intentReceivedTime = System.currentTimeMillis();
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
- getChooserActivityLogger().logSharesheetTriggered();
+ getEventLog().logSharesheetTriggered();
mFeatureFlagRepository = createFeatureFlagRepository();
mIntegratedDeviceComponents = getIntegratedDeviceComponents();
@@ -283,10 +287,6 @@ public class ChooserActivity extends ResolverActivity implements
mEnterTransitionAnimationDelegate,
new HeadlineGeneratorImpl(this));
- setAdditionalTargets(mChooserRequest.getAdditionalTargets());
-
- setSafeForwardingMode(true);
-
mPinnedSharedPrefs = getPinnedSharedPrefs(this);
mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
@@ -304,16 +304,18 @@ public class ChooserActivity extends ResolverActivity implements
super.onCreate(
savedInstanceState,
mChooserRequest.getTargetIntent(),
+ mChooserRequest.getAdditionalTargets(),
mChooserRequest.getTitle(),
mChooserRequest.getDefaultTitleResource(),
mChooserRequest.getInitialIntents(),
- /* rList: List<ResolveInfo> = */ null,
- /* supportsAlwaysUseOption = */ false,
- new DefaultTargetDataLoader(this, getLifecycle(), false));
+ /* resolutionList= */ null,
+ /* supportsAlwaysUseOption= */ false,
+ new DefaultTargetDataLoader(this, getLifecycle(), false),
+ /* safeForwardingMode= */ true);
mChooserShownTime = System.currentTimeMillis();
final long systemCost = mChooserShownTime - intentReceivedTime;
- getChooserActivityLogger().logChooserActivityShown(
+ getEventLog().logChooserActivityShown(
isWorkProfile(), mChooserRequest.getTargetType(), systemCost);
if (mResolverDrawerLayout != null) {
@@ -322,7 +324,7 @@ public class ChooserActivity extends ResolverActivity implements
mResolverDrawerLayout.setOnCollapsedChangedListener(
isCollapsed -> {
mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed);
- getChooserActivityLogger().logSharesheetExpansionChanged(isCollapsed);
+ getEventLog().logSharesheetExpansionChanged(isCollapsed);
});
}
@@ -330,7 +332,7 @@ public class ChooserActivity extends ResolverActivity implements
Log.d(TAG, "System Time Cost is " + systemCost);
}
- getChooserActivityLogger().logShareStarted(
+ getEventLog().logShareStarted(
getReferrerPackageName(),
mChooserRequest.getTargetType(),
mChooserRequest.getCallerChooserTargets().size(),
@@ -549,7 +551,7 @@ public class ChooserActivity extends ResolverActivity implements
if (shouldShowStickyContentPreview()
|| mChooserMultiProfilePagerAdapter
.getCurrentRootAdapter().getSystemRowCount() != 0) {
- getChooserActivityLogger().logActionShareWithPreview(
+ getEventLog().logActionShareWithPreview(
mChooserContentPreviewUi.getPreferredContentPreview());
}
return postRebuildListInternal(rebuildCompleted);
@@ -614,8 +616,7 @@ public class ChooserActivity extends ResolverActivity implements
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
- maybeCancelFinishAnimation();
-
+ mFinishWhenStopped = false;
mRefinementManager.onActivityResume();
}
@@ -716,7 +717,8 @@ public class ChooserActivity extends ResolverActivity implements
super.onStop();
mRefinementManager.onActivityStop(isChangingConfigurations());
- if (maybeCancelFinishAnimation()) {
+ if (mFinishWhenStopped) {
+ mFinishWhenStopped = false;
finish();
}
}
@@ -848,9 +850,7 @@ public class ChooserActivity extends ResolverActivity implements
targetList,
// Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be
// resolved correctly within the same tab.
- getResolveInfoUserHandle(
- targetInfo.getResolveInfo(),
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle()),
+ targetInfo.getResolveInfo().userHandle,
shortcutIdKey,
shortcutTitle,
isShortcutPinned,
@@ -883,7 +883,7 @@ public class ChooserActivity extends ResolverActivity implements
final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
- if (targetInfo.isMultiDisplayResolveInfo()) {
+ if ((targetInfo != null) && targetInfo.isMultiDisplayResolveInfo()) {
MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
if (!mti.hasSelected()) {
// Add userHandle based badge to the stackedAppDialogBox.
@@ -891,20 +891,28 @@ public class ChooserActivity extends ResolverActivity implements
getSupportFragmentManager(),
mti,
which,
- getResolveInfoUserHandle(
- targetInfo.getResolveInfo(),
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle()));
+ targetInfo.getResolveInfo().userHandle);
return;
}
}
super.startSelected(which, always, filtered);
- if (currentListAdapter.getCount() > 0) {
+ // TODO: both of the conditions around this switch logic *should* be redundant, and
+ // can be removed if certain invariants can be guaranteed. In particular, it seems
+ // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably*
+ // expected to be null only at out-of-bounds indexes where `getPositionTargetType()`
+ // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't
+ // need to null-check targetInfo. We only need the null check if it's possible that
+ // the ChooserListAdapter contains null elements "in the middle" of its list data,
+ // such that they're classified as belonging to one of the real target types. That
+ // should probably never happen. But why would this method ever be invoked with a
+ // null target at all? Even an out-of-bounds index should never be "selected"...
+ if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) {
switch (currentListAdapter.getPositionTargetType(which)) {
case ChooserListAdapter.TARGET_SERVICE:
- getChooserActivityLogger().logShareTargetSelected(
- ChooserActivityLogger.SELECTION_TYPE_SERVICE,
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_SERVICE,
targetInfo.getResolveInfo().activityInfo.processName,
which,
/* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
@@ -917,8 +925,8 @@ public class ChooserActivity extends ResolverActivity implements
return;
case ChooserListAdapter.TARGET_CALLER:
case ChooserListAdapter.TARGET_STANDARD:
- getChooserActivityLogger().logShareTargetSelected(
- ChooserActivityLogger.SELECTION_TYPE_APP,
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_APP,
targetInfo.getResolveInfo().activityInfo.processName,
(which - currentListAdapter.getSurfacedTargetInfo().size()),
/* directTargetAlsoRanked= */ -1,
@@ -934,8 +942,8 @@ public class ChooserActivity extends ResolverActivity implements
// 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(
- ChooserActivityLogger.SELECTION_TYPE_STANDARD,
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_STANDARD,
targetInfo.getResolveInfo().activityInfo.processName,
/* value= */ -1,
/* directTargetAlsoRanked= */ -1,
@@ -987,7 +995,7 @@ public class ChooserActivity extends ResolverActivity implements
if (profileRecord == null) {
return;
}
- getChooserActivityLogger().logDirectShareTargetReceived(
+ getEventLog().logDirectShareTargetReceived(
MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
(int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime));
}
@@ -1111,11 +1119,7 @@ public class ChooserActivity extends ResolverActivity implements
// Adding two stage comparator, first stage compares using displayLabel, next stage
// compares using resolveInfo.userHandle
mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator)
- .thenComparingInt(displayResolveInfo ->
- getResolveInfoUserHandle(
- displayResolveInfo.getResolveInfo(),
- // TODO: User resolveInfo.userHandle, once its available.
- UserHandle.SYSTEM).getIdentifier());
+ .thenComparingInt(target -> target.getResolveInfo().userHandle.getIdentifier());
}
@Override
@@ -1125,11 +1129,11 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- protected ChooserActivityLogger getChooserActivityLogger() {
- if (mChooserActivityLogger == null) {
- mChooserActivityLogger = new ChooserActivityLogger();
+ protected EventLog getEventLog() {
+ if (mEventLog == null) {
+ mEventLog = new EventLog();
}
- return mChooserActivityLogger;
+ return mEventLog;
}
public class ChooserListController extends ResolverListController {
@@ -1255,7 +1259,7 @@ public class ChooserActivity extends ResolverActivity implements
targetIntent,
this,
context.getPackageManager(),
- getChooserActivityLogger(),
+ getEventLog(),
chooserRequest,
maxTargetsPerRow,
initialIntentsUserSpace,
@@ -1279,7 +1283,7 @@ public class ChooserActivity extends ResolverActivity implements
AbstractResolverComparator resolverComparator;
if (appPredictor != null) {
resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
- getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger(),
+ getReferrerPackageName(), appPredictor, userHandle, getEventLog(),
getIntegratedDeviceComponents().getNearbySharingComponent());
} else {
resolverComparator =
@@ -1288,7 +1292,7 @@ public class ChooserActivity extends ResolverActivity implements
getTargetIntent(),
getReferrerPackageName(),
null,
- getChooserActivityLogger(),
+ getEventLog(),
getResolverRankerServiceUserHandleList(userHandle),
getIntegratedDeviceComponents().getNearbySharingComponent());
}
@@ -1313,7 +1317,7 @@ public class ChooserActivity extends ResolverActivity implements
this,
mChooserRequest,
mIntegratedDeviceComponents,
- getChooserActivityLogger(),
+ getEventLog(),
(isExcluded) -> mExcludeSharedText = isExcluded,
this::getFirstVisibleImgPreviewView,
new ChooserActionFactory.ActionActivityStarter() {
@@ -1330,7 +1334,10 @@ public class ChooserActivity extends ResolverActivity implements
ChooserActivity.this, sharedElement, sharedElementName);
safelyStartActivityAsUser(
targetInfo, getPersonalProfileUserHandle(), options.toBundle());
- startFinishAnimation();
+ // Can't finish right away because the shared element transition may not
+ // be ready to start.
+ mFinishWhenStopped = true;
+
}
},
(status) -> {
@@ -1528,7 +1535,7 @@ public class ChooserActivity extends ResolverActivity implements
Log.d(TAG, "app target loading time " + duration + " ms");
}
addCallerChooserTargets();
- getChooserActivityLogger().logSharesheetAppLoadComplete();
+ getEventLog().logSharesheetAppLoadComplete();
maybeQueryAdditionalPostProcessingTargets(chooserListAdapter);
mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
}
@@ -1575,7 +1582,7 @@ public class ChooserActivity extends ResolverActivity implements
}
logDirectShareTargetReceived(userHandle);
sendVoiceChoicesIfNeeded();
- getChooserActivityLogger().logSharesheetDirectLoadComplete();
+ getEventLog().logSharesheetDirectLoadComplete();
}
private void setupScrollListener() {
@@ -1715,25 +1722,6 @@ public class ChooserActivity extends ResolverActivity implements
contentPreviewContainer.setVisibility(View.GONE);
}
- private void startFinishAnimation() {
- View rootView = findRootView();
- if (rootView != null) {
- rootView.startAnimation(new FinishAnimation(this, rootView));
- }
- }
-
- private boolean maybeCancelFinishAnimation() {
- View rootView = findRootView();
- Animation animation = (rootView == null) ? null : rootView.getAnimation();
- if (animation instanceof FinishAnimation) {
- boolean hasEnded = animation.hasEnded();
- animation.cancel();
- rootView.clearAnimation();
- return !hasEnded;
- }
- return false;
- }
-
private View findRootView() {
if (mContentView == null) {
mContentView = findViewById(android.R.id.content);
@@ -1814,74 +1802,9 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- /**
- * 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(@NonNull Activity activity, @NonNull View rootView) {
- super(rootView.getAlpha(), 0.0f);
- mActivity = activity;
- mRootView = rootView;
- mFromAlpha = rootView.getAlpha();
- setInterpolator(new LinearInterpolator());
- long duration = activity.getWindow().getTransitionBackgroundFadeDuration();
- setDuration(duration);
- // The scene transition animation looks better when it's not overlapped with this
- // fade-out animation thus the delay.
- // It is most likely that the image editor will cause this activity to stop and this
- // animation will be cancelled in the background without running (i.e. we'll animate
- // only when this activity remains partially visible after the image editor launch).
- setStartOffset(duration);
- super.setAnimationListener(this);
- }
-
- @Override
- public void setAnimationListener(AnimationListener listener) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void cancel() {
- if (mRootView != null) {
- mRootView.setAlpha(mFromAlpha);
- }
- cleanup();
- super.cancel();
- }
-
- @Override
- public void onAnimationStart(Animation animation) {
- }
-
- @Override
- public void onAnimationEnd(Animation animation) {
- Activity activity = mActivity;
- cleanup();
- if (activity != null) {
- activity.finish();
- }
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) {
- }
-
- private void cleanup() {
- mActivity = null;
- mRootView = null;
- }
- }
-
@Override
protected void maybeLogProfileChange() {
- getChooserActivityLogger().logSharesheetProfileChanged();
+ getEventLog().logSharesheetProfileChanged();
}
private static class ProfileRecord {
diff --git a/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt b/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt
deleted file mode 100644
index 3236c1be..00000000
--- a/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.android.intentresolver
-
-import android.content.BroadcastReceiver
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-
-/**
- * Ensures that the unbundled version of [ChooserActivity] does not get stuck in a disabled state.
- */
-class ChooserActivityReEnabler : BroadcastReceiver() {
-
- override fun onReceive(context: Context, intent: Intent) {
- if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
- context.packageManager.setComponentEnabledSetting(
- CHOOSER_COMPONENT,
- PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
- /* flags = */ 0,
- )
-
- // This only needs to be run once, so we disable ourself to avoid additional startup
- // process on future boots
- context.packageManager.setComponentEnabledSetting(
- SELF_COMPONENT,
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
- /* flags = */ 0,
- )
- }
- }
-
- companion object {
- private const val CHOOSER_PACKAGE = "com.android.intentresolver"
- private val CHOOSER_COMPONENT =
- ComponentName(CHOOSER_PACKAGE, "$CHOOSER_PACKAGE.ChooserActivity")
- private val SELF_COMPONENT =
- ComponentName(CHOOSER_PACKAGE, "$CHOOSER_PACKAGE.ChooserActivityReEnabler")
- }
-}
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index b1fa16b0..e6d6dbf4 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -49,6 +49,7 @@ import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.logging.EventLog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -80,7 +81,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
private final ChooserRequestParameters mChooserRequest;
private final int mMaxRankedTargets;
- private final ChooserActivityLogger mChooserActivityLogger;
+ private final EventLog mEventLog;
private final Set<TargetInfo> mRequestedIcons = new HashSet<>();
@@ -139,7 +140,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
Intent targetIntent,
ResolverListCommunicator resolverListCommunicator,
PackageManager packageManager,
- ChooserActivityLogger chooserActivityLogger,
+ EventLog eventLog,
ChooserRequestParameters chooserRequest,
int maxRankedTargets,
UserHandle initialIntentsUserSpace,
@@ -165,7 +166,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);
mTargetDataLoader = targetDataLoader;
createPlaceHolders();
- mChooserActivityLogger = chooserActivityLogger;
+ mEventLog = eventLog;
mShortcutSelectionLogic = new ShortcutSelectionLogic(
context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp),
DeviceConfig.getBoolean(
@@ -384,8 +385,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
.collect(Collectors.groupingBy(target ->
target.getResolvedComponentName().getPackageName()
+ "#" + target.getDisplayLabel()
- + '#' + ResolverActivity.getResolveInfoUserHandle(
- target.getResolveInfo(), getUserHandle()).getIdentifier()
+ + '#' + target.getResolveInfo().userHandle.getIdentifier()
))
.values()
.stream()
@@ -634,7 +634,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
mServiceTargets.removeIf(o -> o.isPlaceHolderTargetInfo());
if (mServiceTargets.isEmpty()) {
mServiceTargets.add(NotSelectableTargetInfo.newEmptyTargetInfo());
- mChooserActivityLogger.logSharesheetEmptyDirectShareRow();
+ mEventLog.logSharesheetEmptyDirectShareRow();
}
notifyDataSetChanged();
}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 57871532..35c7e897 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -27,6 +27,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERS
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
+import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
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;
@@ -119,6 +120,7 @@ import com.android.internal.util.LatencyTracker;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@@ -143,7 +145,14 @@ public class ResolverActivity extends FragmentActivity implements
mIsIntentPicker = isIntentPicker;
}
+ /**
+ * Whether to enable a launch mode that is safe to use when forwarding intents received from
+ * applications and running in system processes. This mode uses Activity.startActivityAsCaller
+ * instead of the normal Activity.startActivity for launching the activity selected
+ * by the user.
+ */
private boolean mSafeForwardingMode;
+
private Button mAlwaysButton;
private Button mOnceButton;
protected View mProfileView;
@@ -332,38 +341,55 @@ public class ResolverActivity extends FragmentActivity implements
mResolvingHome = true;
}
- setSafeForwardingMode(true);
-
- onCreate(savedInstanceState, intent, null, 0, null, null, true, createIconLoader());
+ onCreate(
+ savedInstanceState,
+ intent,
+ /* additionalTargets= */ null,
+ /* title= */ null,
+ /* defaultTitleRes= */ 0,
+ /* initialIntents= */ null,
+ /* resolutionList= */ null,
+ /* supportsAlwaysUseOption= */ true,
+ createIconLoader(),
+ /* safeForwardingMode= */ true);
}
/**
* Compatibility version for other bundled services that use this overload without
* a default title resource
*/
- protected void onCreate(Bundle savedInstanceState, Intent intent,
- CharSequence title, Intent[] initialIntents,
- List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
+ protected void onCreate(
+ Bundle savedInstanceState,
+ Intent intent,
+ CharSequence title,
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean supportsAlwaysUseOption,
+ boolean safeForwardingMode) {
onCreate(
savedInstanceState,
intent,
+ null,
title,
0,
initialIntents,
- rList,
+ resolutionList,
supportsAlwaysUseOption,
- createIconLoader());
+ createIconLoader(),
+ safeForwardingMode);
}
protected void onCreate(
Bundle savedInstanceState,
Intent intent,
+ Intent[] additionalTargets,
CharSequence title,
int defaultTitleRes,
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean supportsAlwaysUseOption,
- TargetDataLoader targetDataLoader) {
+ TargetDataLoader targetDataLoader,
+ boolean safeForwardingMode) {
setTheme(appliedThemeResId());
super.onCreate(savedInstanceState);
@@ -381,12 +407,17 @@ public class ResolverActivity extends FragmentActivity implements
mReferrerPackage = getReferrerPackageName();
- // Add our initial intent as the first item, regardless of what else has already been added.
+ // The initial intent must come before any other targets that are to be added.
mIntents.add(0, new Intent(intent));
+ if (additionalTargets != null) {
+ Collections.addAll(mIntents, additionalTargets);
+ }
+
mTitle = title;
mDefaultTitleResId = defaultTitleRes;
mSupportsAlwaysUseOption = supportsAlwaysUseOption;
+ mSafeForwardingMode = safeForwardingMode;
// 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
@@ -399,7 +430,7 @@ public class ResolverActivity extends FragmentActivity implements
boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction()
&& !shouldShowTabs() && !hasCloneProfile();
mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
- initialIntents, rList, filterLastUsed, targetDataLoader);
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
if (configureContentView(targetDataLoader)) {
return;
}
@@ -455,17 +486,17 @@ public class ResolverActivity extends FragmentActivity implements
protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean filterLastUsed,
TargetDataLoader targetDataLoader) {
AbstractMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
if (shouldShowTabs()) {
resolverMultiProfilePagerAdapter =
createResolverMultiProfilePagerAdapterForTwoProfiles(
- initialIntents, rList, filterLastUsed, targetDataLoader);
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
} else {
resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile(
- initialIntents, rList, filterLastUsed, targetDataLoader);
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
}
return resolverMultiProfilePagerAdapter;
}
@@ -1043,7 +1074,7 @@ public class ResolverActivity extends FragmentActivity implements
Context context,
List<Intent> payloadIntents,
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean filterLastUsed,
UserHandle userHandle,
TargetDataLoader targetDataLoader) {
@@ -1054,7 +1085,7 @@ public class ResolverActivity extends FragmentActivity implements
context,
payloadIntents,
initialIntents,
- rList,
+ resolutionList,
filterLastUsed,
createListController(userHandle),
userHandle,
@@ -1127,6 +1158,12 @@ public class ResolverActivity extends FragmentActivity implements
// flag set, we are now losing it. That should be a very rare case
// and we can live with this.
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+ // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate
+ // side, which means we want to open the target app on the same side as ResolverActivity.
+ if ((intent.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
+ intent.setFlags(intent.getFlags() & ~FLAG_ACTIVITY_LAUNCH_ADJACENT);
+ }
return intent;
}
@@ -1142,14 +1179,14 @@ public class ResolverActivity extends FragmentActivity implements
private ResolverMultiProfilePagerAdapter
createResolverMultiProfilePagerAdapterForOneProfile(
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean filterLastUsed,
TargetDataLoader targetDataLoader) {
ResolverListAdapter adapter = createResolverListAdapter(
/* context */ this,
/* payloadIntents */ mIntents,
initialIntents,
- rList,
+ resolutionList,
filterLastUsed,
/* userHandle */ getPersonalProfileUserHandle(),
targetDataLoader);
@@ -1170,7 +1207,7 @@ public class ResolverActivity extends FragmentActivity implements
private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean filterLastUsed,
TargetDataLoader targetDataLoader) {
// In the edge case when we have 0 apps in the current profile and >1 apps in the other,
@@ -1197,7 +1234,7 @@ public class ResolverActivity extends FragmentActivity implements
/* context */ this,
/* payloadIntents */ mIntents,
selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
- rList,
+ resolutionList,
(filterLastUsed && UserHandle.myUserId()
== getPersonalProfileUserHandle().getIdentifier()),
/* userHandle */ getPersonalProfileUserHandle(),
@@ -1207,7 +1244,7 @@ public class ResolverActivity extends FragmentActivity implements
/* context */ this,
/* payloadIntents */ mIntents,
selectedProfile == PROFILE_WORK ? initialIntents : null,
- rList,
+ resolutionList,
(filterLastUsed && UserHandle.myUserId()
== workProfileUserHandle.getIdentifier()),
/* userHandle */ workProfileUserHandle,
@@ -1365,14 +1402,6 @@ public class ResolverActivity extends FragmentActivity implements
return new Option(target.getDisplayLabel(), index);
}
- protected final void setAdditionalTargets(Intent[] intents) {
- if (intents != null) {
- for (Intent intent : intents) {
- mIntents.add(intent);
- }
- }
- }
-
public final Intent getTargetIntent() {
return mIntents.isEmpty() ? null : mIntents.get(0);
}
@@ -1433,22 +1462,6 @@ public class ResolverActivity extends FragmentActivity implements
() -> getString(R.string.forward_intent_to_work));
}
- /**
- * Turn on launch mode that is safe to use when forwarding intents received from
- * applications and running in system processes. This mode uses Activity.startActivityAsCaller
- * instead of the normal Activity.startActivity for launching the activity selected
- * by the user.
- *
- * <p>This mode is set to true by default if the activity is initialized through
- * {@link #onCreate(android.os.Bundle)}. If a subclass calls one of the other onCreate
- * methods, it is set to false by default. You must set it before calling one of the
- * more detailed onCreate methods, so that it will be set correctly in the case where
- * there is only one intent to resolve and it is thus started immediately.</p>
- */
- public final void setSafeForwardingMode(boolean safeForwarding) {
- mSafeForwardingMode = safeForwarding;
- }
-
protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
final ActionTitle title = mResolvingHome
? ActionTitle.HOME
@@ -1649,10 +1662,9 @@ public class ResolverActivity extends FragmentActivity implements
/** Start the activity specified by the {@link TargetInfo}.*/
public final void safelyStartActivity(TargetInfo cti) {
// In case cloned apps are present, we would want to start those apps in cloned user
- // space, which will not be same as adaptor's userHandle. resolveInfo.userHandle
+ // space, which will not be same as the adapter's userHandle. resolveInfo.userHandle
// identifies the correct user space in such cases.
- UserHandle activityUserHandle = getResolveInfoUserHandle(
- cti.getResolveInfo(), mMultiProfilePagerAdapter.getCurrentUserHandle());
+ UserHandle activityUserHandle = cti.getResolveInfo().userHandle;
safelyStartActivityAsUser(cti, activityUserHandle, null);
}
@@ -2267,11 +2279,7 @@ public class ResolverActivity extends FragmentActivity implements
&& Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName)
// Comparing against resolveInfo.userHandle in case cloned apps are present,
// as they will have the same activityInfo.
- && Objects.equals(
- getResolveInfoUserHandle(lhs,
- mMultiProfilePagerAdapter.getActiveListAdapter().getUserHandle()),
- getResolveInfoUserHandle(rhs,
- mMultiProfilePagerAdapter.getActiveListAdapter().getUserHandle()));
+ && Objects.equals(lhs.userHandle, rhs.userHandle);
}
private boolean inactiveListAdapterHasItems() {
@@ -2409,13 +2417,4 @@ public class ResolverActivity extends FragmentActivity implements
}
return userList;
}
-
- /**
- * This function is temporary in nature, and its usages will be replaced with just
- * resolveInfo.userHandle, once it is available, once sharesheet is stable.
- */
- public static UserHandle getResolveInfoUserHandle(ResolveInfo resolveInfo,
- UserHandle predictedHandle) {
- return resolveInfo.userHandle;
- }
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
index e8367c4e..d279f11f 100644
--- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -16,6 +16,8 @@
package com.android.intentresolver.contentpreview;
+import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
+
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;
@@ -150,26 +152,31 @@ public final class ChooserContentPreviewUi {
isSingleImageShare,
previewData.getUriCount(),
targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT),
+ targetIntent.getType(),
actionFactory,
imageLoader,
typeClassifier,
headlineGenerator);
if (previewData.getUriCount() > 0) {
- previewData.getFileMetadataForImagePreview(
- mLifecycle, previewUi::updatePreviewMetadata);
+ JavaFlowHelper.collectToList(
+ getCoroutineScope(mLifecycle),
+ previewData.getImagePreviewFileInfoFlow(),
+ previewUi::updatePreviewMetadata);
}
return previewUi;
}
- UnifiedContentPreviewUi unifiedContentPreviewUi = new UnifiedContentPreviewUi(
+ return new UnifiedContentPreviewUi(
+ getCoroutineScope(mLifecycle),
isSingleImageShare,
+ targetIntent.getType(),
actionFactory,
imageLoader,
typeClassifier,
transitionElementStatusCallback,
+ previewData.getImagePreviewFileInfoFlow(),
+ previewData.getUriCount(),
headlineGenerator);
- previewData.getFileMetadataForImagePreview(mLifecycle, unifiedContentPreviewUi::setFiles);
- return unifiedContentPreviewUi;
}
public int getPreferredContentPreview() {
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
index 07071236..2d81794e 100644
--- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
@@ -85,7 +85,7 @@ abstract class ContentPreviewUi {
}
}
- protected static ScrollableImagePreviewView.PreviewType getPreviewType(
+ static ScrollableImagePreviewView.PreviewType getPreviewType(
MimeTypeClassifier typeClassifier, String mimeType) {
if (mimeType == null) {
return ScrollableImagePreviewView.PreviewType.File;
diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
index 35990990..6e1212e9 100644
--- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
@@ -49,6 +49,8 @@ import java.util.function.Consumer;
*/
class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
private final Lifecycle mLifecycle;
+ @Nullable
+ private final String mIntentMimeType;
private final CharSequence mText;
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final ImageLoader mImageLoader;
@@ -70,15 +72,17 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
boolean isSingleImage,
int fileCount,
CharSequence text,
+ @Nullable String intentMimeType,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
MimeTypeClassifier typeClassifier,
HeadlineGenerator headlineGenerator) {
- mLifecycle = lifecycle;
if (isSingleImage && fileCount != 1) {
throw new IllegalArgumentException(
"fileCount = " + fileCount + " and isSingleImage = true");
}
+ mLifecycle = lifecycle;
+ mIntentMimeType = intentMimeType;
mFileCount = fileCount;
mIsSingleImage = isSingleImage;
mText = text;
@@ -127,18 +131,25 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
List<ActionRow.Action> actions = mActionFactory.createCustomActions();
actionRow.setActions(actions);
+ if (!mIsSingleImage) {
+ mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE);
+ }
+ prepareTextPreview(mContentPreviewView, mActionFactory);
if (mIsMetadataUpdated) {
updateUiWithMetadata(mContentPreviewView);
- } else if (!mIsSingleImage) {
- mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE);
+ } else {
+ updateHeadline(
+ mContentPreviewView,
+ mFileCount,
+ mTypeClassifier.isImageType(mIntentMimeType),
+ mTypeClassifier.isVideoType(mIntentMimeType));
}
return mContentPreviewView;
}
private void updateUiWithMetadata(ViewGroup contentPreviewView) {
- prepareTextPreview(contentPreviewView, mActionFactory);
- updateHeadline(contentPreviewView);
+ updateHeadline(contentPreviewView, mFileCount, mAllImages, mAllVideos);
ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view);
if (mIsSingleImage && mFirstFilePreviewUri != null) {
@@ -157,24 +168,25 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
}
- private void updateHeadline(ViewGroup contentPreview) {
+ private void updateHeadline(
+ ViewGroup contentPreview, int fileCount, boolean allImages, boolean allVideos) {
CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action);
String headline;
if (includeText.getVisibility() == View.VISIBLE && includeText.isChecked()) {
- if (mAllImages) {
- headline = mHeadlineGenerator.getImagesWithTextHeadline(mText, mFileCount);
- } else if (mAllVideos) {
- headline = mHeadlineGenerator.getVideosWithTextHeadline(mText, mFileCount);
+ if (allImages) {
+ headline = mHeadlineGenerator.getImagesWithTextHeadline(mText, fileCount);
+ } else if (allVideos) {
+ headline = mHeadlineGenerator.getVideosWithTextHeadline(mText, fileCount);
} else {
- headline = mHeadlineGenerator.getFilesWithTextHeadline(mText, mFileCount);
+ headline = mHeadlineGenerator.getFilesWithTextHeadline(mText, fileCount);
}
} else {
- if (mAllImages) {
- headline = mHeadlineGenerator.getImagesHeadline(mFileCount);
- } else if (mAllVideos) {
- headline = mHeadlineGenerator.getVideosHeadline(mFileCount);
+ if (allImages) {
+ headline = mHeadlineGenerator.getImagesHeadline(fileCount);
+ } else if (allVideos) {
+ headline = mHeadlineGenerator.getVideosHeadline(fileCount);
} else {
- headline = mHeadlineGenerator.getFilesHeadline(mFileCount);
+ headline = mHeadlineGenerator.getFilesHeadline(fileCount);
}
}
@@ -201,7 +213,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
textView.setText(getNoTextString(contentPreview.getResources()));
}
shareTextAction.accept(!isChecked);
- updateHeadline(contentPreview);
+ updateHeadline(contentPreview, mFileCount, mAllImages, mAllVideos);
});
if (SHOW_TOGGLE_CHECKMARK) {
includeText.setVisibility(View.VISIBLE);
diff --git a/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt b/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt
new file mode 100644
index 00000000..b29c5774
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+@file:JvmName("JavaFlowHelper")
+
+package com.android.intentresolver.contentpreview
+
+import com.android.intentresolver.widget.ScrollableImagePreviewView.Preview
+import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+
+internal fun mapFileIntoToPreview(
+ flow: Flow<FileInfo>,
+ typeClassifier: MimeTypeClassifier,
+ editAction: Runnable?
+): Flow<Preview> =
+ flow
+ .filter { it.previewUri != null }
+ .map { fileInfo ->
+ Preview(
+ ContentPreviewUi.getPreviewType(typeClassifier, fileInfo.mimeType),
+ requireNotNull(fileInfo.previewUri),
+ editAction
+ )
+ }
+
+internal fun <T> collectToList(
+ clientScope: CoroutineScope,
+ flow: Flow<T>,
+ callback: Consumer<List<T>>
+) {
+ clientScope.launch { callback.accept(flow.toList()) }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
index 8ab3a272..9f1cc6c1 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
@@ -38,14 +38,18 @@ import com.android.intentresolver.measurements.runTracing
import com.android.intentresolver.util.ownedByCurrentUser
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Consumer
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.take
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
/**
@@ -68,31 +72,45 @@ private const val TIMEOUT_MS = 1_000L
*/
@OpenForTesting
open class PreviewDataProvider
-@VisibleForTesting
+@JvmOverloads
constructor(
+ private val scope: CoroutineScope,
private val targetIntent: Intent,
private val contentResolver: ContentInterface,
- private val typeClassifier: MimeTypeClassifier,
- private val dispatcher: CoroutineDispatcher,
+ private val typeClassifier: MimeTypeClassifier = DefaultMimeTypeClassifier,
) {
- constructor(
- targetIntent: Intent,
- contentResolver: ContentInterface,
- ) : this(
- targetIntent,
- contentResolver,
- DefaultMimeTypeClassifier,
- Dispatchers.IO,
- )
private val records = targetIntent.contentUris.map { UriRecord(it) }
+ private val fileInfoSharedFlow: SharedFlow<FileInfo> by lazy {
+ // Alternatively, we could just use [shareIn()] on a [flow] -- and it would be, arguably,
+ // cleaner -- but we'd lost the ability to trace the traverse as [runTracing] does not
+ // generally work over suspend function invocations.
+ MutableSharedFlow<FileInfo>(replay = records.size).apply {
+ scope.launch {
+ runTracing("image-preview-metadata") {
+ for (record in records) {
+ tryEmit(FileInfo.Builder(record.uri).readFromRecord(record).build())
+ }
+ }
+ }
+ }
+ }
+
/** returns number of shared URIs, see [Intent.EXTRA_STREAM] */
@get:OpenForTesting
open val uriCount: Int
get() = records.size
/**
+ * Returns a [Flow] of [FileInfo], for each shared URI in order, with [FileInfo.mimeType] and
+ * [FileInfo.previewUri] set (a data projection tailored for the image preview UI).
+ */
+ @get:OpenForTesting
+ open val imagePreviewFileInfoFlow: Flow<FileInfo>
+ get() = fileInfoSharedFlow.take(records.size)
+
+ /**
* Preview type to use. The type is determined asynchronously with a timeout; the fall-back
* values is [ContentPreviewType.CONTENT_PREVIEW_FILE]
*/
@@ -107,10 +125,18 @@ constructor(
if (!targetIntent.isSend || records.isEmpty()) {
CONTENT_PREVIEW_TEXT
} else {
- runBlocking(dispatcher) {
- withTimeoutOrNull(TIMEOUT_MS) {
- loadPreviewType()
- } ?: CONTENT_PREVIEW_FILE
+ try {
+ runBlocking(scope.coroutineContext) {
+ withTimeoutOrNull(TIMEOUT_MS) { scope.async { loadPreviewType() }.await() }
+ ?: CONTENT_PREVIEW_FILE
+ }
+ } catch (e: CancellationException) {
+ Log.w(
+ ContentPreviewUi.TAG,
+ "An attempt to read preview type from a cancelled scope",
+ e
+ )
+ CONTENT_PREVIEW_FILE
}
}
}
@@ -123,46 +149,24 @@ constructor(
open val firstFileInfo: FileInfo? by lazy {
runTracing("first-uri-metadata") {
records.firstOrNull()?.let { record ->
- runBlocking(dispatcher) {
- val builder = FileInfo.Builder(record.uri)
- withTimeoutOrNull(TIMEOUT_MS) {
- builder.readFromRecord(record)
+ val builder = FileInfo.Builder(record.uri)
+ try {
+ runBlocking(scope.coroutineContext) {
+ withTimeoutOrNull(TIMEOUT_MS) {
+ scope.async { builder.readFromRecord(record) }.await()
+ }
}
- builder.build()
- }
- }
- }
- }
-
- /**
- * Returns a collection of [FileInfo], for each shared URI in order, with [FileInfo.mimeType]
- * and [FileInfo.previewUri] set (a data projection tailored for the image preview UI).
- */
- @OpenForTesting
- open fun getFileMetadataForImagePreview(
- callerLifecycle: Lifecycle,
- callback: Consumer<List<FileInfo>>,
- ) {
- callerLifecycle.coroutineScope.launch {
- val result = withContext(dispatcher) {
- getFileMetadataForImagePreview()
- }
- callback.accept(result)
- }
- }
-
- private fun getFileMetadataForImagePreview(): List<FileInfo> =
- runTracing("image-preview-metadata") {
- ArrayList<FileInfo>(records.size).also { result ->
- for (record in records) {
- result.add(
- FileInfo.Builder(record.uri)
- .readFromRecord(record)
- .build()
+ } catch (e: CancellationException) {
+ Log.w(
+ ContentPreviewUi.TAG,
+ "An attempt to read first file info from a cancelled scope",
+ e
)
}
+ builder.build()
}
}
+ }
private fun FileInfo.Builder.readFromRecord(record: UriRecord): FileInfo.Builder {
withMimeType(record.mimeType)
@@ -186,9 +190,7 @@ constructor(
throw IndexOutOfBoundsException("There are no shared URIs")
}
callerLifecycle.coroutineScope.launch {
- val result = withContext(dispatcher) {
- getFirstFileName()
- }
+ val result = scope.async { getFirstFileName() }.await()
callback.accept(result)
}
}
@@ -237,8 +239,7 @@ constructor(
}
resultDeferred.complete(CONTENT_PREVIEW_FILE)
}
- resultDeferred.await()
- .also { job.cancel() }
+ resultDeferred.await().also { job.cancel() }
}
}
@@ -251,8 +252,8 @@ constructor(
val isImageType: Boolean
get() = typeClassifier.isImageType(mimeType)
val supportsImageType: Boolean by lazy {
- contentResolver.getStreamTypesSafe(uri)
- ?.firstOrNull(typeClassifier::isImageType) != null
+ contentResolver.getStreamTypesSafe(uri)?.firstOrNull(typeClassifier::isImageType) !=
+ null
}
val supportsThumbnail: Boolean
get() = query.supportsThumbnail
@@ -264,9 +265,8 @@ constructor(
private val query by lazy { readQueryResult() }
private fun readQueryResult(): QueryResult {
- val cursor = contentResolver.querySafe(uri)
- ?.takeIf { it.moveToFirst() }
- ?: return QueryResult()
+ val cursor =
+ contentResolver.querySafe(uri)?.takeIf { it.moveToFirst() } ?: return QueryResult()
var flagColIdx = -1
var displayIconUriColIdx = -1
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
index 331b0cb6..6013f5a0 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
@@ -25,11 +25,15 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.android.intentresolver.ChooserRequestParameters
import com.android.intentresolver.R
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
/** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */
-class PreviewViewModel(private val application: Application) : BasePreviewViewModel() {
+class PreviewViewModel(
+ private val application: Application,
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : BasePreviewViewModel() {
private var previewDataProvider: PreviewDataProvider? = null
private var imageLoader: ImagePreviewImageLoader? = null
@@ -38,15 +42,18 @@ class PreviewViewModel(private val application: Application) : BasePreviewViewMo
chooserRequest: ChooserRequestParameters
): PreviewDataProvider =
previewDataProvider
- ?: PreviewDataProvider(chooserRequest.targetIntent, application.contentResolver).also {
- previewDataProvider = it
- }
+ ?: PreviewDataProvider(
+ viewModelScope + dispatcher,
+ chooserRequest.targetIntent,
+ application.contentResolver
+ )
+ .also { previewDataProvider = it }
@MainThread
override fun createOrReuseImageLoader(): ImageLoader =
imageLoader
?: ImagePreviewImageLoader(
- viewModelScope + Dispatchers.IO,
+ viewModelScope + dispatcher,
thumbnailSize =
application.resources.getDimensionPixelSize(
R.dimen.chooser_preview_image_max_dimen
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
index 6385f2b6..8e635aba 100644
--- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
@@ -31,35 +31,50 @@ import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
import com.android.intentresolver.widget.ScrollableImagePreviewView;
-import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.flow.Flow;
+
class UnifiedContentPreviewUi extends ContentPreviewUi {
private final boolean mShowEditAction;
+ @Nullable
+ private final String mIntentMimeType;
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final ImageLoader mImageLoader;
private final MimeTypeClassifier mTypeClassifier;
private final TransitionElementStatusCallback mTransitionElementStatusCallback;
private final HeadlineGenerator mHeadlineGenerator;
+ private final Flow<FileInfo> mFileInfoFlow;
+ private final int mItemCount;
@Nullable
private List<FileInfo> mFiles;
@Nullable
private ViewGroup mContentPreviewView;
UnifiedContentPreviewUi(
+ CoroutineScope scope,
boolean isSingleImage,
+ @Nullable String intentMimeType,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
MimeTypeClassifier typeClassifier,
TransitionElementStatusCallback transitionElementStatusCallback,
+ Flow<FileInfo> fileInfoFlow,
+ int itemCount,
HeadlineGenerator headlineGenerator) {
mShowEditAction = isSingleImage;
+ mIntentMimeType = intentMimeType;
mActionFactory = actionFactory;
mImageLoader = imageLoader;
mTypeClassifier = typeClassifier;
mTransitionElementStatusCallback = transitionElementStatusCallback;
+ mFileInfoFlow = fileInfoFlow;
+ mItemCount = itemCount;
mHeadlineGenerator = headlineGenerator;
+
+ JavaFlowHelper.collectToList(scope, fileInfoFlow, this::setFiles);
}
@Override
@@ -74,7 +89,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
return layout;
}
- public void setFiles(List<FileInfo> files) {
+ private void setFiles(List<FileInfo> files) {
mImageLoader.prePopulate(files.stream()
.map(FileInfo::getPreviewUri)
.filter(Objects::nonNull)
@@ -96,11 +111,25 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
ScrollableImagePreviewView imagePreview =
mContentPreviewView.requireViewById(R.id.scrollable_image_preview);
+ imagePreview.setImageLoader(mImageLoader);
imagePreview.setOnNoPreviewCallback(() -> imagePreview.setVisibility(View.GONE));
imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback);
+ imagePreview.setPreviews(
+ JavaFlowHelper.mapFileIntoToPreview(
+ mFileInfoFlow,
+ mTypeClassifier,
+ mShowEditAction ? mActionFactory.getEditButtonRunnable() : null),
+ mItemCount);
if (mFiles != null) {
updatePreviewWithFiles(mContentPreviewView, mFiles);
+ } else {
+ displayHeadline(
+ mContentPreviewView,
+ mItemCount,
+ mTypeClassifier.isImageType(mIntentMimeType),
+ mTypeClassifier.isVideoType(mIntentMimeType));
+ imagePreview.setLoading(mItemCount);
}
return mContentPreviewView;
@@ -120,7 +149,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
return;
}
- List<ScrollableImagePreviewView.Preview> previews = new ArrayList<>();
boolean allImages = true;
boolean allVideos = true;
for (FileInfo fileInfo : files) {
@@ -128,24 +156,19 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
getPreviewType(mTypeClassifier, fileInfo.getMimeType());
allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image;
allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video;
-
- if (fileInfo.getPreviewUri() != null) {
- Runnable editAction =
- mShowEditAction ? mActionFactory.getEditButtonRunnable() : null;
- previews.add(
- new ScrollableImagePreviewView.Preview(
- previewType, fileInfo.getPreviewUri(), editAction));
- }
}
- imagePreview.setPreviews(previews, count - previews.size(), mImageLoader);
+ displayHeadline(contentPreviewView, count, allImages, allVideos);
+ }
+ private void displayHeadline(
+ ViewGroup layout, int count, boolean allImages, boolean allVideos) {
if (allImages) {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getImagesHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getImagesHeadline(count));
} else if (allVideos) {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getVideosHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getVideosHeadline(count));
} else {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getFilesHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getFilesHeadline(count));
}
}
}
diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt
index b303dd1a..2c20d341 100644
--- a/java/src/com/android/intentresolver/flags/Flags.kt
+++ b/java/src/com/android/intentresolver/flags/Flags.kt
@@ -23,9 +23,8 @@ import com.android.systemui.flags.UnreleasedFlag
// make the flags available in the flag flipper app (see go/sysui-flags).
// All flags added should be included in UnbundledChooserActivityTest.ALL_FLAGS.
object Flags {
- private fun releasedFlag(id: Int, name: String) =
- ReleasedFlag(id, name, "systemui")
+ private fun releasedFlag(name: String) = ReleasedFlag(name, "systemui")
- private fun unreleasedFlag(id: Int, name: String, teamfood: Boolean = false) =
- UnreleasedFlag(id, name, "systemui", teamfood)
+ private fun unreleasedFlag(name: String, teamfood: Boolean = false) =
+ UnreleasedFlag(name, "systemui", teamfood)
}
diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
index 77ae20f5..fadea934 100644
--- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
+++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
@@ -164,8 +164,10 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
return false;
}
- // Limit width to the maximum width of the chooser activity
- width = Math.min(mChooserWidthPixels, width);
+ // Limit width to the maximum width of the chooser activity, if the maximum width is set
+ if (mChooserWidthPixels >= 0) {
+ width = Math.min(mChooserWidthPixels, width);
+ }
int newWidth = width / mMaxTargetsPerRow;
if (newWidth != mChooserTargetWidth) {
diff --git a/java/src/com/android/intentresolver/icons/LoadIconTask.java b/java/src/com/android/intentresolver/icons/LoadIconTask.java
index 37ce4093..75132208 100644
--- a/java/src/com/android/intentresolver/icons/LoadIconTask.java
+++ b/java/src/com/android/intentresolver/icons/LoadIconTask.java
@@ -24,7 +24,6 @@ import android.os.Trace;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.ResolverActivity;
import com.android.intentresolver.TargetPresentationGetter;
import com.android.intentresolver.chooser.DisplayResolveInfo;
@@ -64,8 +63,7 @@ class LoadIconTask extends BaseLoadIconTask {
protected final Drawable loadIconForResolveInfo(ResolveInfo ri) {
// Load icons based on userHandle from ResolveInfo. If in work profile/clone profile, icons
// should be badged.
- return mPresentationFactory.makePresentationGetter(ri)
- .getIcon(ResolverActivity.getResolveInfoUserHandle(ri, mUserHandle));
+ return mPresentationFactory.makePresentationGetter(ri).getIcon(ri.userHandle);
}
}
diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/logging/EventLog.java
index 1f606f26..b30e825b 100644
--- a/java/src/com/android/intentresolver/ChooserActivityLogger.java
+++ b/java/src/com/android/intentresolver/logging/EventLog.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2023 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,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.logging;
import android.annotation.Nullable;
import android.content.Intent;
@@ -24,6 +24,7 @@ import android.provider.MediaStore;
import android.util.HashedStringCache;
import android.util.Log;
+import com.android.intentresolver.ChooserActivity;
import com.android.intentresolver.contentpreview.ContentPreviewType;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
@@ -39,7 +40,7 @@ import com.android.internal.util.FrameworkStatsLog;
* Helper for writing Sharesheet atoms to statsd log.
* @hide
*/
-public class ChooserActivityLogger {
+public class EventLog {
private static final String TAG = "ChooserActivity";
private static final boolean DEBUG = true;
@@ -94,12 +95,12 @@ public class ChooserActivityLogger {
private final FrameworkStatsLogger mFrameworkStatsLogger;
private final MetricsLogger mMetricsLogger;
- public ChooserActivityLogger() {
+ public EventLog() {
this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger());
}
@VisibleForTesting
- ChooserActivityLogger(
+ EventLog(
UiEventLogger uiEventLogger,
FrameworkStatsLogger frameworkLogger,
MetricsLogger metricsLogger) {
diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index bc54e01e..ff2d6a0f 100644
--- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -30,7 +30,7 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.ResolverActivity;
import com.android.intentresolver.chooser.TargetInfo;
@@ -72,7 +72,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
private static final int WATCHDOG_TIMEOUT_MILLIS = 500;
private final Comparator<ResolveInfo> mAzComparator;
- private ChooserActivityLogger mChooserActivityLogger;
+ private EventLog mEventLog;
protected final Handler mHandler = new Handler(Looper.getMainLooper()) {
public void handleMessage(Message msg) {
@@ -94,8 +94,8 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
mHandler.removeMessages(RANKER_SERVICE_RESULT);
afterCompute();
- if (mChooserActivityLogger != null) {
- mChooserActivityLogger.logSharesheetAppShareRankingTimeout();
+ if (mEventLog != null) {
+ mEventLog.logSharesheetAppShareRankingTimeout();
}
break;
@@ -161,12 +161,12 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
mAfterCompute = afterCompute;
}
- void setChooserActivityLogger(ChooserActivityLogger chooserActivityLogger) {
- mChooserActivityLogger = chooserActivityLogger;
+ void setEventLog(EventLog eventLog) {
+ mEventLog = eventLog;
}
- ChooserActivityLogger getChooserActivityLogger() {
- return mChooserActivityLogger;
+ EventLog getEventLog() {
+ return mEventLog;
}
protected final void afterCompute() {
diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index ba054731..621ae306 100644
--- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -31,7 +31,7 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -72,7 +72,7 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
String referrerPackage,
AppPredictor appPredictor,
UserHandle user,
- ChooserActivityLogger chooserActivityLogger,
+ EventLog eventLog,
@Nullable ComponentName promoteToFirst) {
super(context, intent, Lists.newArrayList(user), promoteToFirst);
mContext = context;
@@ -80,7 +80,7 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
mAppPredictor = appPredictor;
mUser = user;
mReferrerPackage = referrerPackage;
- setChooserActivityLogger(chooserActivityLogger);
+ setEventLog(eventLog);
mComparatorModel = buildUpdatedModel();
}
@@ -116,7 +116,7 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
mIntent,
mReferrerPackage,
() -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
- getChooserActivityLogger(),
+ getEventLog(),
mUser,
mPromoteToFirst);
mComparatorModel = buildUpdatedModel();
diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
index ebaffc36..7d473660 100644
--- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
@@ -39,7 +39,7 @@ import android.service.resolver.ResolverRankerService;
import android.service.resolver.ResolverTarget;
import android.util.Log;
-import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.internal.logging.MetricsLogger;
@@ -102,9 +102,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
*/
public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent,
String referrerPackage, Runnable afterCompute,
- ChooserActivityLogger chooserActivityLogger, UserHandle targetUserSpace,
+ EventLog eventLog, UserHandle targetUserSpace,
ComponentName promoteToFirst) {
- this(launchedFromContext, intent, referrerPackage, afterCompute, chooserActivityLogger,
+ this(launchedFromContext, intent, referrerPackage, afterCompute, eventLog,
Lists.newArrayList(targetUserSpace), promoteToFirst);
}
@@ -118,7 +118,7 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
*/
public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent,
String referrerPackage, Runnable afterCompute,
- ChooserActivityLogger chooserActivityLogger, List<UserHandle> targetUserSpaceList,
+ EventLog eventLog, List<UserHandle> targetUserSpaceList,
@Nullable ComponentName promoteToFirst) {
super(launchedFromContext, intent, targetUserSpaceList, promoteToFirst);
mCollator = Collator.getInstance(
@@ -139,7 +139,7 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
mAction = intent.getAction();
mRankerServiceName = new ComponentName(mContext, this.getClass());
setCallBack(afterCompute);
- setChooserActivityLogger(chooserActivityLogger);
+ setEventLog(eventLog);
mComparatorModel = buildUpdatedModel();
}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
index 3ffbe039..f05542e2 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
@@ -136,7 +136,8 @@ constructor(
}
/** Clear application targets (see [updateAppTargets] and initiate shrtcuts loading. */
- fun reset() {
+ @OpenForTesting
+ open fun reset() {
Log.d(TAG, "reset shortcut loader for user $userHandle")
appTargetSource.tryEmit(null)
shortcutSource.tryEmit(null)
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
index 583a2887..3bbafc40 100644
--- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
@@ -39,14 +39,12 @@ import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatu
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
-import kotlinx.coroutines.plus
private const val TRANSITION_NAME = "screenshot_preview_image"
private const val PLURALS_COUNT = "count"
@@ -127,7 +125,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
isMeasured = true
updateMaxWidthHint(widthSpec)
updateMaxAspectRatio()
- batchLoader?.loadAspectRatios(getMaxWidth(), this::updatePreviewSize)
+ maybeLoadAspectRatios()
}
}
@@ -145,6 +143,17 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
)
}
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ batchLoader?.totalItemCount?.let(previewAdapter::reset)
+ maybeLoadAspectRatios()
+ }
+
+ override fun onDetachedFromWindow() {
+ batchLoader?.cancel()
+ super.onDetachedFromWindow()
+ }
+
override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) {
previewAdapter.transitionStatusElementCallback = callback
}
@@ -158,32 +167,38 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
return null
}
- fun setPreviews(previews: List<Preview>, otherItemCount: Int, imageLoader: CachingImageLoader) {
- previewAdapter.reset(0, imageLoader)
+ fun setImageLoader(imageLoader: CachingImageLoader) {
+ previewAdapter.imageLoader = imageLoader
+ }
+
+ fun setLoading(totalItemCount: Int) {
+ previewAdapter.reset(totalItemCount)
+ }
+
+ fun setPreviews(previews: Flow<Preview>, totalItemCount: Int) {
+ previewAdapter.reset(totalItemCount)
batchLoader?.cancel()
batchLoader =
BatchPreviewLoader(
- imageLoader,
- previews,
- otherItemCount,
- onReset = { totalItemCount ->
- previewAdapter.reset(totalItemCount, imageLoader)
- },
- onUpdate = previewAdapter::addPreviews,
- onCompletion = {
- if (!previewAdapter.hasPreviews) {
- onNoPreviewCallback?.run()
- }
- }
- )
- .apply {
- if (isMeasured) {
- loadAspectRatios(
- getMaxWidth(),
- this@ScrollableImagePreviewView::updatePreviewSize
- )
+ previewAdapter.imageLoader ?: error("Image loader is not set"),
+ previews,
+ totalItemCount,
+ onUpdate = previewAdapter::addPreviews,
+ onCompletion = {
+ batchLoader = null
+ if (!previewAdapter.hasPreviews) {
+ onNoPreviewCallback?.run()
}
+ previewAdapter.markLoaded()
}
+ )
+ maybeLoadAspectRatios()
+ }
+
+ private fun maybeLoadAspectRatios() {
+ if (isMeasured && isAttachedToWindow()) {
+ batchLoader?.let { it.loadAspectRatios(getMaxWidth(), this::updatePreviewSize) }
+ }
}
var onNoPreviewCallback: Runnable? = null
@@ -262,10 +277,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
context.resources.getString(R.string.video_preview_a11y_description)
private val filePreviewDescription =
context.resources.getString(R.string.file_preview_a11y_description)
- private var imageLoader: CachingImageLoader? = null
+ var imageLoader: CachingImageLoader? = null
private var firstImagePos = -1
private var totalItemCount: Int = 0
+ private var isLoading = false
private val hasOtherItem
get() = previews.size < totalItemCount
val hasPreviews: Boolean
@@ -273,61 +289,79 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
var transitionStatusElementCallback: TransitionElementStatusCallback? = null
- fun reset(totalItemCount: Int, imageLoader: CachingImageLoader) {
- this.imageLoader = imageLoader
+ fun reset(totalItemCount: Int) {
firstImagePos = -1
previews.clear()
this.totalItemCount = maxOf(0, totalItemCount)
+ isLoading = this.totalItemCount > 0
notifyDataSetChanged()
}
+ fun markLoaded() {
+ if (!isLoading) return
+ isLoading = false
+ if (hasOtherItem) {
+ notifyItemChanged(previews.size)
+ } else {
+ notifyItemRemoved(previews.size)
+ }
+ }
+
fun addPreviews(newPreviews: Collection<Preview>) {
if (newPreviews.isEmpty()) return
val insertPos = previews.size
val hadOtherItem = hasOtherItem
+ val wasEmpty = previews.isEmpty()
previews.addAll(newPreviews)
if (firstImagePos < 0) {
val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image }
if (pos >= 0) firstImagePos = insertPos + pos
}
- notifyItemRangeInserted(insertPos, newPreviews.size)
- when {
- hadOtherItem && previews.size >= totalItemCount -> {
- notifyItemRemoved(previews.size)
- }
- !hadOtherItem && previews.size < totalItemCount -> {
- notifyItemInserted(previews.size)
+ if (wasEmpty) {
+ // we don't want any item animation in that case
+ notifyDataSetChanged()
+ } else {
+ notifyItemRangeInserted(insertPos, newPreviews.size)
+ when {
+ hadOtherItem && !hasOtherItem -> {
+ notifyItemRemoved(previews.size)
+ }
+ !hadOtherItem && hasOtherItem -> {
+ notifyItemInserted(previews.size)
+ }
+ else -> notifyItemChanged(previews.size)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(itemType, parent, false)
- return if (itemType == R.layout.image_preview_other_item) {
- OtherItemViewHolder(view)
- } else {
- PreviewViewHolder(
- view,
- imagePreviewDescription,
- videoPreviewDescription,
- filePreviewDescription,
- )
+ return when (itemType) {
+ R.layout.image_preview_other_item -> OtherItemViewHolder(view)
+ R.layout.image_preview_loading_item -> LoadingItemViewHolder(view)
+ else ->
+ PreviewViewHolder(
+ view,
+ imagePreviewDescription,
+ videoPreviewDescription,
+ filePreviewDescription,
+ )
}
}
- override fun getItemCount(): Int = previews.size + if (hasOtherItem) 1 else 0
+ override fun getItemCount(): Int = previews.size + if (isLoading || hasOtherItem) 1 else 0
- override fun getItemViewType(position: Int): Int {
- return if (position == previews.size) {
- R.layout.image_preview_other_item
- } else {
- R.layout.image_preview_image_item
+ override fun getItemViewType(position: Int): Int =
+ when {
+ position == previews.size && isLoading -> R.layout.image_preview_loading_item
+ position == previews.size -> R.layout.image_preview_other_item
+ else -> R.layout.image_preview_image_item
}
- }
override fun onBindViewHolder(vh: ViewHolder, position: Int) {
when (vh) {
is OtherItemViewHolder -> vh.bind(totalItemCount - previews.size)
+ is LoadingItemViewHolder -> vh.bind()
is PreviewViewHolder ->
vh.bind(
previews[position],
@@ -440,7 +474,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
private fun resetScope(): CoroutineScope =
- (MainScope() + Dispatchers.Main.immediate).also {
+ CoroutineScope(Dispatchers.Main.immediate).also {
scope?.cancel()
scope = it
}
@@ -466,6 +500,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
override fun unbind() = Unit
}
+ private class LoadingItemViewHolder(view: View) : ViewHolder(view) {
+ fun bind() = Unit
+ override fun unbind() = Unit
+ }
+
private class SpacingDecoration(private val innerSpacing: Int, private val outerSpacing: Int) :
ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
@@ -485,27 +524,22 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
@VisibleForTesting
class BatchPreviewLoader(
private val imageLoader: CachingImageLoader,
- previews: List<Preview>,
- otherItemCount: Int,
- private val onReset: (Int) -> Unit,
+ private val previews: Flow<Preview>,
+ val totalItemCount: Int,
private val onUpdate: (List<Preview>) -> Unit,
private val onCompletion: () -> Unit,
) {
- private val previews: List<Preview> =
- if (previews is RandomAccess) previews else ArrayList(previews)
- private val totalItemCount = previews.size + otherItemCount
- private var scope: CoroutineScope? = MainScope() + Dispatchers.Main.immediate
+ private var scope: CoroutineScope = createScope()
+
+ private fun createScope() = CoroutineScope(Dispatchers.Main.immediate)
fun cancel() {
- scope?.cancel()
- scope = null
+ scope.cancel()
+ scope = createScope()
}
fun loadAspectRatios(maxWidth: Int, previewSizeUpdater: (Preview, Int, Int) -> Int) {
- val scope = this.scope ?: return
- // -1 encodes that the preview has not been processed,
- // 0 means failed, > 0 is a preview width
- val previewWidths = IntArray(previews.size) { -1 }
+ val previewInfos = ArrayList<PreviewWidthInfo>(totalItemCount)
var blockStart = 0 // inclusive
var blockEnd = 0 // exclusive
@@ -514,26 +548,16 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
val updateEvent = Any()
val completedEvent = Any()
- // throttle adapter updates using flow; the flow first emits when enough preview
- // elements is loaded to fill the viewport and then each time a subsequent block of
- // previews is loaded
+ // collects updates from [reportFlow] throttling adapter updates;
scope.launch(Dispatchers.Main) {
reportFlow
.takeWhile { it !== completedEvent }
.throttle(ADAPTER_UPDATE_INTERVAL_MS)
- .onCompletion { cause ->
- if (cause == null) {
- onCompletion()
- }
- }
.collect {
- if (blockStart == 0) {
- onReset(totalItemCount)
- }
val updates = ArrayList<Preview>(blockEnd - blockStart)
while (blockStart < blockEnd) {
- if (previewWidths[blockStart] > 0) {
- updates.add(previews[blockStart])
+ if (previewInfos[blockStart].width > 0) {
+ updates.add(previewInfos[blockStart].preview)
}
blockStart++
}
@@ -541,57 +565,64 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
onUpdate(updates)
}
}
+ onCompletion()
}
+ // Collects [previews] flow and loads aspect ratios, emits updates into [reportFlow]
+ // when a next sequential block of preview aspect ratios is loaded: initially emits when
+ // enough preview elements is loaded to fill the viewport.
scope.launch {
var blockWidth = 0
var isFirstBlock = true
- var nextIdx = 0
- List<Job>(4) {
- launch {
- while (true) {
- val i = nextIdx++
- if (i >= previews.size) break
- val preview = previews[i]
-
- previewWidths[i] =
- runCatching {
- // TODO: decide on adding a timeout
- imageLoader(preview.uri, isFirstBlock)?.let { bitmap ->
- previewSizeUpdater(
- preview,
- bitmap.width,
- bitmap.height
- )
- }
- ?: 0
- }
- .getOrDefault(0)
-
- if (blockEnd != i) continue
- while (
- blockEnd < previewWidths.size && previewWidths[blockEnd] >= 0
- ) {
- blockWidth += previewWidths[blockEnd]
- blockEnd++
- }
- if (isFirstBlock) {
- if (blockWidth >= maxWidth) {
- isFirstBlock = false
- // notify that the preview now can be displayed
- reportFlow.emit(updateEvent)
+
+ val jobs = ArrayList<Job>()
+ previews.collect { preview ->
+ val i = previewInfos.size
+ val pair = PreviewWidthInfo(preview)
+ previewInfos.add(pair)
+
+ val job = launch {
+ pair.width =
+ runCatching {
+ // TODO: decide on adding a timeout. The worst case I can
+ // imagine is one of the first images never loads so we never
+ // fill the initial viewport and does not show the previews at
+ // all.
+ imageLoader(preview.uri, isFirstBlock)?.let { bitmap ->
+ previewSizeUpdater(preview, bitmap.width, bitmap.height)
}
- } else {
- reportFlow.emit(updateEvent)
+ ?: 0
}
+ .getOrDefault(0)
+
+ if (i == blockEnd) {
+ while (
+ blockEnd < previewInfos.size && previewInfos[blockEnd].width >= 0
+ ) {
+ blockWidth += previewInfos[blockEnd].width
+ blockEnd++
+ }
+ if (isFirstBlock && blockWidth >= maxWidth) {
+ isFirstBlock = false
+ }
+ if (!isFirstBlock) {
+ reportFlow.emit(updateEvent)
}
}
}
- .joinAll()
+ jobs.add(job)
+ }
+ jobs.joinAll()
// in case all previews have failed to load
reportFlow.emit(updateEvent)
reportFlow.emit(completedEvent)
}
}
}
+
+ private class PreviewWidthInfo(val preview: Preview) {
+ // -1 encodes that the preview has not been processed,
+ // 0 means failed, > 0 is a preview width
+ var width: Int = -1
+ }
}