/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.intentresolver.logging; 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 androidx.annotation.Nullable; import com.android.intentresolver.ChooserActivity; 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.nano.MetricsProto.MetricsEvent; import com.android.internal.util.FrameworkStatsLog; import javax.inject.Inject; /** * Helper for writing Sharesheet atoms to statsd log. */ public class EventLogImpl implements EventLog { private static final String TAG = "ChooserActivity"; private static final boolean DEBUG = true; private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13); private final InstanceId mInstanceId; private final UiEventLogger mUiEventLogger; private final FrameworkStatsLogger mFrameworkStatsLogger; private final MetricsLogger mMetricsLogger; public static InstanceIdSequence newIdSequence() { return new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX); } @Inject public EventLogImpl(UiEventLogger uiEventLogger, FrameworkStatsLogger frameworkLogger, MetricsLogger metricsLogger, InstanceId instanceId) { mUiEventLogger = uiEventLogger; mFrameworkStatsLogger = frameworkLogger; mMetricsLogger = metricsLogger; mInstanceId = instanceId; } /** Records metrics for the start time of the {@link ChooserActivity}. */ @Override 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. */ @Override 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 */ mInstanceId.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. */ @Override 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 */ mInstanceId.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. */ @Override 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 */ mInstanceId.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. */ @Override 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. */ @Override 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}. */ @Override 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 */ mInstanceId.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}. */ @Override 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. */ @Override public void logSharesheetTriggered() { log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, mInstanceId); } /** Logs a UiEventReported event for the system sharesheet completing loading app targets. */ @Override public void logSharesheetAppLoadComplete() { log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, mInstanceId); } /** * Logs a UiEventReported event for the system sharesheet completing loading service targets. */ @Override public void logSharesheetDirectLoadComplete() { log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, mInstanceId); } /** * Logs a UiEventReported event for the system sharesheet timing out loading service targets. */ @Override public void logSharesheetDirectLoadTimeout() { log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, mInstanceId); } /** * Logs a UiEventReported event for the system sharesheet switching * between work and main profile. */ @Override public void logSharesheetProfileChanged() { log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, mInstanceId); } /** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */ @Override public void logSharesheetExpansionChanged(boolean isCollapsed) { log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED : SharesheetStandardEvent.SHARESHEET_EXPANDED, mInstanceId); } /** * Logs a UiEventReported event for the system sharesheet app share ranking timing out. */ @Override public void logSharesheetAppShareRankingTimeout() { log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, mInstanceId); } /** * Logs a UiEventReported event for the system sharesheet when direct share row is empty. */ @Override public void logSharesheetEmptyDirectShareRow() { log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, mInstanceId); } /** * 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); } /** * 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), @UiEvent(doc = "Shareousel payload item toggled") SHARESHEET_PAYLOAD_TOGGLED(1662); 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; } } }