From c1fdc6cb1540098cb9feed1147141c9a28df7043 Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Tue, 21 Feb 2023 16:43:04 +0000 Subject: Add logging for modify share and custom action clicks Just a regular UiEvent log for modify share. Include position information for custom actions (*just* the position within the custom action set, ignoring other system actions that may be within the display container). Bug: 265504112 Test: atest ChooserActivityLoggerTest Test: atest ChooserActivityFactoryTest Change-Id: If64db5c1afccf6571d23395624d6ffbbef677188 --- .../intentresolver/ChooserActionFactory.java | 57 +++++--- .../intentresolver/ChooserActivityLogger.java | 27 +++- .../ChooserIntegratedDeviceComponents.java | 2 +- .../intentresolver/ChooserActionFactoryTest.kt | 154 +++++++++++++++++++++ .../intentresolver/ChooserActivityLoggerTest.java | 11 ++ 5 files changed, 233 insertions(+), 18 deletions(-) create mode 100644 java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt (limited to 'java') diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index 1fe55890..566b2546 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -49,7 +49,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.function.Consumer; -import java.util.stream.Collectors; /** * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application @@ -96,9 +95,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio private final TargetInfo mNearbySharingTarget; private final Runnable mOnNearbyButtonClicked; private final ImmutableList mCustomActions; - private final PendingIntent mReselectionIntent; + private final Runnable mOnModifyShareClicked; private final Consumer mExcludeSharedTextAction; private final Consumer mFinishCallback; + private final ChooserActivityLogger mLogger; /** * @param context @@ -160,8 +160,13 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio logger), chooserRequest.getChooserActions(), (featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION) - ? chooserRequest.getModifyShareAction() : null), + ? createModifyShareRunnable( + chooserRequest.getModifyShareAction(), + finishCallback, + logger) + : null), onUpdateSharedTextIsExcluded, + logger, finishCallback); } @@ -176,8 +181,9 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio TargetInfo nearbySharingTarget, Runnable onNearbyButtonClicked, List customActions, - @Nullable PendingIntent reselectionIntent, + @Nullable Runnable onModifyShareClicked, Consumer onUpdateSharedTextIsExcluded, + ChooserActivityLogger logger, Consumer finishCallback) { mContext = context; mCopyButtonLabel = copyButtonLabel; @@ -188,8 +194,9 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio mNearbySharingTarget = nearbySharingTarget; mOnNearbyButtonClicked = onNearbyButtonClicked; mCustomActions = ImmutableList.copyOf(customActions); - mReselectionIntent = reselectionIntent; + mOnModifyShareClicked = onModifyShareClicked; mExcludeSharedTextAction = onUpdateSharedTextIsExcluded; + mLogger = logger; mFinishCallback = finishCallback; } @@ -236,10 +243,15 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio /** Create custom actions */ @Override public List createCustomActions() { - return mCustomActions.stream() - .map(target -> createCustomAction(mContext, target, mFinishCallback)) - .filter(action -> action != null) - .collect(Collectors.toList()); + List actions = new ArrayList<>(); + for (int i = 0; i < mCustomActions.size(); i++) { + ActionRow.Action actionRow = createCustomAction( + mContext, mCustomActions.get(i), mFinishCallback, i, mLogger); + if (actionRow != null) { + actions.add(actionRow); + } + } + return actions; } /** @@ -248,18 +260,25 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio @Override @Nullable public Runnable getModifyShareAction() { - return (mReselectionIntent == null) ? null : createReselectionRunnable(mReselectionIntent); + return mOnModifyShareClicked; } - private Runnable createReselectionRunnable(PendingIntent pendingIntent) { + private static Runnable createModifyShareRunnable( + PendingIntent pendingIntent, + Consumer finishCallback, + ChooserActivityLogger logger) { + if (pendingIntent == null) { + return null; + } + return () -> { try { pendingIntent.send(); } catch (PendingIntent.CanceledException e) { Log.d(TAG, "Payload reselection action has been cancelled"); } - // TODO: add reporting - mFinishCallback.accept(Activity.RESULT_OK); + logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE); + finishCallback.accept(Activity.RESULT_OK); }; } @@ -402,7 +421,9 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio Intent originalIntent, ChooserIntegratedDeviceComponents integratedComponents) { final ComponentName cn = integratedComponents.getNearbySharingComponent(); - if (cn == null) return null; + if (cn == null) { + return null; + } final Intent resolveIntent = new Intent(originalIntent); resolveIntent.setComponent(cn); @@ -455,7 +476,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio @Nullable private static ActionRow.Action createCustomAction( - Context context, ChooserAction action, Consumer finishCallback) { + Context context, + ChooserAction action, + Consumer finishCallback, + int position, + ChooserActivityLogger logger) { Drawable icon = action.getIcon().loadDrawable(context); if (icon == null && TextUtils.isEmpty(action.getLabel())) { return null; @@ -469,7 +494,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio } catch (PendingIntent.CanceledException e) { Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled"); } - // TODO: add reporting + logger.logCustomActionSelected(position); finishCallback.accept(Activity.RESULT_OK); } ); diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/ChooserActivityLogger.java index 1725a7bf..f298955b 100644 --- a/java/src/com/android/intentresolver/ChooserActivityLogger.java +++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java @@ -48,6 +48,8 @@ public class ChooserActivityLogger { 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 @@ -133,6 +135,21 @@ public class ChooserActivityLogger { /* reselection_action_provided = 11 */ false); } + /** + * 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 @@ -332,7 +349,11 @@ public class ChooserActivityLogger { @UiEvent(doc = "User selected the nearby target.") SHARESHEET_NEARBY_TARGET_SELECTED(626), @UiEvent(doc = "User selected the edit target.") - SHARESHEET_EDIT_TARGET_SELECTED(669); + 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) { @@ -356,6 +377,10 @@ public class ChooserActivityLogger { 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; } diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java index 9b124c20..14255ca0 100644 --- a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java +++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java @@ -32,7 +32,7 @@ import com.android.internal.annotations.VisibleForTesting; * Because this describes the app's external execution environment, test methods may prefer to * provide explicit values to override the default lookup logic. */ -public final class ChooserIntegratedDeviceComponents { +public class ChooserIntegratedDeviceComponents { @Nullable private final ComponentName mEditSharingComponent; diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt new file mode 100644 index 00000000..af134fcd --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt @@ -0,0 +1,154 @@ +/* + * 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 + +import android.app.Activity +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Resources +import android.graphics.drawable.Icon +import android.service.chooser.ChooserAction +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.intentresolver.flags.FeatureFlagRepository +import com.android.intentresolver.flags.Flags +import com.google.common.collect.ImmutableList +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import java.util.concurrent.Callable +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.function.Consumer + +@RunWith(AndroidJUnit4::class) +class ChooserActionFactoryTest { + private val context = InstrumentationRegistry.getInstrumentation().getContext() + + private val logger = mock() + private val flags = mock() + private val actionLabel = "Action label" + private val testAction = "com.android.intentresolver.testaction" + private val countdown = CountDownLatch(1) + private val testReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + // Just doing at most a single countdown per test. + countdown.countDown() + } + } + private object resultConsumer : Consumer { + var latestReturn = Integer.MIN_VALUE + + override fun accept(resultCode: Int) { + latestReturn = resultCode + } + + } + + @Before + fun setup() { + whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(true) + context.registerReceiver(testReceiver, IntentFilter(testAction)) + } + + @After + fun teardown() { + context.unregisterReceiver(testReceiver) + } + + @Test + fun testCreateCustomActions() { + val factory = createFactory() + + val customActions = factory.createCustomActions() + + assertThat(customActions.size).isEqualTo(1) + assertThat(customActions[0].label).isEqualTo(actionLabel) + + // click it + customActions[0].onClicked.run() + + Mockito.verify(logger).logCustomActionSelected(eq(0)) + assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) + // Verify the pendingintent has been called + countdown.await(500, TimeUnit.MILLISECONDS) + } + + @Test + fun testNoModifyShareAction() { + val factory = createFactory(includeModifyShare = false) + + assertThat(factory.modifyShareAction).isNull() + } + + @Test + fun testNoModifyShareAction_flagDisabled() { + whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(false) + val factory = createFactory(includeModifyShare = true) + + assertThat(factory.modifyShareAction).isNull() + } + + @Test + fun testModifyShareAction() { + val factory = createFactory(includeModifyShare = true) + + factory.modifyShareAction!!.run() + + Mockito.verify(logger).logActionSelected( + eq(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE)) + assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) + // Verify the pendingintent has been called + countdown.await(500, TimeUnit.MILLISECONDS) + } + + private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory { + val testPendingIntent = PendingIntent.getActivity(context, 0, Intent(testAction),0) + val targetIntent = Intent() + val action = ChooserAction.Builder( + Icon.createWithResource("", Resources.ID_NULL), + actionLabel, + testPendingIntent + ).build() + val chooserRequest = mock() + whenever(chooserRequest.targetIntent).thenReturn(targetIntent) + whenever(chooserRequest.chooserActions).thenReturn(ImmutableList.of(action)) + + if (includeModifyShare) { + whenever(chooserRequest.modifyShareAction).thenReturn(testPendingIntent) + } + + return ChooserActionFactory( + context, + chooserRequest, + flags, + mock(), + logger, + Consumer{}, + Callable{null}, + mock(), + resultConsumer) + } +} \ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java index c6a9b63f..d8868fc1 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java @@ -204,6 +204,17 @@ public final class ChooserActivityLoggerTest { assertThat(event.getSubtype()).isEqualTo(1); } + @Test + public void testLogCustomActionSelected() { + final int position = 4; + mChooserLogger.logCustomActionSelected(position); + + verify(mFrameworkLog).write( + eq(FrameworkStatsLog.RANKING_SELECTED), + eq(SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId()), + any(), anyInt(), eq(position), eq(false)); + } + @Test public void testLogDirectShareTargetReceived() { final int category = MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER; -- cgit v1.2.3-59-g8ed1b