| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.intentresolver; |
| |
| import 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.intentresolver.contentpreview.ContentPreviewType; |
| 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; |
| |
| /** |
| * Helper for writing Sharesheet atoms to statsd log. |
| * @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; |
| public static final int SELECTION_TYPE_MODIFY_SHARE = 7; |
| public static final int SELECTION_TYPE_CUSTOM_ACTION = 8; |
| |
| /** |
| * This shim is provided only for testing. In production, clients will only ever use a |
| * {@link DefaultFrameworkStatsLogger}. |
| */ |
| @VisibleForTesting |
| interface FrameworkStatsLogger { |
| /** Overload to use for logging {@code FrameworkStatsLog.SHARESHEET_STARTED}. */ |
| void write( |
| int frameworkEventId, |
| int appEventId, |
| String packageName, |
| int instanceId, |
| String mimeType, |
| int numAppProvidedDirectTargets, |
| int numAppProvidedAppTargets, |
| boolean isWorkProfile, |
| int previewType, |
| int intentType, |
| int numCustomActions, |
| boolean modifyShareActionProvided); |
| |
| /** Overload to use for logging {@code FrameworkStatsLog.RANKING_SELECTED}. */ |
| void write( |
| int frameworkEventId, |
| int appEventId, |
| String packageName, |
| int instanceId, |
| int positionPicked, |
| boolean isPinned); |
| } |
| |
| private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13); |
| |
| // A small per-notification ID, used for statsd logging. |
| // TODO: consider precomputing and storing as final. |
| private static InstanceIdSequence sInstanceIdSequence; |
| private InstanceId mInstanceId; |
| |
| private final UiEventLogger mUiEventLogger; |
| private final FrameworkStatsLogger mFrameworkStatsLogger; |
| private final MetricsLogger mMetricsLogger; |
| |
| public ChooserActivityLogger() { |
| this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger()); |
| } |
| |
| @VisibleForTesting |
| ChooserActivityLogger( |
| UiEventLogger uiEventLogger, |
| FrameworkStatsLogger frameworkLogger, |
| MetricsLogger metricsLogger) { |
| mUiEventLogger = uiEventLogger; |
| mFrameworkStatsLogger = frameworkLogger; |
| mMetricsLogger = metricsLogger; |
| } |
| |
| /** Records metrics for the start time of the {@link ChooserActivity}. */ |
| public void logChooserActivityShown( |
| boolean isWorkProfile, String targetMimeType, long systemCost) { |
| mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN) |
| .setSubtype( |
| isWorkProfile ? MetricsEvent.MANAGED_PROFILE : MetricsEvent.PARENT_PROFILE) |
| .addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, targetMimeType) |
| .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost)); |
| } |
| |
| /** Logs a UiEventReported event for the system sharesheet completing initial start-up. */ |
| public void logShareStarted( |
| String packageName, |
| String mimeType, |
| int appProvidedDirect, |
| int appProvidedApp, |
| boolean isWorkprofile, |
| int previewType, |
| String intent, |
| int customActionCount, |
| boolean modifyShareActionProvided) { |
| mFrameworkStatsLogger.write(FrameworkStatsLog.SHARESHEET_STARTED, |
| /* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(), |
| /* package_name = 2 */ packageName, |
| /* instance_id = 3 */ getInstanceId().getId(), |
| /* mime_type = 4 */ mimeType, |
| /* num_app_provided_direct_targets = 5 */ appProvidedDirect, |
| /* num_app_provided_app_targets = 6 */ appProvidedApp, |
| /* is_workprofile = 7 */ isWorkprofile, |
| /* previewType = 8 */ typeFromPreviewInt(previewType), |
| /* intentType = 9 */ typeFromIntentString(intent), |
| /* num_provided_custom_actions = 10 */ customActionCount, |
| /* modify_share_action_provided = 11 */ modifyShareActionProvided); |
| } |
| |
| /** |
| * Log that a custom action has been tapped by the user. |
| * |
| * @param positionPicked index of the custom action within the list of custom actions. |
| */ |
| public void logCustomActionSelected(int positionPicked) { |
| mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED, |
| /* event_id = 1 */ |
| SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId(), |
| /* package_name = 2 */ null, |
| /* instance_id = 3 */ getInstanceId().getId(), |
| /* position_picked = 4 */ positionPicked, |
| /* is_pinned = 5 */ false); |
| } |
| |
| /** |
| * 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. */ |
| public void logSharesheetTriggered() { |
| log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, getInstanceId()); |
| } |
| |
| /** Logs a UiEventReported event for the system sharesheet completing loading app targets. */ |
| public void logSharesheetAppLoadComplete() { |
| log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, getInstanceId()); |
| } |
| |
| /** |
| * Logs a UiEventReported event for the system sharesheet completing loading service targets. |
| */ |
| public void logSharesheetDirectLoadComplete() { |
| log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, getInstanceId()); |
| } |
| |
| /** |
| * Logs a UiEventReported event for the system sharesheet timing out loading service targets. |
| */ |
| public void logSharesheetDirectLoadTimeout() { |
| log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, getInstanceId()); |
| } |
| |
| /** |
| * Logs a UiEventReported event for the system sharesheet switching |
| * between work and main profile. |
| */ |
| public void logSharesheetProfileChanged() { |
| log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, getInstanceId()); |
| } |
| |
| /** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */ |
| public void logSharesheetExpansionChanged(boolean isCollapsed) { |
| log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED : |
| SharesheetStandardEvent.SHARESHEET_EXPANDED, getInstanceId()); |
| } |
| |
| /** |
| * Logs a UiEventReported event for the system sharesheet app share ranking timing out. |
| */ |
| public void logSharesheetAppShareRankingTimeout() { |
| log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, getInstanceId()); |
| } |
| |
| /** |
| * Logs a UiEventReported event for the system sharesheet when direct share row is empty. |
| */ |
| public void logSharesheetEmptyDirectShareRow() { |
| log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, getInstanceId()); |
| } |
| |
| /** |
| * Logs a UiEventReported event for a given share activity |
| * @param event |
| * @param instanceId |
| */ |
| private void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) { |
| mUiEventLogger.logWithInstanceId( |
| event, |
| 0, |
| null, |
| instanceId); |
| } |
| |
| /** |
| * @return A unique {@link InstanceId} to join across events recorded by this logger instance. |
| */ |
| private InstanceId getInstanceId() { |
| if (mInstanceId == null) { |
| if (sInstanceIdSequence == null) { |
| sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX); |
| } |
| mInstanceId = sInstanceIdSequence.newInstanceId(); |
| } |
| return mInstanceId; |
| } |
| |
| /** |
| * The UiEvent enums that this class can log. |
| */ |
| enum SharesheetStartedEvent implements UiEventLogger.UiEventEnum { |
| @UiEvent(doc = "Basic system Sharesheet has started and is visible.") |
| SHARE_STARTED(228); |
| |
| private final int mId; |
| SharesheetStartedEvent(int id) { |
| mId = id; |
| } |
| @Override |
| public int getId() { |
| return mId; |
| } |
| } |
| |
| /** |
| * The UiEvent enums that this class can log. |
| */ |
| enum SharesheetTargetSelectedEvent implements UiEventLogger.UiEventEnum { |
| INVALID(0), |
| @UiEvent(doc = "User selected a service target.") |
| SHARESHEET_SERVICE_TARGET_SELECTED(232), |
| @UiEvent(doc = "User selected an app target.") |
| SHARESHEET_APP_TARGET_SELECTED(233), |
| @UiEvent(doc = "User selected a standard target.") |
| SHARESHEET_STANDARD_TARGET_SELECTED(234), |
| @UiEvent(doc = "User selected the copy target.") |
| SHARESHEET_COPY_TARGET_SELECTED(235), |
| @UiEvent(doc = "User selected the nearby target.") |
| SHARESHEET_NEARBY_TARGET_SELECTED(626), |
| @UiEvent(doc = "User selected the edit target.") |
| SHARESHEET_EDIT_TARGET_SELECTED(669), |
| @UiEvent(doc = "User selected the modify share target.") |
| SHARESHEET_MODIFY_SHARE_SELECTED(1316), |
| @UiEvent(doc = "User selected a custom action.") |
| SHARESHEET_CUSTOM_ACTION_SELECTED(1317); |
| |
| private final int mId; |
| SharesheetTargetSelectedEvent(int id) { |
| mId = id; |
| } |
| @Override public int getId() { |
| return mId; |
| } |
| |
| public static SharesheetTargetSelectedEvent fromTargetType(int targetType) { |
| switch(targetType) { |
| case SELECTION_TYPE_SERVICE: |
| return SHARESHEET_SERVICE_TARGET_SELECTED; |
| case SELECTION_TYPE_APP: |
| return SHARESHEET_APP_TARGET_SELECTED; |
| case SELECTION_TYPE_STANDARD: |
| return SHARESHEET_STANDARD_TARGET_SELECTED; |
| case SELECTION_TYPE_COPY: |
| return SHARESHEET_COPY_TARGET_SELECTED; |
| case SELECTION_TYPE_NEARBY: |
| return SHARESHEET_NEARBY_TARGET_SELECTED; |
| case SELECTION_TYPE_EDIT: |
| return SHARESHEET_EDIT_TARGET_SELECTED; |
| case SELECTION_TYPE_MODIFY_SHARE: |
| return SHARESHEET_MODIFY_SHARE_SELECTED; |
| case SELECTION_TYPE_CUSTOM_ACTION: |
| return SHARESHEET_CUSTOM_ACTION_SELECTED; |
| default: |
| return INVALID; |
| } |
| } |
| } |
| |
| /** |
| * The UiEvent enums that this class can log. |
| */ |
| enum SharesheetStandardEvent implements UiEventLogger.UiEventEnum { |
| INVALID(0), |
| @UiEvent(doc = "User clicked share.") |
| SHARESHEET_TRIGGERED(227), |
| @UiEvent(doc = "User changed from work to personal profile or vice versa.") |
| SHARESHEET_PROFILE_CHANGED(229), |
| @UiEvent(doc = "User expanded target list.") |
| SHARESHEET_EXPANDED(230), |
| @UiEvent(doc = "User collapsed target list.") |
| SHARESHEET_COLLAPSED(231), |
| @UiEvent(doc = "Sharesheet app targets is fully populated.") |
| SHARESHEET_APP_LOAD_COMPLETE(322), |
| @UiEvent(doc = "Sharesheet direct targets is fully populated.") |
| SHARESHEET_DIRECT_LOAD_COMPLETE(323), |
| @UiEvent(doc = "Sharesheet direct targets timed out.") |
| SHARESHEET_DIRECT_LOAD_TIMEOUT(324), |
| @UiEvent(doc = "Sharesheet app share ranking timed out.") |
| SHARESHEET_APP_SHARE_RANKING_TIMEOUT(831), |
| @UiEvent(doc = "Sharesheet empty direct share row.") |
| SHARESHEET_EMPTY_DIRECT_SHARE_ROW(828); |
| |
| private final int mId; |
| SharesheetStandardEvent(int id) { |
| mId = id; |
| } |
| @Override public int getId() { |
| return mId; |
| } |
| } |
| |
| /** |
| * Returns the enum used in sharesheet started atom to indicate what preview type was used. |
| */ |
| private static int typeFromPreviewInt(int previewType) { |
| switch(previewType) { |
| case ContentPreviewType.CONTENT_PREVIEW_IMAGE: |
| return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_IMAGE; |
| case ContentPreviewType.CONTENT_PREVIEW_FILE: |
| return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE; |
| case ContentPreviewType.CONTENT_PREVIEW_TEXT: |
| default: |
| return FrameworkStatsLog |
| .SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_TYPE_UNKNOWN; |
| } |
| } |
| |
| /** |
| * Returns the enum used in sharesheet started atom to indicate what intent triggers the |
| * ChooserActivity. |
| */ |
| private static int typeFromIntentString(String intent) { |
| if (intent == null) { |
| return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT; |
| } |
| switch (intent) { |
| case Intent.ACTION_VIEW: |
| return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_VIEW; |
| case Intent.ACTION_EDIT: |
| return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_EDIT; |
| case Intent.ACTION_SEND: |
| return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SEND; |
| case Intent.ACTION_SENDTO: |
| return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO; |
| case Intent.ACTION_SEND_MULTIPLE: |
| return FrameworkStatsLog |
| .SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SEND_MULTIPLE; |
| case MediaStore.ACTION_IMAGE_CAPTURE: |
| return FrameworkStatsLog |
| .SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_IMAGE_CAPTURE; |
| case Intent.ACTION_MAIN: |
| return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_MAIN; |
| default: |
| return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT; |
| } |
| } |
| |
| @VisibleForTesting |
| static int getTargetSelectionCategory(int targetType) { |
| switch (targetType) { |
| case SELECTION_TYPE_SERVICE: |
| return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET; |
| case SELECTION_TYPE_APP: |
| return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET; |
| case SELECTION_TYPE_STANDARD: |
| return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET; |
| default: |
| return 0; |
| } |
| } |
| |
| private static class DefaultFrameworkStatsLogger implements FrameworkStatsLogger { |
| @Override |
| public void write( |
| int frameworkEventId, |
| int appEventId, |
| String packageName, |
| int instanceId, |
| String mimeType, |
| int numAppProvidedDirectTargets, |
| int numAppProvidedAppTargets, |
| boolean isWorkProfile, |
| int previewType, |
| int intentType, |
| int numCustomActions, |
| boolean modifyShareActionProvided) { |
| FrameworkStatsLog.write( |
| frameworkEventId, |
| /* event_id = 1 */ appEventId, |
| /* package_name = 2 */ packageName, |
| /* instance_id = 3 */ instanceId, |
| /* mime_type = 4 */ mimeType, |
| /* num_app_provided_direct_targets */ numAppProvidedDirectTargets, |
| /* num_app_provided_app_targets */ numAppProvidedAppTargets, |
| /* is_workprofile */ isWorkProfile, |
| /* previewType = 8 */ previewType, |
| /* intentType = 9 */ intentType, |
| /* num_provided_custom_actions = 10 */ numCustomActions, |
| /* modify_share_action_provided = 11 */ modifyShareActionProvided); |
| } |
| |
| @Override |
| public void write( |
| int frameworkEventId, |
| int appEventId, |
| String packageName, |
| int instanceId, |
| int positionPicked, |
| boolean isPinned) { |
| FrameworkStatsLog.write( |
| frameworkEventId, |
| /* event_id = 1 */ appEventId, |
| /* package_name = 2 */ packageName, |
| /* instance_id = 3 */ instanceId, |
| /* position_picked = 4 */ positionPicked, |
| /* is_pinned = 5 */ isPinned); |
| } |
| } |
| } |