diff options
10 files changed, 746 insertions, 10 deletions
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index dca682e4ee29..10c742817d0a 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -124,6 +124,7 @@ 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; import com.android.internal.widget.GridLayoutManager; import com.android.internal.widget.RecyclerView; import com.android.internal.widget.ResolverDrawerLayout; @@ -178,7 +179,7 @@ public class ChooserActivity extends ResolverActivity implements private static final boolean USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES = true; // TODO(b/123088566) Share these in a better way. private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share"; - public static final String LAUNCH_LOCATON_DIRECT_SHARE = "direct_share"; + public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20; public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter"; @@ -194,6 +195,14 @@ 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; + + // statsd logger wrapper + protected ChooserActivityLogger mChooserActivityLogger; + private static final boolean USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS = true; @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = { @@ -270,9 +279,9 @@ public class ChooserActivity extends ResolverActivity implements // Starting at 1 since 0 is considered "undefined" for some of the database transformations // of tron logs. - private static final int CONTENT_PREVIEW_IMAGE = 1; - private static final int CONTENT_PREVIEW_FILE = 2; - private static final int CONTENT_PREVIEW_TEXT = 3; + protected static final int CONTENT_PREVIEW_IMAGE = 1; + protected static final int CONTENT_PREVIEW_FILE = 2; + protected static final int CONTENT_PREVIEW_TEXT = 3; protected MetricsLogger mMetricsLogger; private ContentPreviewCoordinator mPreviewCoord; @@ -500,6 +509,9 @@ public class ChooserActivity extends ResolverActivity implements case CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT: mMinTimeoutPassed = true; + if (!mServiceConnections.isEmpty()) { + getChooserActivityLogger().logSharesheetDirectLoadTimeout(); + } unbindRemainingServices(); maybeStopServiceRequestTimer(); break; @@ -533,6 +545,7 @@ public class ChooserActivity extends ResolverActivity implements logDirectShareTargetReceived( MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER); sendVoiceChoicesIfNeeded(); + getChooserActivityLogger().logSharesheetDirectLoadComplete(); break; default: @@ -544,6 +557,7 @@ public class ChooserActivity extends ResolverActivity implements @Override protected void onCreate(Bundle savedInstanceState) { final long intentReceivedTime = System.currentTimeMillis(); + getChooserActivityLogger().logSharesheetTriggered(); // This is the only place this value is being set. Effectively final. mIsAppPredictorComponentAvailable = isAppPredictionServiceAvailable(); @@ -707,6 +721,8 @@ public class ChooserActivity extends ResolverActivity implements incrementNumSheetExpansions(); mWrittenOnce = true; } + getChooserActivityLogger() + .logSharesheetExpansionChanged(isCollapsed); } }); } @@ -715,6 +731,16 @@ public class ChooserActivity extends ResolverActivity implements Log.d(TAG, "System Time Cost is " + systemCost); } + getChooserActivityLogger().logShareStarted( + FrameworkStatsLog.SHARESHEET_STARTED, + getReferrerPackageName(), + target.getType(), + initialIntents == null ? 0 : initialIntents.length, + mCallerChooserTargets == null ? 0 : mCallerChooserTargets.length, + isWorkProfile(), + findPreferredContentPreview(getTargetIntent(), getContentResolver()), + target.getAction() + ); mDirectShareShortcutInfoCache = new HashMap<>(); } @@ -969,6 +995,10 @@ public class ChooserActivity extends ResolverActivity implements LogMaker targetLogMaker = new LogMaker( MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET).setSubtype(1); getMetricsLogger().write(targetLogMaker); + getChooserActivityLogger().logShareTargetSelected( + SELECTION_TYPE_COPY, + "", + -1); finish(); } @@ -1644,18 +1674,33 @@ public class ChooserActivity extends ResolverActivity implements if (mCallerChooserTargets != null) { numCallerProvided = mCallerChooserTargets.length; } + getChooserActivityLogger().logShareTargetSelected( + SELECTION_TYPE_SERVICE, + targetInfo.getResolveInfo().activityInfo.processName, + value + ); break; case ChooserListAdapter.TARGET_CALLER: case ChooserListAdapter.TARGET_STANDARD: cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET; value -= currentListAdapter.getSelectableServiceTargetCount(); numCallerProvided = currentListAdapter.getCallerTargetCount(); + getChooserActivityLogger().logShareTargetSelected( + SELECTION_TYPE_APP, + targetInfo.getResolveInfo().activityInfo.processName, + value + ); break; 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; + getChooserActivityLogger().logShareTargetSelected( + SELECTION_TYPE_STANDARD, + targetInfo.getResolveInfo().activityInfo.processName, + value + ); break; } @@ -2131,7 +2176,7 @@ public class ChooserActivity extends ResolverActivity implements if (appTarget != null) { directShareAppPredictor.notifyAppTargetEvent( new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) - .setLaunchLocation(LAUNCH_LOCATON_DIRECT_SHARE) + .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE) .build()); } } @@ -2290,6 +2335,13 @@ public class ChooserActivity extends ResolverActivity implements return mMetricsLogger; } + protected ChooserActivityLogger getChooserActivityLogger() { + if (mChooserActivityLogger == null) { + mChooserActivityLogger = new ChooserActivityLoggerImpl(); + } + return mChooserActivityLogger; + } + public class ChooserListController extends ResolverListController { public ChooserListController(Context context, PackageManager pm, @@ -2601,6 +2653,7 @@ public class ChooserActivity extends ResolverActivity implements // don't support direct share on low ram devices if (ActivityManager.isLowRamDeviceStatic()) { + getChooserActivityLogger().logSharesheetAppLoadComplete(); return; } @@ -2619,6 +2672,8 @@ public class ChooserActivity extends ResolverActivity implements queryTargetServices(chooserListAdapter); } + + getChooserActivityLogger().logSharesheetAppLoadComplete(); } private void setupScrollListener() { @@ -3777,4 +3832,9 @@ public class ChooserActivity extends ResolverActivity implements canvas.drawRoundRect(x, y, width, height, mRadius, mRadius, mRoundRectPaint); } } + + @Override + protected void maybeLogProfileChange() { + getChooserActivityLogger().logShareheetProfileChanged(); + } } diff --git a/core/java/com/android/internal/app/ChooserActivityLogger.java b/core/java/com/android/internal/app/ChooserActivityLogger.java new file mode 100644 index 000000000000..dc482443040a --- /dev/null +++ b/core/java/com/android/internal/app/ChooserActivityLogger.java @@ -0,0 +1,215 @@ +/* + * 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.internal.app; + +import android.content.Intent; +import android.provider.MediaStore; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.util.FrameworkStatsLog; + +/** + * Interface for writing Sharesheet atoms to statsd log. + * @hide + */ +public interface ChooserActivityLogger { + /** Logs a UiEventReported event for the system sharesheet completing initial start-up. */ + void logShareStarted(int eventId, String packageName, String mimeType, int appProvidedDirect, + int appProvidedApp, boolean isWorkprofile, int previewType, String intent); + + /** Logs a UiEventReported event for the system sharesheet when the user selects a target. */ + void logShareTargetSelected(int targetType, String packageName, int positionPicked); + + /** Logs a UiEventReported event for the system sharesheet being triggered by the user. */ + default void logSharesheetTriggered() { + log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, getInstanceId()); + } + + /** Logs a UiEventReported event for the system sharesheet completing loading app targets. */ + default void logSharesheetAppLoadComplete() { + log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, getInstanceId()); + } + + /** + * Logs a UiEventReported event for the system sharesheet completing loading service targets. + */ + default void logSharesheetDirectLoadComplete() { + log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, getInstanceId()); + } + + /** + * Logs a UiEventReported event for the system sharesheet timing out loading service targets. + */ + default void logSharesheetDirectLoadTimeout() { + log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, getInstanceId()); + } + + /** + * Logs a UiEventReported event for the system sharesheet switching + * between work and main profile. + */ + default void logShareheetProfileChanged() { + log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, getInstanceId()); + } + + /** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */ + default void logSharesheetExpansionChanged(boolean isCollapsed) { + log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED : + SharesheetStandardEvent.SHARESHEET_EXPANDED, getInstanceId()); + } + + /** + * Logs a UiEventReported event for a given share activity + * @param event + * @param instanceId + */ + void log(UiEventLogger.UiEventEnum event, InstanceId instanceId); + + /** + * + * @return + */ + InstanceId getInstanceId(); + + /** + * 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); + + private final int mId; + SharesheetTargetSelectedEvent(int id) { + mId = id; + } + @Override public int getId() { + return mId; + } + + public static SharesheetTargetSelectedEvent fromTargetType(int targetType) { + switch(targetType) { + case ChooserActivity.SELECTION_TYPE_SERVICE: + return SHARESHEET_SERVICE_TARGET_SELECTED; + case ChooserActivity.SELECTION_TYPE_APP: + return SHARESHEET_APP_TARGET_SELECTED; + case ChooserActivity.SELECTION_TYPE_STANDARD: + return SHARESHEET_STANDARD_TARGET_SELECTED; + case ChooserActivity.SELECTION_TYPE_COPY: + return SHARESHEET_COPY_TARGET_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); + + 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. + */ + default int typeFromPreviewInt(int previewType) { + switch(previewType) { + case ChooserActivity.CONTENT_PREVIEW_IMAGE: + return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_IMAGE; + case ChooserActivity.CONTENT_PREVIEW_FILE: + return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE; + case ChooserActivity.CONTENT_PREVIEW_TEXT: + default: + return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_TEXT; + } + } + + /** + * Returns the enum used in sharesheet started atom to indicate what intent triggers the + * ChooserActivity. + */ + default int typeFromIntentString(String intent) { + 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; + } + } +} diff --git a/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java b/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java new file mode 100644 index 000000000000..48bdba3f5dae --- /dev/null +++ b/core/java/com/android/internal/app/ChooserActivityLoggerImpl.java @@ -0,0 +1,82 @@ +/* + * 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.internal.app; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLoggerImpl; +import com.android.internal.util.FrameworkStatsLog; + +/** + * Standard implementation of ChooserActivityLogger interface. + * @hide + */ +public class ChooserActivityLoggerImpl implements ChooserActivityLogger { + private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13); + + private UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); + // A small per-notification ID, used for statsd logging. + private InstanceId mInstanceId; + private static InstanceIdSequence sInstanceIdSequence; + + @Override + public void logShareStarted(int eventId, String packageName, String mimeType, + int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType, + String intent) { + FrameworkStatsLog.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)); + } + + @Override + public void logShareTargetSelected(int targetType, String packageName, int positionPicked) { + FrameworkStatsLog.write(FrameworkStatsLog.RANKING_SELECTED, + /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(), + /* package_name = 2 */ packageName, + /* instance_id = 3 */ getInstanceId().getId(), + /* position_picked = 4 */ positionPicked); + } + + @Override + public void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) { + mUiEventLogger.logWithInstanceId( + event, + 0, + null, + instanceId); + } + + @Override + public InstanceId getInstanceId() { + if (mInstanceId == null) { + if (sInstanceIdSequence == null) { + sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX); + } + mInstanceId = sInstanceIdSequence.newInstanceId(); + } + return mInstanceId; + } + +} diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index f088ab38658c..35253b68aac7 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -1578,6 +1578,7 @@ public class ResolverActivity extends Activity implements viewPager.setCurrentItem(1); } setupViewVisibilities(); + maybeLogProfileChange(); DevicePolicyEventLogger .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS) .setInt(viewPager.getCurrentItem()) @@ -1998,4 +1999,6 @@ public class ResolverActivity extends Activity implements } } } + + protected void maybeLogProfileChange() {} } diff --git a/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java b/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java index 91ba0dfbcc54..180ab0810f5b 100644 --- a/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java +++ b/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java @@ -85,7 +85,7 @@ public class UiEventLoggerFake implements UiEventLogger { } @Override - public void logWithInstanceId(UiEventLogger.UiEventEnum event, int uid, String packageName, + public void logWithInstanceId(UiEventEnum event, int uid, String packageName, InstanceId instance) { final int eventId = event.getId(); if (eventId > 0) { diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityLoggerFake.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityLoggerFake.java new file mode 100644 index 000000000000..374edb837057 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityLoggerFake.java @@ -0,0 +1,123 @@ +/* + * 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.internal.app; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.util.FrameworkStatsLog; + +import java.util.ArrayList; +import java.util.List; + +public class ChooserActivityLoggerFake implements ChooserActivityLogger { + static class CallRecord { + // shared fields between all logs + public int atomId; + public String packageName; + public InstanceId instanceId; + + // generic log field + public UiEventLogger.UiEventEnum event; + + // share started fields + public String mimeType; + public int appProvidedDirect; + public int appProvidedApp; + public boolean isWorkprofile; + public int previewType; + public String intent; + + // share completed fields + public int targetType; + public int positionPicked; + + CallRecord(int atomId, UiEventLogger.UiEventEnum eventId, + String packageName, InstanceId instanceId) { + this.atomId = atomId; + this.packageName = packageName; + this.instanceId = instanceId; + this.event = eventId; + } + + CallRecord(int atomId, String packageName, InstanceId instanceId, String mimeType, + int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType, + String intent) { + this.atomId = atomId; + this.packageName = packageName; + this.instanceId = instanceId; + this.mimeType = mimeType; + this.appProvidedDirect = appProvidedDirect; + this.appProvidedApp = appProvidedApp; + this.isWorkprofile = isWorkprofile; + this.previewType = previewType; + this.intent = intent; + } + + CallRecord(int atomId, String packageName, InstanceId instanceId, int targetType, + int positionPicked) { + this.atomId = atomId; + this.packageName = packageName; + this.instanceId = instanceId; + this.targetType = targetType; + this.positionPicked = positionPicked; + } + + } + private List<CallRecord> mCalls = new ArrayList<>(); + + public int numCalls() { + return mCalls.size(); + } + + List<CallRecord> getCalls() { + return mCalls; + } + + CallRecord get(int index) { + return mCalls.get(index); + } + + UiEventLogger.UiEventEnum event(int index) { + return mCalls.get(index).event; + } + + @Override + public void logShareStarted(int eventId, String packageName, String mimeType, + int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType, + String intent) { + mCalls.add(new CallRecord(FrameworkStatsLog.SHARESHEET_STARTED, packageName, + getInstanceId(), mimeType, appProvidedDirect, appProvidedApp, isWorkprofile, + previewType, intent)); + } + + @Override + public void logShareTargetSelected(int targetType, String packageName, int positionPicked) { + mCalls.add(new CallRecord(FrameworkStatsLog.RANKING_SELECTED, packageName, getInstanceId(), + SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(), positionPicked)); + } + + @Override + public void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) { + mCalls.add(new CallRecord(FrameworkStatsLog.UI_EVENT_REPORTED, + event, "", instanceId)); + } + + @Override + public InstanceId getInstanceId() { + return InstanceId.fakeInstanceId(-1); + } +} diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index 812e2a6c63fd..be129c7a2f26 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -77,6 +77,7 @@ import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; import com.android.internal.app.chooser.DisplayResolveInfo; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.FrameworkStatsLog; import org.junit.Before; import org.junit.Ignore; @@ -1430,6 +1431,251 @@ public class ChooserActivityTest { .check(matches(isDisplayed())); } + @Test + public void testAppTargetLogging() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ChooserWrapperActivity activity = mActivityRule + .launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + assertThat(activity.getAdapter().getCount(), is(2)); + onView(withId(R.id.profile_button)).check(doesNotExist()); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + waitForIdle(); + + ChooserActivityLoggerFake logger = + (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); + assertThat(logger.numCalls(), is(6)); + // first one should be SHARESHEET_TRIGGERED uievent + assertThat(logger.get(0).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(0).event.getId(), + is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); + // second one should be SHARESHEET_STARTED event + assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); + assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); + assertThat(logger.get(1).mimeType, is("text/plain")); + assertThat(logger.get(1).packageName, is("com.android.frameworks.coretests")); + assertThat(logger.get(1).appProvidedApp, is(0)); + assertThat(logger.get(1).appProvidedDirect, is(0)); + assertThat(logger.get(1).isWorkprofile, is(false)); + assertThat(logger.get(1).previewType, is(3)); + // third one should be SHARESHEET_APP_LOAD_COMPLETE uievent + assertThat(logger.get(2).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(2).event.getId(), + is(ChooserActivityLogger + .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); + // fourth and fifth are just artifacts of test set-up + // sixth one should be ranking atom with SHARESHEET_APP_TARGET_SELECTED event + assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED)); + assertThat(logger.get(5).targetType, + is(ChooserActivityLogger + .SharesheetTargetSelectedEvent.SHARESHEET_APP_TARGET_SELECTED.getId())); + } + + @Test + public void testDirectTargetLogging() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + // We need app targets for direct targets to get displayed + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + // Create direct share target + List<ChooserTarget> serviceTargets = createDirectShareTargets(1, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + + // Start activity + final ChooserWrapperActivity activity = mActivityRule + .launchActivity(Intent.createChooser(sendIntent, null)); + + // Insert the direct share target + Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); + directShareToShortcutInfos.put(serviceTargets.get(0), null); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> activity.getAdapter().addServiceResults( + activity.createTestDisplayResolveInfo(sendIntent, + ri, + "testLabel", + "testInfo", + sendIntent, + /* resolveInfoPresentationGetter */ null), + serviceTargets, + TARGET_TYPE_CHOOSER_TARGET, + directShareToShortcutInfos, + null) + ); + // Thread.sleep shouldn't be a thing in an integration test but it's + // necessary here because of the way the code is structured + // TODO: restructure the tests b/129870719 + Thread.sleep(ChooserActivity.LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS); + + assertThat("Chooser should have 3 targets (2 apps, 1 direct)", + activity.getAdapter().getCount(), is(3)); + assertThat("Chooser should have exactly one selectable direct target", + activity.getAdapter().getSelectableServiceTargetCount(), is(1)); + assertThat("The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + ChooserActivityLoggerFake logger = + (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); + assertThat(logger.numCalls(), is(6)); + // first one should be SHARESHEET_TRIGGERED uievent + assertThat(logger.get(0).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(0).event.getId(), + is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); + // second one should be SHARESHEET_STARTED event + assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); + assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); + assertThat(logger.get(1).mimeType, is("text/plain")); + assertThat(logger.get(1).packageName, is("com.android.frameworks.coretests")); + assertThat(logger.get(1).appProvidedApp, is(0)); + assertThat(logger.get(1).appProvidedDirect, is(0)); + assertThat(logger.get(1).isWorkprofile, is(false)); + assertThat(logger.get(1).previewType, is(3)); + // third one should be SHARESHEET_APP_LOAD_COMPLETE uievent + assertThat(logger.get(2).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(2).event.getId(), + is(ChooserActivityLogger + .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); + // fourth and fifth are just artifacts of test set-up + // sixth one should be ranking atom with SHARESHEET_COPY_TARGET_SELECTED event + assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED)); + assertThat(logger.get(5).targetType, + is(ChooserActivityLogger + .SharesheetTargetSelectedEvent.SHARESHEET_SERVICE_TARGET_SELECTED.getId())); + } + + @Test + public void testCopyTextToClipboardLogging() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when(ChooserWrapperActivity.sOverrides.resolverListController.getResolversForIntent( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ChooserWrapperActivity activity = mActivityRule + .launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.chooser_copy_button)).check(matches(isDisplayed())); + onView(withId(R.id.chooser_copy_button)).perform(click()); + + ChooserActivityLoggerFake logger = + (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); + assertThat(logger.numCalls(), is(6)); + // first one should be SHARESHEET_TRIGGERED uievent + assertThat(logger.get(0).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(0).event.getId(), + is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); + // second one should be SHARESHEET_STARTED event + assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); + assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); + assertThat(logger.get(1).mimeType, is("text/plain")); + assertThat(logger.get(1).packageName, is("com.android.frameworks.coretests")); + assertThat(logger.get(1).appProvidedApp, is(0)); + assertThat(logger.get(1).appProvidedDirect, is(0)); + assertThat(logger.get(1).isWorkprofile, is(false)); + assertThat(logger.get(1).previewType, is(3)); + // third one should be SHARESHEET_APP_LOAD_COMPLETE uievent + assertThat(logger.get(2).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(2).event.getId(), + is(ChooserActivityLogger + .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); + // fourth and fifth are just artifacts of test set-up + // sixth one should be ranking atom with SHARESHEET_COPY_TARGET_SELECTED event + assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED)); + assertThat(logger.get(5).targetType, + is(ChooserActivityLogger + .SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId())); + } + + @Test + public void testSwitchProfileLogging() throws InterruptedException { + // enable the work tab feature flag + ResolverActivity.ENABLE_TABBED_VIEW = true; + markWorkProfileUserAvailable(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType("TestType"); + + final ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withText(R.string.resolver_personal_tab)).perform(click()); + waitForIdle(); + + ChooserActivityLoggerFake logger = + (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); + assertThat(logger.numCalls(), is(8)); + // first one should be SHARESHEET_TRIGGERED uievent + assertThat(logger.get(0).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(0).event.getId(), + is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); + // second one should be SHARESHEET_STARTED event + assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); + assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); + assertThat(logger.get(1).mimeType, is("TestType")); + assertThat(logger.get(1).packageName, is("com.android.frameworks.coretests")); + assertThat(logger.get(1).appProvidedApp, is(0)); + assertThat(logger.get(1).appProvidedDirect, is(0)); + assertThat(logger.get(1).isWorkprofile, is(false)); + assertThat(logger.get(1).previewType, is(3)); + // third one should be SHARESHEET_APP_LOAD_COMPLETE uievent + assertThat(logger.get(2).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(2).event.getId(), + is(ChooserActivityLogger + .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); + // fourth one is artifact of test setup + // fifth one is switch to work profile + assertThat(logger.get(4).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(4).event.getId(), + is(ChooserActivityLogger + .SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED.getId())); + // sixth one should be SHARESHEET_APP_LOAD_COMPLETE uievent + assertThat(logger.get(5).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(5).event.getId(), + is(ChooserActivityLogger + .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); + // seventh one is artifact of test setup + // eigth one is switch to work profile + assertThat(logger.get(7).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); + assertThat(logger.get(7).event.getId(), + is(ChooserActivityLogger + .SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED.getId())); + } + private Intent createSendTextIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java index 363551bc92fc..5b83f952b745 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java @@ -146,6 +146,11 @@ public class ChooserWrapperActivity extends ChooserActivity { } @Override + protected ChooserActivityLogger getChooserActivityLogger() { + return sOverrides.chooserActivityLogger; + } + + @Override public Cursor queryResolver(ContentResolver resolver, Uri uri) { if (sOverrides.resolverCursor != null) { return sOverrides.resolverCursor; @@ -205,6 +210,7 @@ public class ChooserWrapperActivity extends ChooserActivity { public boolean resolverForceException; public Bitmap previewThumbnail; public MetricsLogger metricsLogger; + public ChooserActivityLogger chooserActivityLogger; public int alternateProfileSetting; public Resources resources; public UserHandle workProfileUserHandle; @@ -223,6 +229,7 @@ public class ChooserWrapperActivity extends ChooserActivity { resolverListController = mock(ResolverListController.class); workResolverListController = mock(ResolverListController.class); metricsLogger = mock(MetricsLogger.class); + chooserActivityLogger = new ChooserActivityLoggerFake(); alternateProfileSetting = 0; resources = null; workProfileUserHandle = null; diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index 136ee91dd685..c87ece29800c 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -220,7 +220,7 @@ public class DataManager { String mimeType = intentFilter != null ? intentFilter.getDataType(0) : null; @Event.EventType int eventType = mimeTypeToShareEventType(mimeType); EventHistoryImpl eventHistory; - if (ChooserActivity.LAUNCH_LOCATON_DIRECT_SHARE.equals(event.getLaunchLocation())) { + if (ChooserActivity.LAUNCH_LOCATION_DIRECT_SHARE.equals(event.getLaunchLocation())) { // Direct share event if (appTarget.getShortcutInfo() == null) { return; diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java index 51996047a74b..728e1492c0d5 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java @@ -299,7 +299,7 @@ public final class DataManagerTest { .build(); AppTargetEvent appTargetEvent = new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) - .setLaunchLocation(ChooserActivity.LAUNCH_LOCATON_DIRECT_SHARE) + .setLaunchLocation(ChooserActivity.LAUNCH_LOCATION_DIRECT_SHARE) .build(); IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SEND, "image/jpg"); mDataManager.reportShareTargetEvent(appTargetEvent, intentFilter); @@ -319,7 +319,7 @@ public final class DataManagerTest { .build(); AppTargetEvent appTargetEvent = new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) - .setLaunchLocation(ChooserActivity.LAUNCH_LOCATON_DIRECT_SHARE) + .setLaunchLocation(ChooserActivity.LAUNCH_LOCATION_DIRECT_SHARE) .build(); IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SEND, "image/jpg"); @@ -667,7 +667,7 @@ public final class DataManagerTest { .build(); AppTargetEvent appTargetEvent = new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH) - .setLaunchLocation(ChooserActivity.LAUNCH_LOCATON_DIRECT_SHARE) + .setLaunchLocation(ChooserActivity.LAUNCH_LOCATION_DIRECT_SHARE) .build(); IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SEND, "image/jpg"); |