diff options
Diffstat (limited to 'java')
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 |