blob: 1f606f2665a661083e0fbabd52746649e14c6b26 [file] [log] [blame]
/*
* 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);
}
}
}