summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
Diffstat (limited to 'java')
-rw-r--r--java/res/layout/chooser_action_row.xml7
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java273
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityLogger.java145
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewUi.java123
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java4
-rw-r--r--java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java10
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java219
-rw-r--r--java/src/com/android/intentresolver/TargetPresentationGetter.java267
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java28
-rw-r--r--java/src/com/android/intentresolver/widget/ActionRow.kt93
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java199
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java3
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java9
-rw-r--r--java/tests/src/com/android/intentresolver/IChooserWrapper.java3
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverActivityTest.java56
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverDataProvider.java12
-rw-r--r--java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt204
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java210
18 files changed, 1157 insertions, 708 deletions
diff --git a/java/res/layout/chooser_action_row.xml b/java/res/layout/chooser_action_row.xml
index ea756112..fd47155c 100644
--- a/java/res/layout/chooser_action_row.xml
+++ b/java/res/layout/chooser_action_row.xml
@@ -14,13 +14,10 @@
~ limitations under the License
-->
-<LinearLayout
+<com.android.intentresolver.widget.ActionRow
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/chooser_edge_margin_normal"
android:paddingRight="@dimen/chooser_edge_margin_normal"
- android:gravity="center"
- >
-
-</LinearLayout>
+ android:gravity="center" />
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 295d5b70..c7470ab2 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -58,7 +58,6 @@ import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.drawable.Drawable;
-import android.metrics.LogMaker;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
@@ -74,12 +73,10 @@ import android.provider.DeviceConfig;
import android.provider.Settings;
import android.service.chooser.ChooserTarget;
import android.text.TextUtils;
-import android.util.HashedStringCache;
import android.util.Log;
import android.util.Size;
import android.util.Slog;
import android.util.SparseArray;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
@@ -88,7 +85,6 @@ import android.view.WindowInsets;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
-import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.MainThread;
@@ -99,7 +95,6 @@ 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.ResolverListAdapter.ViewHolder;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -110,11 +105,11 @@ 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;
@@ -186,13 +181,6 @@ public class ChooserActivity extends ResolverActivity implements
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;
@@ -251,8 +239,6 @@ public class ChooserActivity extends ResolverActivity implements
private SharedPreferences mPinnedSharedPrefs;
private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings";
- protected MetricsLogger mMetricsLogger;
-
private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
@Nullable
@@ -346,13 +332,8 @@ public class ChooserActivity extends ResolverActivity implements
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, mChooserRequest.getTargetType())
- .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost));
+ getChooserActivityLogger().logChooserActivityShown(
+ isWorkProfile(), mChooserRequest.getTargetType(), systemCost);
if (mResolverDrawerLayout != null) {
mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
@@ -593,7 +574,9 @@ public class ChooserActivity extends ResolverActivity implements
if (shouldShowStickyContentPreview()
|| mChooserMultiProfilePagerAdapter
.getCurrentRootAdapter().getSystemRowCount() != 0) {
- logActionShareWithPreview();
+ getChooserActivityLogger().logActionShareWithPreview(
+ ChooserContentPreviewUi.findPreferredContentPreview(
+ getTargetIntent(), getContentResolver(), this::isImageType));
}
return postRebuildListInternal(rebuildCompleted);
}
@@ -644,7 +627,7 @@ public class ChooserActivity extends ResolverActivity implements
updateProfileViewButton();
}
- private void onCopyButtonClicked(View v) {
+ private void onCopyButtonClicked() {
Intent targetIntent = getTargetIntent();
if (targetIntent == null) {
finish();
@@ -682,15 +665,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();
@@ -775,21 +750,23 @@ public class ChooserActivity extends ResolverActivity implements
int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
targetIntent, getContentResolver(), this::isImageType);
- ChooserContentPreviewUi.ActionButtonFactory buttonFactory =
- new ChooserContentPreviewUi.ActionButtonFactory() {
+ ChooserContentPreviewUi.ActionFactory actionFactory =
+ new ChooserContentPreviewUi.ActionFactory() {
@Override
- public Button createCopyButton() {
- return ChooserActivity.this.createCopyButton();
+ public ActionRow.Action createCopyButton() {
+ return ChooserActivity.this.createCopyAction();
}
+ @Nullable
@Override
- public Button createEditButton() {
- return ChooserActivity.this.createEditButton(targetIntent);
+ public ActionRow.Action createEditButton() {
+ return ChooserActivity.this.createEditAction(targetIntent);
}
+ @Nullable
@Override
- public Button createNearbyButton() {
- return ChooserActivity.this.createNearbyButton(targetIntent);
+ public ActionRow.Action createNearbyButton() {
+ return ChooserActivity.this.createNearbyAction(targetIntent);
}
};
@@ -798,7 +775,7 @@ public class ChooserActivity extends ResolverActivity implements
targetIntent,
getResources(),
getLayoutInflater(),
- buttonFactory,
+ actionFactory,
parent,
previewCoordinator,
getContentResolver(),
@@ -925,64 +902,49 @@ public class ChooserActivity extends ResolverActivity implements
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.getDisplayIconHolder().getDisplayIcon(),
+ 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.getDisplayIconHolder().getDisplayIcon(),
+ 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) {
@@ -997,8 +959,6 @@ public class ChooserActivity extends ResolverActivity implements
}
}
);
- b.setId(com.android.internal.R.id.chooser_edit_button);
- return b;
}
@Nullable
@@ -1007,17 +967,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);
- }
-
/**
* Wrapping the ContentResolver call to expose for easier mocking,
* and to avoid mocking Android core classes.
@@ -1032,14 +981,6 @@ public class ChooserActivity extends ResolverActivity implements
return mimeType != null && mimeType.startsWith("image/");
}
- 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 int getNumSheetExpansions() {
return getPreferences(Context.MODE_PRIVATE).getInt(PREF_NUM_SHEET_EXPANSIONS, 0);
}
@@ -1249,78 +1190,51 @@ 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;
- directTargetHashed = targetInfo.getHashedTargetIdForMetrics(this);
- directTargetAlsoRanked = getRankedPosition(targetInfo);
-
- numCallerProvided = mChooserRequest.getCallerChooserTargets().size();
getChooserActivityLogger().logShareTargetSelected(
- SELECTION_TYPE_SERVICE,
+ ChooserActivityLogger.SELECTION_TYPE_SERVICE,
targetInfo.getResolveInfo().activityInfo.processName,
- value,
- targetInfo.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;
}
}
}
@@ -1396,15 +1310,14 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- private void logDirectShareTargetReceived(int logCategory, UserHandle forUser) {
+ private void logDirectShareTargetReceived(UserHandle forUser) {
ProfileRecord profileRecord = getProfileRecord(forUser);
if (profileRecord == null) {
return;
}
-
- final int apiLatency =
- (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime);
- 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) {
@@ -1546,13 +1459,6 @@ 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 ChooserActivityLogger();
@@ -1736,7 +1642,7 @@ 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;
}
@@ -1996,10 +1902,7 @@ public class ChooserActivity extends ResolverActivity implements
adapter.completeServiceTargetLoading();
}
- logDirectShareTargetReceived(
- MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
- userHandle);
-
+ logDirectShareTargetReceived(userHandle);
sendVoiceChoicesIfNeeded();
getChooserActivityLogger().logSharesheetDirectLoadComplete();
}
@@ -2130,14 +2033,6 @@ public class ChooserActivity extends ResolverActivity implements
contentPreviewContainer.setVisibility(View.GONE);
}
- private void logActionShareWithPreview() {
- Intent targetIntent = getTargetIntent();
- int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
- targetIntent, getContentResolver(), this::isImageType);
- getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)
- .setSubtype(previewType));
- }
-
private void startFinishAnimation() {
View rootView = findRootView();
if (rootView != null) {
diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/ChooserActivityLogger.java
index 811d5f3e..9109bf93 100644
--- a/java/src/com/android/intentresolver/ChooserActivityLogger.java
+++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java
@@ -16,15 +16,22 @@
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;
/**
@@ -32,6 +39,16 @@ import com.android.internal.util.FrameworkStatsLog;
* @hide
*/
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}.
@@ -70,15 +87,30 @@ public class ChooserActivityLogger {
private final UiEventLogger mUiEventLogger;
private final FrameworkStatsLogger mFrameworkStatsLogger;
+ private final MetricsLogger mMetricsLogger;
public ChooserActivityLogger() {
- this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger());
+ this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger());
}
@VisibleForTesting
- ChooserActivityLogger(UiEventLogger uiEventLogger, FrameworkStatsLogger frameworkLogger) {
+ 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. */
@@ -97,15 +129,92 @@ public class ChooserActivityLogger {
/* intentType = 9 */ typeFromIntentString(intent));
}
- /** Logs a UiEventReported event for the system sharesheet when the user selects a target. */
- public 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. */
@@ -231,17 +340,17 @@ public class 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;
@@ -328,6 +437,20 @@ public class ChooserActivityLogger {
}
}
+ @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(
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
index 22ff55db..f9f4ee98 100644
--- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
@@ -34,11 +34,12 @@ import android.util.PluralsMessageFormatter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.RoundedRectImageView;
import com.android.internal.annotations.VisibleForTesting;
@@ -88,15 +89,17 @@ public final class ChooserContentPreviewUi {
* they're determined to be appropriate for the particular preview we display.
* TODO: clarify why action buttons are part of preview logic.
*/
- public interface ActionButtonFactory {
- /** Create a button that copies the share content to the clipboard. */
- Button createCopyButton();
+ public interface ActionFactory {
+ /** Create an action that copies the share content to the clipboard. */
+ ActionRow.Action createCopyButton();
- /** Create a button that opens the share content in a system-default editor. */
- Button createEditButton();
+ /** Create an action that opens the share content in a system-default editor. */
+ @Nullable
+ ActionRow.Action createEditButton();
- /** Create a "Share to Nearby" button. */
- Button createNearbyButton();
+ /** Create an "Share to Nearby" action. */
+ @Nullable
+ ActionRow.Action createNearbyButton();
}
/**
@@ -173,7 +176,7 @@ public final class ChooserContentPreviewUi {
Intent targetIntent,
Resources resources,
LayoutInflater layoutInflater,
- ActionButtonFactory buttonFactory,
+ ActionFactory actionFactory,
ViewGroup parent,
ContentPreviewCoordinator previewCoord,
ContentResolver contentResolver,
@@ -184,18 +187,16 @@ public final class ChooserContentPreviewUi {
case CONTENT_PREVIEW_TEXT:
layout = displayTextContentPreview(
targetIntent,
- resources,
layoutInflater,
- buttonFactory,
+ createTextPreviewActions(actionFactory),
parent,
previewCoord);
break;
case CONTENT_PREVIEW_IMAGE:
layout = displayImageContentPreview(
targetIntent,
- resources,
layoutInflater,
- buttonFactory,
+ createImagePreviewActions(actionFactory),
parent,
previewCoord,
contentResolver,
@@ -206,7 +207,7 @@ public final class ChooserContentPreviewUi {
targetIntent,
resources,
layoutInflater,
- buttonFactory,
+ createFilePreviewActions(actionFactory),
parent,
previewCoord,
contentResolver);
@@ -235,20 +236,18 @@ public final class ChooserContentPreviewUi {
private static ViewGroup displayTextContentPreview(
Intent targetIntent,
- Resources resources,
LayoutInflater layoutInflater,
- ActionButtonFactory buttonFactory,
+ List<ActionRow.Action> actions,
ViewGroup parent,
ContentPreviewCoordinator previewCoord) {
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);
- final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
- addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
- addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
+ final ActionRow actionRow =
+ contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
if (sharingText == null) {
@@ -296,11 +295,20 @@ public final class ChooserContentPreviewUi {
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,
- Resources resources,
LayoutInflater layoutInflater,
- ActionButtonFactory buttonFactory,
+ List<ActionRow.Action> actions,
ViewGroup parent,
ContentPreviewCoordinator previewCoord,
ContentResolver contentResolver,
@@ -310,13 +318,11 @@ public final class ChooserContentPreviewUi {
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);
- final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
- //TODO: addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
- addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
- addActionButton(actionRow, buttonFactory.createEditButton(), iconMargin);
+ final ActionRow actionRow =
+ contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
String action = targetIntent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
@@ -375,24 +381,37 @@ public final class ChooserContentPreviewUi {
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,
- ActionButtonFactory buttonFactory,
+ List<ActionRow.Action> actions,
ViewGroup parent,
ContentPreviewCoordinator previewCoord,
ContentResolver contentResolver) {
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);
- final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
- //TODO(b/120417119):
- // addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
- addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
+ final ActionRow actionRow =
+ contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
String action = targetIntent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
@@ -438,6 +457,17 @@ public final class ChooserContentPreviewUi {
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 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 "
@@ -475,19 +505,6 @@ public final class ChooserContentPreviewUi {
}
}
- private static void addActionButton(ViewGroup parent, Button b, int iconMargin) {
- if (b == null) {
- return;
- }
- final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT
- );
- final int gap = iconMargin / 2;
- lp.setMarginsRelative(gap, 0, gap, 0);
- parent.addView(b, lp);
- }
-
private static FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
String fileName = null;
boolean hasThumbnail = false;
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 12a054b9..699190f9 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -224,7 +224,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
ri.icon = 0;
}
DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo(
- ii, ri, ii, makePresentationGetter(ri));
+ ii, ri, ii, mPresentationFactory.makePresentationGetter(ri));
mCallerTargets.add(displayResolveInfo);
if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
}
@@ -715,7 +715,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
// Now fetch app icon and raster with no badging even in work profile
- Bitmap appIcon = makePresentationGetter(info).getIconBitmap(null);
+ Bitmap appIcon = mPresentationFactory.makePresentationGetter(info).getIconBitmap(null);
// Raster target drawable with appIcon as a badge
SimpleIconFactory sif = SimpleIconFactory.obtain(context);
diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
index f4d4a6d1..0aa32505 100644
--- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -19,8 +19,6 @@ 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;
@@ -136,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));
@@ -270,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) {
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 5513eda2..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;
@@ -74,6 +70,7 @@ public class ResolverListAdapter extends BaseAdapter {
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;
@@ -126,6 +123,7 @@ public class ResolverListAdapter extends BaseAdapter {
mIsAudioCaptureDevice = isAudioCaptureDevice;
final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE);
mIconDpi = am.getLauncherLargeIconDensity();
+ mPresentationFactory = new TargetPresentationGetter.Factory(mContext, mIconDpi);
}
public final DisplayResolveInfo getFirstDisplayResolveInfo() {
@@ -479,7 +477,7 @@ public class ResolverListAdapter extends BaseAdapter {
ri.loadLabel(mPm),
null,
ii,
- makePresentationGetter(ri)));
+ mPresentationFactory.makePresentationGetter(ri)));
}
}
@@ -532,7 +530,7 @@ public class ResolverListAdapter extends BaseAdapter {
intent,
add,
(replaceIntent != null) ? replaceIntent : defaultIntent,
- makePresentationGetter(add));
+ mPresentationFactory.makePresentationGetter(add));
dri.setPinned(rci.isPinned());
if (rci.isPinned()) {
Log.i(TAG, "Pinned item: " + rci.name);
@@ -765,17 +763,9 @@ 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() {
@@ -875,8 +865,9 @@ public class ResolverListAdapter extends BaseAdapter {
Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
resolveInfo.activityInfo, targetIntent);
- ResolveInfoPresentationGetter presentationGetter =
- new ResolveInfoPresentationGetter(context, iconDpi, resolveInfo);
+ TargetPresentationGetter presentationGetter =
+ new TargetPresentationGetter.Factory(context, iconDpi)
+ .makePresentationGetter(resolveInfo);
return DisplayResolveInfo.newDisplayResolveInfo(
resolvedComponentInfo.getIntentAt(0),
@@ -979,8 +970,8 @@ public class ResolverListAdapter extends BaseAdapter {
@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
@@ -1051,194 +1042,4 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
}
-
- /**
- * 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/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/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
index c1b007af..db5ae0b4 100644
--- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -28,7 +28,7 @@ import android.os.Bundle;
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;
@@ -45,7 +45,7 @@ public class DisplayResolveInfo implements TargetInfo {
private final Intent mResolvedIntent;
private final List<Intent> mSourceIntents = new ArrayList<>();
private final boolean mIsSuspended;
- private ResolveInfoPresentationGetter mResolveInfoPresentationGetter;
+ private TargetPresentationGetter mPresentationGetter;
private boolean mPinned = false;
private final IconHolder mDisplayIconHolder = new SettableIconHolder();
@@ -54,7 +54,7 @@ public class DisplayResolveInfo implements TargetInfo {
Intent originalIntent,
ResolveInfo resolveInfo,
@NonNull Intent resolvedIntent,
- @Nullable ResolveInfoPresentationGetter presentationGetter) {
+ @Nullable TargetPresentationGetter presentationGetter) {
return newDisplayResolveInfo(
originalIntent,
resolveInfo,
@@ -71,14 +71,14 @@ public class DisplayResolveInfo implements TargetInfo {
CharSequence displayLabel,
CharSequence extendedInfo,
@NonNull Intent resolvedIntent,
- @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ @Nullable TargetPresentationGetter presentationGetter) {
return new DisplayResolveInfo(
originalIntent,
resolveInfo,
displayLabel,
extendedInfo,
resolvedIntent,
- resolveInfoPresentationGetter);
+ presentationGetter);
}
private DisplayResolveInfo(
@@ -87,12 +87,12 @@ public class DisplayResolveInfo implements TargetInfo {
CharSequence displayLabel,
CharSequence extendedInfo,
@NonNull Intent resolvedIntent,
- @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ @Nullable TargetPresentationGetter presentationGetter) {
mSourceIntents.add(originalIntent);
mResolveInfo = resolveInfo;
mDisplayLabel = displayLabel;
mExtendedInfo = extendedInfo;
- mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+ mPresentationGetter = presentationGetter;
final ActivityInfo ai = mResolveInfo.activityInfo;
mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
@@ -108,7 +108,7 @@ public class DisplayResolveInfo implements TargetInfo {
DisplayResolveInfo other,
Intent fillInIntent,
int flags,
- ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ TargetPresentationGetter presentationGetter) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
mIsSuspended = other.mIsSuspended;
@@ -116,7 +116,7 @@ public class DisplayResolveInfo implements TargetInfo {
mExtendedInfo = other.mExtendedInfo;
mResolvedIntent = new Intent(other.mResolvedIntent);
mResolvedIntent.fillIn(fillInIntent, flags);
- mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+ mPresentationGetter = presentationGetter;
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
@@ -128,7 +128,7 @@ public class DisplayResolveInfo implements TargetInfo {
mDisplayLabel = other.mDisplayLabel;
mExtendedInfo = other.mExtendedInfo;
mResolvedIntent = other.mResolvedIntent;
- mResolveInfoPresentationGetter = other.mResolveInfoPresentationGetter;
+ mPresentationGetter = other.mPresentationGetter;
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
@@ -143,9 +143,9 @@ public class DisplayResolveInfo implements TargetInfo {
}
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;
}
@@ -169,7 +169,7 @@ public class DisplayResolveInfo implements TargetInfo {
@Override
public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return new DisplayResolveInfo(this, fillInIntent, flags, mResolveInfoPresentationGetter);
+ return new DisplayResolveInfo(this, fillInIntent, flags, mPresentationGetter);
}
@Override
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..1be48f34
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ActionRow.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.content.res.Resources.ID_NULL
+import android.graphics.drawable.Drawable
+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
+
+// TODO: extract an interface out of the class, use it in layout hierarchy an have a layout inflater
+// to instantiate the right view based on a flag value.
+class ActionRow : LinearLayout {
+ 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)
+ }
+
+ 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)
+ }
+
+ 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/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
index 702e725a..705a3228 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
@@ -30,14 +30,17 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.Intent;
+import android.metrics.LogMaker;
import com.android.intentresolver.ChooserActivityLogger.FrameworkStatsLogger;
import com.android.intentresolver.ChooserActivityLogger.SharesheetStandardEvent;
import com.android.intentresolver.ChooserActivityLogger.SharesheetStartedEvent;
import com.android.intentresolver.ChooserActivityLogger.SharesheetTargetSelectedEvent;
import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLogger.UiEventEnum;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FrameworkStatsLog;
import org.junit.After;
@@ -52,18 +55,59 @@ import org.mockito.junit.MockitoJUnitRunner;
public final class ChooserActivityLoggerTest {
@Mock private UiEventLogger mUiEventLog;
@Mock private FrameworkStatsLogger mFrameworkLog;
+ @Mock private MetricsLogger mMetricsLogger;
private ChooserActivityLogger mChooserLogger;
@Before
public void setUp() {
- mChooserLogger = new ChooserActivityLogger(mUiEventLog, mFrameworkLog);
+ //Mockito.reset(mUiEventLog, mFrameworkLog, mMetricsLogger);
+ mChooserLogger = new ChooserActivityLogger(mUiEventLog, mFrameworkLog, mMetricsLogger);
}
@After
public void tearDown() {
verifyNoMoreInteractions(mUiEventLog);
verifyNoMoreInteractions(mFrameworkLog);
+ verifyNoMoreInteractions(mMetricsLogger);
+ }
+
+ @Test
+ public void testLogChooserActivityShown_personalProfile() {
+ final boolean isWorkProfile = false;
+ final String mimeType = "application/TestType";
+ final long systemCost = 456;
+
+ mChooserLogger.logChooserActivityShown(isWorkProfile, mimeType, systemCost);
+
+ ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ verify(mMetricsLogger).write(eventCaptor.capture());
+ LogMaker event = eventCaptor.getValue();
+
+ assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN);
+ assertThat(event.getSubtype()).isEqualTo(MetricsEvent.PARENT_PROFILE);
+ assertThat(event.getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE)).isEqualTo(mimeType);
+ assertThat(event.getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS))
+ .isEqualTo(systemCost);
+ }
+
+ @Test
+ public void testLogChooserActivityShown_workProfile() {
+ final boolean isWorkProfile = true;
+ final String mimeType = "application/TestType";
+ final long systemCost = 456;
+
+ mChooserLogger.logChooserActivityShown(isWorkProfile, mimeType, systemCost);
+
+ ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ verify(mMetricsLogger).write(eventCaptor.capture());
+ LogMaker event = eventCaptor.getValue();
+
+ assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN);
+ assertThat(event.getSubtype()).isEqualTo(MetricsEvent.MANAGED_PROFILE);
+ assertThat(event.getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE)).isEqualTo(mimeType);
+ assertThat(event.getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS))
+ .isEqualTo(systemCost);
}
@Test
@@ -102,20 +146,87 @@ public final class ChooserActivityLoggerTest {
@Test
public void testLogShareTargetSelected() {
- final int targetType = ChooserActivity.SELECTION_TYPE_COPY;
+ final int targetType = ChooserActivityLogger.SELECTION_TYPE_SERVICE;
final String packageName = "com.test.foo";
final int positionPicked = 123;
- final boolean pinned = true;
-
- mChooserLogger.logShareTargetSelected(targetType, packageName, positionPicked, pinned);
+ final int directTargetAlsoRanked = -1;
+ final int callerTargetCount = 0;
+ final boolean isPinned = true;
+ final boolean isSuccessfullySelected = true;
+ final long selectionCost = 456;
+
+ mChooserLogger.logShareTargetSelected(
+ targetType,
+ packageName,
+ positionPicked,
+ directTargetAlsoRanked,
+ callerTargetCount,
+ /* directTargetHashed= */ null,
+ isPinned,
+ isSuccessfullySelected,
+ selectionCost);
verify(mFrameworkLog).write(
eq(FrameworkStatsLog.RANKING_SELECTED),
- eq(SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId()),
+ eq(SharesheetTargetSelectedEvent.SHARESHEET_SERVICE_TARGET_SELECTED.getId()),
eq(packageName),
/* instanceId=*/ gt(0),
eq(positionPicked),
- eq(pinned));
+ eq(isPinned));
+
+ ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ verify(mMetricsLogger).write(eventCaptor.capture());
+ LogMaker event = eventCaptor.getValue();
+ assertThat(event.getCategory()).isEqualTo(
+ MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET);
+ assertThat(event.getSubtype()).isEqualTo(positionPicked);
+ }
+
+ @Test
+ public void testLogActionSelected() {
+ mChooserLogger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
+
+ verify(mFrameworkLog).write(
+ eq(FrameworkStatsLog.RANKING_SELECTED),
+ eq(SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId()),
+ eq(""),
+ /* instanceId=*/ gt(0),
+ eq(-1),
+ eq(false));
+
+ ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ verify(mMetricsLogger).write(eventCaptor.capture());
+ LogMaker event = eventCaptor.getValue();
+ assertThat(event.getCategory()).isEqualTo(
+ MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET);
+ assertThat(event.getSubtype()).isEqualTo(1);
+ }
+
+ @Test
+ public void testLogDirectShareTargetReceived() {
+ final int category = MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER;
+ final int latency = 123;
+
+ mChooserLogger.logDirectShareTargetReceived(category, latency);
+
+ ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ verify(mMetricsLogger).write(eventCaptor.capture());
+ LogMaker event = eventCaptor.getValue();
+ assertThat(event.getCategory()).isEqualTo(category);
+ assertThat(event.getSubtype()).isEqualTo(latency);
+ }
+
+ @Test
+ public void testLogActionShareWithPreview() {
+ final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT;
+
+ mChooserLogger.logActionShareWithPreview(previewType);
+
+ ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ verify(mMetricsLogger).write(eventCaptor.capture());
+ LogMaker event = eventCaptor.getValue();
+ assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_SHARE_WITH_PREVIEW);
+ assertThat(event.getSubtype()).isEqualTo(previewType);
}
@Test
@@ -194,15 +305,38 @@ public final class ChooserActivityLoggerTest {
public void testDifferentLoggerInstancesUseDifferentInstanceIds() {
ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class);
ChooserActivityLogger chooserLogger2 =
- new ChooserActivityLogger(mUiEventLog, mFrameworkLog);
+ new ChooserActivityLogger(mUiEventLog, mFrameworkLog, mMetricsLogger);
- final int targetType = ChooserActivity.SELECTION_TYPE_COPY;
+ final int targetType = ChooserActivityLogger.SELECTION_TYPE_COPY;
final String packageName = "com.test.foo";
final int positionPicked = 123;
- final boolean pinned = true;
-
- mChooserLogger.logShareTargetSelected(targetType, packageName, positionPicked, pinned);
- chooserLogger2.logShareTargetSelected(targetType, packageName, positionPicked, pinned);
+ final int directTargetAlsoRanked = -1;
+ final int callerTargetCount = 0;
+ final boolean isPinned = true;
+ final boolean isSuccessfullySelected = true;
+ final long selectionCost = 456;
+
+ mChooserLogger.logShareTargetSelected(
+ targetType,
+ packageName,
+ positionPicked,
+ directTargetAlsoRanked,
+ callerTargetCount,
+ /* directTargetHashed= */ null,
+ isPinned,
+ isSuccessfullySelected,
+ selectionCost);
+
+ chooserLogger2.logShareTargetSelected(
+ targetType,
+ packageName,
+ positionPicked,
+ directTargetAlsoRanked,
+ callerTargetCount,
+ /* directTargetHashed= */ null,
+ isPinned,
+ isSuccessfullySelected,
+ selectionCost);
verify(mFrameworkLog, times(2)).write(
anyInt(), anyInt(), anyString(), idIntCaptor.capture(), anyInt(), anyBoolean());
@@ -220,12 +354,26 @@ public final class ChooserActivityLoggerTest {
ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<InstanceId> idObjectCaptor = ArgumentCaptor.forClass(InstanceId.class);
- final int targetType = ChooserActivity.SELECTION_TYPE_COPY;
+ final int targetType = ChooserActivityLogger.SELECTION_TYPE_COPY;
final String packageName = "com.test.foo";
final int positionPicked = 123;
- final boolean pinned = true;
+ final int directTargetAlsoRanked = -1;
+ final int callerTargetCount = 0;
+ final boolean isPinned = true;
+ final boolean isSuccessfullySelected = true;
+ final long selectionCost = 456;
+
+ mChooserLogger.logShareTargetSelected(
+ targetType,
+ packageName,
+ positionPicked,
+ directTargetAlsoRanked,
+ callerTargetCount,
+ /* directTargetHashed= */ null,
+ isPinned,
+ isSuccessfullySelected,
+ selectionCost);
- mChooserLogger.logShareTargetSelected(targetType, packageName, positionPicked, pinned);
verify(mFrameworkLog).write(
anyInt(), anyInt(), anyString(), idIntCaptor.capture(), anyInt(), anyBoolean());
@@ -236,4 +384,23 @@ public final class ChooserActivityLoggerTest {
assertThat(idIntCaptor.getValue()).isGreaterThan(0);
assertThat(idObjectCaptor.getValue().getId()).isEqualTo(idIntCaptor.getValue());
}
+
+ @Test
+ public void testTargetSelectionCategories() {
+ assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+ ChooserActivityLogger.SELECTION_TYPE_SERVICE))
+ .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET);
+ assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+ ChooserActivityLogger.SELECTION_TYPE_APP))
+ .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET);
+ assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+ ChooserActivityLogger.SELECTION_TYPE_STANDARD))
+ .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET);
+ assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+ ChooserActivityLogger.SELECTION_TYPE_COPY)).isEqualTo(0);
+ assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+ ChooserActivityLogger.SELECTION_TYPE_NEARBY)).isEqualTo(0);
+ assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+ ChooserActivityLogger.SELECTION_TYPE_EDIT)).isEqualTo(0);
+ }
}
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
index 5acdb42c..5df0d4a2 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
@@ -32,7 +32,6 @@ import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvi
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.shortcuts.ShortcutLoader;
-import com.android.internal.logging.MetricsLogger;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -66,7 +65,6 @@ public class ChooserActivityOverrideData {
public Cursor resolverCursor;
public boolean resolverForceException;
public Bitmap previewThumbnail;
- public MetricsLogger metricsLogger;
public ChooserActivityLogger chooserActivityLogger;
public int alternateProfileSetting;
public Resources resources;
@@ -89,7 +87,6 @@ public class ChooserActivityOverrideData {
resolverForceException = false;
resolverListController = mock(ResolverListController.class);
workResolverListController = mock(ResolverListController.class);
- metricsLogger = mock(MetricsLogger.class);
chooserActivityLogger = mock(ChooserActivityLogger.class);
alternateProfileSetting = 0;
resources = null;
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index 04e727ba..97de97f5 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -38,13 +38,11 @@ import android.util.Size;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
-import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.shortcuts.ShortcutLoader;
-import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.util.List;
@@ -223,11 +221,6 @@ public class ChooserWrapperActivity
}
@Override
- protected MetricsLogger getMetricsLogger() {
- return sOverrides.metricsLogger;
- }
-
- @Override
public ChooserActivityLogger getChooserActivityLogger() {
return sOverrides.chooserActivityLogger;
}
@@ -256,7 +249,7 @@ public class ChooserWrapperActivity
@Override
public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
- @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ @Nullable TargetPresentationGetter resolveInfoPresentationGetter) {
return DisplayResolveInfo.newDisplayResolveInfo(
originalIntent,
pri,
diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
index 0d44e147..af897a47 100644
--- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java
+++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
@@ -22,7 +22,6 @@ import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
-import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import java.util.concurrent.Executor;
@@ -40,7 +39,7 @@ public interface IChooserWrapper {
UsageStatsManager getUsageStatsManager();
DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
- @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter);
+ @Nullable TargetPresentationGetter resolveInfoPresentationGetter);
UserHandle getCurrentUserHandle();
ChooserActivityLogger getChooserActivityLogger();
Executor getMainExecutor();
diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
index 07cbd6a4..62c16ff5 100644
--- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
@@ -27,7 +27,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.android.intentresolver.MatcherUtils.first;
-import static com.android.intentresolver.ResolverDataProvider.createPackageManagerMockedInfo;
import static com.android.intentresolver.ResolverWrapperActivity.sOverrides;
import static org.hamcrest.CoreMatchers.allOf;
@@ -39,34 +38,25 @@ import static org.mockito.Mockito.when;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.fail;
-import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
-import android.os.Bundle;
-import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
-import android.util.Log;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.test.InstrumentationRegistry;
-import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.NoMatchingViewException;
-import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
-import com.android.internal.R;
import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
-import com.android.intentresolver.ResolverDataProvider.PackageManagerMockedInfo;
-import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
-import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
import com.android.intentresolver.widget.ResolverDrawerLayout;
+import com.android.internal.R;
import org.junit.Before;
import org.junit.Ignore;
@@ -364,50 +354,6 @@ public class ResolverActivityTest {
}
@Test
- public void getActivityLabelAndSubLabel() throws Exception {
- ActivityInfoPresentationGetter pg;
- PackageManagerMockedInfo info;
-
- info = createPackageManagerMockedInfo(false);
- pg = new ActivityInfoPresentationGetter(
- info.ctx, 0, info.activityInfo);
- assertThat("Label should match app label", pg.getLabel().equals(
- info.setAppLabel));
- assertThat("Sublabel should match activity label if set",
- pg.getSubLabel().equals(info.setActivityLabel));
-
- info = createPackageManagerMockedInfo(true);
- pg = new ActivityInfoPresentationGetter(
- info.ctx, 0, info.activityInfo);
- assertThat("With override permission label should match activity label if set",
- pg.getLabel().equals(info.setActivityLabel));
- assertThat("With override permission sublabel should be empty",
- TextUtils.isEmpty(pg.getSubLabel()));
- }
-
- @Test
- public void getResolveInfoLabelAndSubLabel() throws Exception {
- ResolveInfoPresentationGetter pg;
- PackageManagerMockedInfo info;
-
- info = createPackageManagerMockedInfo(false);
- pg = new ResolveInfoPresentationGetter(
- info.ctx, 0, info.resolveInfo);
- assertThat("Label should match app label", pg.getLabel().equals(
- info.setAppLabel));
- assertThat("Sublabel should match resolve info label if set",
- pg.getSubLabel().equals(info.setResolveInfoLabel));
-
- info = createPackageManagerMockedInfo(true);
- pg = new ResolveInfoPresentationGetter(
- info.ctx, 0, info.resolveInfo);
- assertThat("With override permission label should match activity label if set",
- pg.getLabel().equals(info.setActivityLabel));
- assertThat("With override permission the sublabel should be the resolve info label",
- pg.getSubLabel().equals(info.setResolveInfoLabel));
- }
-
- @Test
public void testWorkTab_displayedWhenWorkProfileUserAvailable() {
Intent sendIntent = createSendImageIntent();
markWorkProfileUserAvailable();
diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
index 01d07639..fb928e09 100644
--- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
+++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
@@ -93,11 +93,17 @@ public class ResolverDataProvider {
public String setResolveInfoLabel;
}
+ /** Create a {@link PackageManagerMockedInfo} with all distinct labels. */
static PackageManagerMockedInfo createPackageManagerMockedInfo(boolean hasOverridePermission) {
- final String appLabel = "app_label";
- final String activityLabel = "activity_label";
- final String resolveInfoLabel = "resolve_info_label";
+ return createPackageManagerMockedInfo(
+ hasOverridePermission, "app_label", "activity_label", "resolve_info_label");
+ }
+ static PackageManagerMockedInfo createPackageManagerMockedInfo(
+ boolean hasOverridePermission,
+ String appLabel,
+ String activityLabel,
+ String resolveInfoLabel) {
MockContext ctx = new MockContext() {
@Override
public PackageManager getPackageManager() {
diff --git a/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt
new file mode 100644
index 00000000..e62672a3
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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 com.android.intentresolver.ResolverDataProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+/**
+ * Unit tests for the various implementations of {@link TargetPresentationGetter}.
+ * TODO: consider expanding to cover icon logic (not just labels/sublabels).
+ * TODO: these are conceptually "acceptance tests" that provide comprehensive coverage of the
+ * apparent variations in the legacy implementation. The tests probably don't have to be so
+ * exhaustive if we're able to impose a simpler design on the implementation.
+ */
+class TargetPresentationGetterTest {
+ fun makeResolveInfoPresentationGetter(
+ withSubstitutePermission: Boolean,
+ appLabel: String,
+ activityLabel: String,
+ resolveInfoLabel: String): TargetPresentationGetter {
+ val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo(
+ withSubstitutePermission, appLabel, activityLabel, resolveInfoLabel)
+ val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100)
+ return factory.makePresentationGetter(testPackageInfo.resolveInfo)
+ }
+
+ fun makeActivityInfoPresentationGetter(
+ withSubstitutePermission: Boolean,
+ appLabel: String?,
+ activityLabel: String?): TargetPresentationGetter {
+ val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo(
+ withSubstitutePermission, appLabel, activityLabel, "")
+ val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100)
+ return factory.makePresentationGetter(testPackageInfo.activityInfo)
+ }
+
+ @Test
+ fun testActivityInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(
+ false, "app_label", "activity_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label")
+ }
+
+ @Test
+ fun testActivityInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(
+ false, "app_label", "app_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+ // Without the substitute permission, there's no logic to dedupe the labels.
+ // TODO: this matches our observations in the legacy code, but is it the right behavior? It
+ // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in
+ // the UI at least, but maybe that logic should be pulled back to the "presentation"?
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label")
+ }
+
+ @Test
+ fun testActivityInfoLabels_noSubstitutePermission_nullRequestedLabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(false, null, "activity_label")
+ assertThat(presentationGetter.getLabel()).isNull()
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label")
+ }
+
+ @Test
+ fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedLabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(false, "", "activity_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("")
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label")
+ }
+
+ @Test
+ fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedSublabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(false, "app_label", "")
+ assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+ // Without the substitute permission, empty sublabels are passed through as-is.
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("")
+ }
+
+ @Test
+ fun testActivityInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(
+ true, "app_label", "activity_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+ // With the substitute permission, the same ("activity") label is requested as both the label
+ // and sublabel, even though the other value ("app_label") was distinct. Thus this behaves the
+ // same as a dupe.
+ assertThat(presentationGetter.getSubLabel()).isEqualTo(null)
+ }
+
+ @Test
+ fun testActivityInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(
+ true, "app_label", "app_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+ // With the substitute permission, duped sublabels get converted to nulls.
+ assertThat(presentationGetter.getSubLabel()).isNull()
+ }
+
+ @Test
+ fun testActivityInfoLabels_withSubstitutePermission_nullRequestedLabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", null)
+ assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+ // With the substitute permission, null inputs are a special case that produces null outputs
+ // (i.e., they're not simply passed-through from the inputs).
+ assertThat(presentationGetter.getSubLabel()).isNull()
+ }
+
+ @Test
+ fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedLabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", "")
+ // Empty "labels" are taken as-is and (unlike nulls) don't prompt a fallback to the sublabel.
+ // Thus (as in the previous case with substitute permission & "distinct" labels), this is
+ // treated as a dupe.
+ assertThat(presentationGetter.getLabel()).isEqualTo("")
+ assertThat(presentationGetter.getSubLabel()).isNull()
+ }
+
+ @Test
+ fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedSublabel() {
+ val presentationGetter = makeActivityInfoPresentationGetter(true, "", "activity_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+ // With the substitute permission, empty sublabels get converted to nulls.
+ assertThat(presentationGetter.getSubLabel()).isNull()
+ }
+
+ @Test
+ fun testResolveInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() {
+ val presentationGetter = makeResolveInfoPresentationGetter(
+ false, "app_label", "activity_label", "resolve_info_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label")
+ }
+
+ @Test
+ fun testResolveInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() {
+ val presentationGetter = makeResolveInfoPresentationGetter(
+ false, "app_label", "activity_label", "app_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+ // Without the substitute permission, there's no logic to dedupe the labels.
+ // TODO: this matches our observations in the legacy code, but is it the right behavior? It
+ // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in
+ // the UI at least, but maybe that logic should be pulled back to the "presentation"?
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label")
+ }
+
+ @Test
+ fun testResolveInfoLabels_noSubstitutePermission_emptyRequestedSublabel() {
+ val presentationGetter = makeResolveInfoPresentationGetter(
+ false, "app_label", "activity_label", "")
+ assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+ // Without the substitute permission, empty sublabels are passed through as-is.
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("")
+ }
+
+ @Test
+ fun testResolveInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() {
+ val presentationGetter = makeResolveInfoPresentationGetter(
+ true, "app_label", "activity_label", "resolve_info_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+ assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label")
+ }
+
+ @Test
+ fun testResolveInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() {
+ val presentationGetter = makeResolveInfoPresentationGetter(
+ true, "app_label", "activity_label", "activity_label")
+ assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+ // With the substitute permission, duped sublabels get converted to nulls.
+ assertThat(presentationGetter.getSubLabel()).isNull()
+ }
+
+ @Test
+ fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedSublabel() {
+ val presentationGetter = makeResolveInfoPresentationGetter(
+ true, "app_label", "activity_label", "")
+ assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+ // With the substitute permission, empty sublabels get converted to nulls.
+ assertThat(presentationGetter.getSubLabel()).isNull()
+ }
+
+ @Test
+ fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedLabelAndSublabel() {
+ val presentationGetter = makeResolveInfoPresentationGetter(
+ true, "app_label", "", "")
+ assertThat(presentationGetter.getLabel()).isEqualTo("")
+ // With the substitute permission, empty sublabels get converted to nulls.
+ assertThat(presentationGetter.getSubLabel()).isNull()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index ff166fb7..af2557ef 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -43,15 +43,13 @@ import static junit.framework.Assert.assertNull;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -77,12 +75,12 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Icon;
-import android.metrics.LogMaker;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
+import android.util.HashedStringCache;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
@@ -99,7 +97,6 @@ import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import org.hamcrest.Description;
@@ -786,26 +783,15 @@ public class UnbundledChooserActivityTest {
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed()));
onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click());
- verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
-
- // The last captured event is the selection of the target.
- boolean containsTargetEvent = logMakerCaptor.getAllValues()
- .stream()
- .anyMatch(item ->
- item.getCategory()
- == MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET);
- assertTrue(
- "ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET is expected", containsTargetEvent);
- assertThat(logMakerCaptor.getValue().getSubtype(), is(1));
+ ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ verify(logger, times(1)).logActionSelected(eq(ChooserActivityLogger.SELECTION_TYPE_COPY));
}
@Test
@@ -979,25 +965,12 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
- waitForIdle();
- verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
- assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
- is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
- assertThat(logMakerCaptor
- .getAllValues().get(0)
- .getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS),
- is(notNullValue()));
- assertThat(logMakerCaptor
- .getAllValues().get(0)
- .getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE),
- is(TEST_MIME_TYPE));
- assertThat(logMakerCaptor
- .getAllValues().get(0)
- .getSubtype(),
- is(MetricsEvent.PARENT_PROFILE));
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+ ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ waitForIdle();
+
+ verify(logger).logChooserActivityShown(eq(false), eq(TEST_MIME_TYPE), anyLong());
}
@Test
@@ -1006,49 +979,32 @@ public class UnbundledChooserActivityTest {
sendIntent.setType(TEST_MIME_TYPE);
ChooserActivityOverrideData.getInstance().alternateProfileSetting =
MetricsEvent.MANAGED_PROFILE;
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
- waitForIdle();
- verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
- assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
- is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
- assertThat(logMakerCaptor
- .getAllValues().get(0)
- .getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS),
- is(notNullValue()));
- assertThat(logMakerCaptor
- .getAllValues().get(0)
- .getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE),
- is(TEST_MIME_TYPE));
- assertThat(logMakerCaptor
- .getAllValues().get(0)
- .getSubtype(),
- is(MetricsEvent.MANAGED_PROFILE));
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+ ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ waitForIdle();
+
+ verify(logger).logChooserActivityShown(eq(true), eq(TEST_MIME_TYPE), anyLong());
}
@Test
public void testEmptyPreviewLogging() {
Intent sendIntent = createSendTextIntentWithPreview(null, null);
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, "empty preview logger test"));
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(
+ Intent.createChooser(sendIntent, "empty preview logger test"));
+ ChooserActivityLogger logger = activity.getChooserActivityLogger();
waitForIdle();
- verify(mockLogger, Mockito.times(1)).write(logMakerCaptor.capture());
- // First invocation is from onCreate
- assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
- is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
+ verify(logger).logChooserActivityShown(eq(false), eq(null), anyLong());
}
@Test
public void testTitlePreviewLogging() {
Intent sendIntent = createSendTextIntentWithPreview("TestTitle", null);
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
@@ -1057,14 +1013,13 @@ public class UnbundledChooserActivityTest {
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
+
// Second invocation is from onCreate
- verify(mockLogger, Mockito.times(2)).write(logMakerCaptor.capture());
- assertThat(logMakerCaptor.getAllValues().get(0).getSubtype(),
- is(CONTENT_PREVIEW_TEXT));
- assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
- is(MetricsEvent.ACTION_SHARE_WITH_PREVIEW));
+ ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_TEXT));
}
@Test
@@ -1092,16 +1047,11 @@ public class UnbundledChooserActivityTest {
Mockito.isA(List.class)))
.thenReturn(resolvedComponentInfos);
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
- verify(mockLogger, Mockito.times(2)).write(logMakerCaptor.capture());
- // First invocation is from onCreate
- assertThat(logMakerCaptor.getAllValues().get(0).getSubtype(),
- is(CONTENT_PREVIEW_IMAGE));
- assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
- is(MetricsEvent.ACTION_SHARE_WITH_PREVIEW));
+ ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_IMAGE));
}
@Test
@@ -1302,10 +1252,6 @@ public class UnbundledChooserActivityTest {
Mockito.isA(List.class)))
.thenReturn(resolvedComponentInfos);
- // Set up resources
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-
// create test shortcut loader factory, remember loaders and their callbacks
SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
createShortcutLoaderFactory();
@@ -1361,25 +1307,22 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- // Currently we're seeing 4 invocations
- // 1. ChooserActivity.logActionShareWithPreview()
- // 2. ChooserActivity.onCreate()
- // 3. ChooserActivity.logDirectShareTargetReceived()
- // 4. ChooserActivity.startSelected -- which is the one we're after
- verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture());
- LogMaker selectionLog = logMakerCaptor.getAllValues().get(3);
- assertThat(
- selectionLog.getCategory(),
- is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
- String hashedName = (String) selectionLog.getTaggedData(
- MetricsEvent.FIELD_HASHED_TARGET_NAME);
+ ArgumentCaptor<HashedStringCache.HashResult> hashCaptor =
+ ArgumentCaptor.forClass(HashedStringCache.HashResult.class);
+ verify(activity.getChooserActivityLogger(), times(1)).logShareTargetSelected(
+ eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+ /* packageName= */ any(),
+ /* positionPicked= */ anyInt(),
+ /* directTargetAlsoRanked= */ eq(-1),
+ /* numCallerProvided= */ anyInt(),
+ /* directTargetHashed= */ hashCaptor.capture(),
+ /* isPinned= */ anyBoolean(),
+ /* successfullySelected= */ anyBoolean(),
+ /* selectionCost= */ anyLong());
+ String hashedName = hashCaptor.getValue().hashedString;
assertThat(
"Hash is not predictable but must be obfuscated",
hashedName, is(not(name)));
- assertThat(
- "The packages shouldn't match for app target and direct target",
- selectionLog.getTaggedData(MetricsEvent.FIELD_RANKED_POSITION),
- is(-1));
}
// This test is too long and too slow and should not be taken as an example for future tests.
@@ -1399,10 +1342,6 @@ public class UnbundledChooserActivityTest {
Mockito.isA(List.class)))
.thenReturn(resolvedComponentInfos);
- // Set up resources
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-
// create test shortcut loader factory, remember loaders and their callbacks
SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
createShortcutLoaderFactory();
@@ -1460,16 +1399,16 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- // Currently we're seeing 4 invocations
- // 1. ChooserActivity.logActionShareWithPreview()
- // 2. ChooserActivity.onCreate()
- // 3. ChooserActivity.logDirectShareTargetReceived()
- // 4. ChooserActivity.startSelected -- which is the one we're after
- verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture());
- assertThat(logMakerCaptor.getAllValues().get(3).getCategory(),
- is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
- assertThat("The packages should match for app target and direct target", logMakerCaptor
- .getAllValues().get(3).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0));
+ verify(activity.getChooserActivityLogger(), times(1)).logShareTargetSelected(
+ eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+ /* packageName= */ any(),
+ /* positionPicked= */ anyInt(),
+ /* directTargetAlsoRanked= */ eq(0),
+ /* numCallerProvided= */ anyInt(),
+ /* directTargetHashed= */ any(),
+ /* isPinned= */ anyBoolean(),
+ /* successfullySelected= */ anyBoolean(),
+ /* selectionCost= */ anyLong());
}
@Test
@@ -1787,9 +1726,6 @@ public class UnbundledChooserActivityTest {
Mockito.isA(List.class)))
.thenReturn(resolvedComponentInfos);
- // Set up resources
- MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
- ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
// Create direct share target
List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName);
@@ -1830,15 +1766,18 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- // Currently we're seeing 3 invocations
- // 1. ChooserActivity.onCreate()
- // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
- // 3. ChooserActivity.startSelected -- which is the one we're after
- verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
- assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
- is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
- assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor
- .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1));
+ ChooserActivityLogger logger = wrapper.getChooserActivityLogger();
+ verify(logger, times(1)).logShareTargetSelected(
+ eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+ /* packageName= */ any(),
+ /* positionPicked= */ anyInt(),
+ // The packages sholdn't match for app target and direct target:
+ /* directTargetAlsoRanked= */ eq(-1),
+ /* numCallerProvided= */ anyInt(),
+ /* directTargetHashed= */ any(),
+ /* isPinned= */ anyBoolean(),
+ /* successfullySelected= */ anyBoolean(),
+ /* selectionCost= */ anyLong());
}
@Test
@@ -2179,9 +2118,16 @@ public class UnbundledChooserActivityTest {
ChooserActivityLogger logger = activity.getChooserActivityLogger();
ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class);
- Mockito.verify(logger, times(1))
- .logShareTargetSelected(typeCaptor.capture(), any(), anyInt(), anyBoolean());
- assertThat(typeCaptor.getValue(), is(ChooserActivity.SELECTION_TYPE_SERVICE));
+ verify(logger, times(1)).logShareTargetSelected(
+ eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+ /* packageName= */ any(),
+ /* positionPicked= */ anyInt(),
+ /* directTargetAlsoRanked= */ anyInt(),
+ /* numCallerProvided= */ anyInt(),
+ /* directTargetHashed= */ any(),
+ /* isPinned= */ anyBoolean(),
+ /* successfullySelected= */ anyBoolean(),
+ /* selectionCost= */ anyLong());
}
@Test @Ignore