diff options
| author | 2024-02-05 12:18:13 -0500 | |
|---|---|---|
| committer | 2024-02-12 15:39:18 -0500 | |
| commit | a5162406bf48d155d3927c33e51aeee4368a24ff (patch) | |
| tree | f17de311ae2b6bdc2758d2ddca0ce3c0208e291f | |
| parent | 452240b2fa39855b059c3fe084362fd5b9d07ee1 (diff) | |
Additional Details for Sharesheet App Callbacks
Bug: 263474465
Test: atest ShareResultSenderImplTest
Change-Id: Icb61fa49dd2989cc50d7024da19d863e6c2fc189
8 files changed, 477 insertions, 67 deletions
diff --git a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java index db840387..70a2b58e 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java @@ -40,6 +40,8 @@ import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.TargetInfo;  import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;  import com.android.intentresolver.logging.EventLog; +import com.android.intentresolver.v2.ui.ShareResultSender; +import com.android.intentresolver.v2.ui.model.ShareAction;  import com.android.intentresolver.widget.ActionRow;  import com.android.internal.annotations.VisibleForTesting; @@ -97,12 +99,12 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio      private final Context mContext; -    @Nullable -    private final Runnable mCopyButtonRunnable; -    private final Runnable mEditButtonRunnable; +    @Nullable private Runnable mCopyButtonRunnable; +    private Runnable mEditButtonRunnable;      private final ImmutableList<ChooserAction> mCustomActions; -    private final @Nullable ChooserAction mModifyShareAction; +    @Nullable private final ChooserAction mModifyShareAction;      private final Consumer<Boolean> mExcludeSharedTextAction; +    @Nullable private final ShareResultSender mShareResultSender;      private final Consumer</* @Nullable */ Integer> mFinishCallback;      private final EventLog mLog; @@ -122,12 +124,13 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              Intent targetIntent,              String referrerPackageName,              List<ChooserAction> chooserActions, -            ChooserAction modifyShareAction, +            @Nullable ChooserAction modifyShareAction,              Optional<ComponentName> imageEditor,              EventLog log,              Consumer<Boolean> onUpdateSharedTextIsExcluded,              Callable</* @Nullable */ View> firstVisibleImageQuery,              ActionActivityStarter activityStarter, +            @Nullable ShareResultSender shareResultSender,              Consumer</* @Nullable */ Integer> finishCallback) {          this(                  context, @@ -149,7 +152,9 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                  modifyShareAction,                  onUpdateSharedTextIsExcluded,                  log, +                shareResultSender,                  finishCallback); +      }      @VisibleForTesting @@ -161,6 +166,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio              @Nullable ChooserAction modifyShareAction,              Consumer<Boolean> onUpdateSharedTextIsExcluded,              EventLog log, +            @Nullable ShareResultSender shareResultSender,              Consumer</* @Nullable */ Integer> finishCallback) {          mContext = context;          mCopyButtonRunnable = copyButtonRunnable; @@ -169,7 +175,22 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio          mModifyShareAction = modifyShareAction;          mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;          mLog = log; +        mShareResultSender = shareResultSender;          mFinishCallback = finishCallback; + +        if (mShareResultSender != null) { +            mEditButtonRunnable = () -> { +                mShareResultSender.onActionSelected(ShareAction.SYSTEM_EDIT); +                mEditButtonRunnable.run(); +            }; +            if (mCopyButtonRunnable != null) { +                mCopyButtonRunnable = () -> { +                    mShareResultSender.onActionSelected(ShareAction.SYSTEM_COPY); +                    //noinspection DataFlowIssue +                    mCopyButtonRunnable.run(); +                }; +            } +        }      }      @Override @@ -353,12 +374,12 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio      }      @Nullable -    private static ActionRow.Action createCustomAction( +    private ActionRow.Action createCustomAction(              Context context, -            ChooserAction action, +            @Nullable ChooserAction action,              Consumer<Integer> finishCallback,              Runnable loggingRunnable) { -        if (action == null || action.getAction() == null) { +        if (action == null) {              return null;          }          Drawable icon = action.getIcon().loadDrawable(context); @@ -388,6 +409,9 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio                      if (loggingRunnable != null) {                          loggingRunnable.run();                      } +                    if (mShareResultSender != null) { +                        mShareResultSender.onActionSelected(ShareAction.APPLICATION_DEFINED); +                    }                      finishCallback.accept(Activity.RESULT_OK);                  }          ); diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index 35812071..30845818 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -37,7 +37,6 @@ import static java.util.Collections.emptyList;  import static java.util.Objects.requireNonNull;  import static java.util.Objects.requireNonNullElse; -import android.app.Activity;  import android.app.ActivityManager;  import android.app.ActivityOptions;  import android.app.ActivityThread; @@ -148,6 +147,8 @@ import com.android.intentresolver.v2.platform.AppPredictionAvailable;  import com.android.intentresolver.v2.platform.ImageEditor;  import com.android.intentresolver.v2.platform.NearbyShare;  import com.android.intentresolver.v2.ui.ActionTitle; +import com.android.intentresolver.v2.ui.ShareResultSender; +import com.android.intentresolver.v2.ui.ShareResultSenderFactory;  import com.android.intentresolver.v2.ui.model.ActivityLaunch;  import com.android.intentresolver.v2.ui.model.ChooserRequest;  import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel; @@ -275,6 +276,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements      @Inject public DevicePolicyResources mDevicePolicyResources;      @Inject public PackageManager mPackageManager;      @Inject public IntentForwarding mIntentForwarding; +    @Inject public ShareResultSenderFactory mShareResultSenderFactory; +    @Nullable +    private ShareResultSender mShareResultSender;      private ChooserRefinementManager mRefinementManager; @@ -354,6 +358,12 @@ public class ChooserActivity extends Hilt_ChooserActivity implements              finish();              return;          } +        IntentSender chosenComponentSender = +                mViewModel.getChooserRequest().getChosenComponentSender(); +        if (chosenComponentSender != null) { +            mShareResultSender = mShareResultSenderFactory +                    .create(mActivityLaunch.getFromUid(), chosenComponentSender); +        }          mLogic = createActivityLogic();          init();      } @@ -819,13 +829,13 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          }          try {              if (cti.startAsCaller(this, options, user.getIdentifier())) { -                onActivityStarted(cti); +                maybeSendShareResult(cti);                  maybeLogCrossProfileTargetLaunch(cti, user);              }          } catch (RuntimeException e) {              Slog.wtf(TAG,                      "Unable to launch as uid " + mActivityLaunch.getFromUid() -                            + " package " + getLaunchedFromPackage() + ", while running in " +                            + " package " + mActivityLaunch.getFromPackage() + ", while running in "                              + ActivityThread.currentProcessName(), e);          }      } @@ -1586,19 +1596,11 @@ public class ChooserActivity extends Hilt_ChooserActivity implements          return result;      } -    public void onActivityStarted(TargetInfo cti) { -        ChooserRequest chooserRequest = mViewModel.getChooserRequest(); -        if (chooserRequest.getChosenComponentSender() != null) { +    private void maybeSendShareResult(TargetInfo cti) { +        if (mShareResultSender != null) {              final ComponentName target = cti.getResolvedComponentName();              if (target != null) { -                final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target); -                try { -                    chooserRequest.getChosenComponentSender().sendIntent( -                            this, Activity.RESULT_OK, fillIn, null, null); -                } catch (IntentSender.SendIntentException e) { -                    Slog.e(TAG, "Unable to launch supplied IntentSender to report " -                            + "the chosen component: " + e); -                } +                mShareResultSender.onComponentSelected(target, cti.isChooserTargetInfo());              }          }      } @@ -2121,6 +2123,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements                          mFinishWhenStopped = true;                      }                  }, +                mShareResultSender,                  (status) -> {                      if (status != null) {                          setResult(status); diff --git a/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt b/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt new file mode 100644 index 00000000..2b01b5e7 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 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.v2.ui + +import android.app.Activity +import android.app.compat.CompatChanges +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentSender +import android.service.chooser.ChooserResult +import android.service.chooser.ChooserResult.CHOOSER_RESULT_COPY +import android.service.chooser.ChooserResult.CHOOSER_RESULT_EDIT +import android.service.chooser.ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT +import android.service.chooser.ChooserResult.CHOOSER_RESULT_UNKNOWN +import android.service.chooser.ChooserResult.ResultType +import android.util.Log +import com.android.intentresolver.inject.Background +import com.android.intentresolver.inject.ChooserServiceFlags +import com.android.intentresolver.inject.Main +import com.android.intentresolver.v2.ui.model.ShareAction +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ActivityContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private const val TAG = "ShareResultSender" + +/** Reports the result of a share to another process across binder, via an [IntentSender] */ +interface ShareResultSender { +    /** Reports user selection of an activity to launch from the provided choices. */ +    fun onComponentSelected(component: ComponentName, directShare: Boolean) + +    /** Reports user invocation of a built-in system action. See [ShareAction]. */ +    fun onActionSelected(action: ShareAction) +} + +@AssistedFactory +interface ShareResultSenderFactory { +    fun create(callerUid: Int, chosenComponentSender: IntentSender): ShareResultSenderImpl +} + +/** Dispatches Intents via IntentSender */ +fun interface IntentSenderDispatcher { +    fun dispatchIntent(intentSender: IntentSender, intent: Intent) +} + +class ShareResultSenderImpl( +    private val flags: ChooserServiceFlags, +    @Main private val scope: CoroutineScope, +    @Background val backgroundDispatcher: CoroutineDispatcher, +    private val callerUid: Int, +    private val resultSender: IntentSender, +    private val intentDispatcher: IntentSenderDispatcher +) : ShareResultSender { +    @AssistedInject +    constructor( +        @ActivityContext context: Context, +        flags: ChooserServiceFlags, +        @Main scope: CoroutineScope, +        @Background backgroundDispatcher: CoroutineDispatcher, +        @Assisted callerUid: Int, +        @Assisted chosenComponentSender: IntentSender, +    ) : this( +        flags, +        scope, +        backgroundDispatcher, +        callerUid, +        chosenComponentSender, +        IntentSenderDispatcher { sender, intent -> sender.dispatchIntent(context, intent) } +    ) + +    override fun onComponentSelected(component: ComponentName, directShare: Boolean) { +        Log.i(TAG, "onComponentSelected: $component directShare=$directShare") +        scope.launch { +            val intent = createChosenComponentIntent(component, directShare) +            intentDispatcher.dispatchIntent(resultSender, intent) +        } +    } + +    override fun onActionSelected(action: ShareAction) { +        Log.i(TAG, "onActionSelected: $action") +        scope.launch { +            if (flags.enableChooserResult() && chooserResultSupported(callerUid)) { +                @ResultType val chosenAction = shareActionToChooserResult(action) +                val intent: Intent = createSelectedActionIntent(chosenAction) +                intentDispatcher.dispatchIntent(resultSender, intent) +            } else { +                Log.i(TAG, "Not sending SelectedAction") +            } +        } +    } + +    private suspend fun createChosenComponentIntent( +        component: ComponentName, +        direct: Boolean, +    ): Intent { +        // Add extra with component name for backwards compatibility. +        val intent: Intent = Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, component) + +        // Add ChooserResult value for Android V+ +        if (flags.enableChooserResult() && chooserResultSupported(callerUid)) { +            intent.putExtra( +                Intent.EXTRA_CHOOSER_RESULT, +                ChooserResult(CHOOSER_RESULT_SELECTED_COMPONENT, component, direct) +            ) +        } else { +            Log.i(TAG, "Not including ${Intent.EXTRA_CHOOSER_RESULT}") +        } +        return intent +    } + +    @ResultType +    private fun shareActionToChooserResult(action: ShareAction) = +        when (action) { +            ShareAction.SYSTEM_COPY -> CHOOSER_RESULT_COPY +            ShareAction.SYSTEM_EDIT -> CHOOSER_RESULT_EDIT +            ShareAction.APPLICATION_DEFINED -> CHOOSER_RESULT_UNKNOWN +        } + +    private fun createSelectedActionIntent(@ResultType result: Int): Intent { +        return Intent().putExtra(Intent.EXTRA_CHOOSER_RESULT, ChooserResult(result, null, false)) +    } + +    private suspend fun chooserResultSupported(uid: Int): Boolean { +        return withContext(backgroundDispatcher) { +            // background -> Binder call to system_server +            CompatChanges.isChangeEnabled(ChooserResult.SEND_CHOOSER_RESULT, uid) +        } +    } +} + +private fun IntentSender.dispatchIntent(context: Context, intent: Intent) { +    try { +        sendIntent( +            /* context = */ context, +            /* code = */ Activity.RESULT_OK, +            /* intent = */ intent, +            /* onFinished = */ null, +            /* handler = */ null +        ) +    } catch (e: IntentSender.SendIntentException) { +        Log.e(TAG, "Failed to send intent to IntentSender", e) +    } +} diff --git a/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt b/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt new file mode 100644 index 00000000..e13ef101 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 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.v2.ui.model + +enum class ShareAction { +    SYSTEM_COPY, +    SYSTEM_EDIT, +    APPLICATION_DEFINED +} diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt index cb1ef1ae..0269168e 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt @@ -23,6 +23,7 @@ import android.content.Intent.EXTRA_ALTERNATE_INTENTS  import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS  import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION  import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER +import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER  import android.content.Intent.EXTRA_CHOOSER_TARGETS  import android.content.Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER  import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS @@ -111,7 +112,8 @@ fun readChooserRequest(                  ?: emptyList()          val chosenComponentSender = -            optional(value<IntentSender>(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER)) +            optional(value<IntentSender>(EXTRA_CHOOSER_RESULT_INTENT_SENDER)) +                ?: optional(value<IntentSender>(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER))          val refinementIntentSender =              optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index a07af1a4..f8b80c72 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -52,6 +52,7 @@ android_test {          "junit",          "kotlinx_coroutines_test",          "mockito-target-minus-junit4", +        "platform-compat-test-rules", // PlatformCompatChangeRule          "testables", // TestableContext/TestableResources          "truth",          "truth-java8-extension", diff --git a/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt index b3486bb1..717d26bd 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt @@ -134,17 +134,18 @@ class ChooserActionFactoryTest {              }          val testSubject =              ChooserActionFactory( -                context, -                chooserRequest.targetIntent, -                chooserRequest.referrerPackageName, -                chooserRequest.chooserActions, -                chooserRequest.modifyShareAction, -                Optional.empty(), -                logger, -                {}, -                { null }, -                mock(), -                {}, +                /* context = */ context, +                /* targetIntent = */ chooserRequest.targetIntent, +                /* referrerPackageName = */ chooserRequest.referrerPackageName, +                /* chooserActions = */ chooserRequest.chooserActions, +                /* modifyShareAction = */ chooserRequest.modifyShareAction, +                /* imageEditor = */ Optional.empty(), +                /* log = */ logger, +                /* onUpdateSharedTextIsExcluded = */ {}, +                /* firstVisibleImageQuery = */ { null }, +                /* activityStarter = */ mock(), +                /* shareResultSender = */ null, +                /* finishCallback = */ {},              )          assertThat(testSubject.copyButtonRunnable).isNull()      } @@ -160,17 +161,18 @@ class ChooserActionFactoryTest {              }          val testSubject =              ChooserActionFactory( -                context, -                chooserRequest.targetIntent, -                chooserRequest.referrerPackageName, -                chooserRequest.chooserActions, -                chooserRequest.modifyShareAction, -                Optional.empty(), -                logger, -                {}, -                { null }, -                mock(), -                {}, +                /* context = */ context, +                /* targetIntent = */ chooserRequest.targetIntent, +                /* referrerPackageName = */ chooserRequest.referrerPackageName, +                /* chooserActions = */ chooserRequest.chooserActions, +                /* modifyShareAction = */ chooserRequest.modifyShareAction, +                /* imageEditor = */ Optional.empty(), +                /* log = */ logger, +                /* onUpdateSharedTextIsExcluded = */ {}, +                /* firstVisibleImageQuery = */ { null }, +                /* activityStarter = */ mock(), +                /* shareResultSender = */ null, +                /* finishCallback = */ {},              )          assertThat(testSubject.copyButtonRunnable).isNull()      } @@ -186,17 +188,18 @@ class ChooserActionFactoryTest {              }          val testSubject =              ChooserActionFactory( -                context, -                chooserRequest.targetIntent, -                chooserRequest.referrerPackageName, -                chooserRequest.chooserActions, -                chooserRequest.modifyShareAction, -                Optional.empty(), -                logger, -                {}, -                { null }, -                mock(), -                {}, +                /* context = */ context, +                /* targetIntent = */ chooserRequest.targetIntent, +                /* referrerPackageName = */ chooserRequest.referrerPackageName, +                /* chooserActions = */ chooserRequest.chooserActions, +                /* modifyShareAction = */ chooserRequest.modifyShareAction, +                /* imageEditor = */ Optional.empty(), +                /* log = */ logger, +                /* onUpdateSharedTextIsExcluded = */ {}, +                /* firstVisibleImageQuery = */ { null }, +                /* activityStarter = */ mock(), +                /* shareResultSender = */ null, +                /* finishCallback = */ {},              )          assertThat(testSubject.copyButtonRunnable).isNotNull()      } @@ -228,17 +231,18 @@ class ChooserActionFactoryTest {          }          return ChooserActionFactory( -            context, -            chooserRequest.targetIntent, -            chooserRequest.referrerPackageName, -            chooserRequest.chooserActions, -            chooserRequest.modifyShareAction, -            Optional.empty(), -            logger, -            {}, -            { null }, -            mock(), -            resultConsumer +            /* context = */ context, +            /* targetIntent = */ chooserRequest.targetIntent, +            /* referrerPackageName = */ chooserRequest.referrerPackageName, +            /* chooserActions = */ chooserRequest.chooserActions, +            /* modifyShareAction = */ chooserRequest.modifyShareAction, +            /* imageEditor = */ Optional.empty(), +            /* log = */ logger, +            /* onUpdateSharedTextIsExcluded = */ {}, +            /* firstVisibleImageQuery = */ { null }, +            /* activityStarter = */ mock(), +            /* shareResultSender = */ null, +            /* finishCallback = */ resultConsumer          )      }  } diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/ShareResultSenderImplTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/ShareResultSenderImplTest.kt new file mode 100644 index 00000000..371f9c26 --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/ui/ShareResultSenderImplTest.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2024 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.v2.ui + +import android.app.PendingIntent +import android.compat.testing.PlatformCompatChangeRule +import android.content.ComponentName +import android.content.Intent +import android.os.Process +import android.service.chooser.ChooserResult +import android.service.chooser.Flags +import androidx.test.InstrumentationRegistry +import com.android.intentresolver.inject.FakeChooserServiceFlags +import com.android.intentresolver.v2.ui.model.ShareAction +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule + +@OptIn(ExperimentalCoroutinesApi::class) +class ShareResultSenderImplTest { + +    private val context = InstrumentationRegistry.getInstrumentation().context + +    @get:Rule val compatChangeRule: TestRule = PlatformCompatChangeRule() + +    val flags = FakeChooserServiceFlags() + +    @OptIn(ExperimentalCoroutinesApi::class) +    @EnableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) +    @Test +    fun onComponentSelected_chooserResultEnabled() = runTest { +        val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) +        val deferred = CompletableDeferred<Intent>() +        val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } + +        flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true) + +        val resultSender = +            ShareResultSenderImpl( +                flags = flags, +                scope = this, +                backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), +                callerUid = Process.myUid(), +                resultSender = pi.intentSender, +                intentDispatcher = intentDispatcher +            ) + +        resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true) +        runCurrent() + +        val intentReceived = deferred.await() +        val chooserResult = +            intentReceived.getParcelableExtra( +                Intent.EXTRA_CHOOSER_RESULT, +                ChooserResult::class.java +            ) +        assertThat(chooserResult).isNotNull() +        assertThat(chooserResult?.type).isEqualTo(ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT) +        assertThat(chooserResult?.selectedComponent).isEqualTo(ComponentName("example.com", "Foo")) +        assertThat(chooserResult?.isShortcut).isTrue() +    } + +    @DisableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) +    @Test +    fun onComponentSelected_chooserResultDisabled() = runTest { +        val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) +        val deferred = CompletableDeferred<Intent>() +        val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } + +        flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true) + +        val resultSender = +            ShareResultSenderImpl( +                flags = flags, +                scope = this, +                backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), +                callerUid = Process.myUid(), +                resultSender = pi.intentSender, +                intentDispatcher = intentDispatcher +            ) + +        resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true) +        runCurrent() + +        val intentReceived = deferred.await() +        val componentName = +            intentReceived.getParcelableExtra( +                Intent.EXTRA_CHOSEN_COMPONENT, +                ComponentName::class.java +            ) + +        assertWithMessage("EXTRA_CHOSEN_COMPONENT from received intent") +            .that(componentName) +            .isEqualTo(ComponentName("example.com", "Foo")) + +        assertWithMessage("received intent has EXTRA_CHOOSER_RESULT") +            .that(intentReceived.hasExtra(Intent.EXTRA_CHOOSER_RESULT)) +            .isFalse() +    } + +    @EnableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) +    @Test +    fun onActionSelected_chooserResultEnabled() = runTest { +        val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) +        val deferred = CompletableDeferred<Intent>() +        val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } + +        flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true) + +        val resultSender = +            ShareResultSenderImpl( +                flags = flags, +                scope = this, +                backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), +                callerUid = Process.myUid(), +                resultSender = pi.intentSender, +                intentDispatcher = intentDispatcher +            ) + +        resultSender.onActionSelected(ShareAction.SYSTEM_COPY) +        runCurrent() + +        val intentReceived = deferred.await() +        val chosenComponent = +            intentReceived.getParcelableExtra( +                Intent.EXTRA_CHOSEN_COMPONENT, +                ChooserResult::class.java +            ) +        assertThat(chosenComponent).isNull() + +        val chooserResult = +            intentReceived.getParcelableExtra( +                Intent.EXTRA_CHOOSER_RESULT, +                ChooserResult::class.java +            ) +        assertThat(chooserResult).isNotNull() +        assertThat(chooserResult?.type).isEqualTo(ChooserResult.CHOOSER_RESULT_COPY) +        assertThat(chooserResult?.selectedComponent).isNull() +        assertThat(chooserResult?.isShortcut).isFalse() +    } + +    @DisableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) +    @Test +    fun onActionSelected_chooserResultDisabled() = runTest { +        val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) +        val deferred = CompletableDeferred<Intent>() +        val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } + +        flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true) + +        val resultSender = +            ShareResultSenderImpl( +                flags = flags, +                scope = this, +                backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), +                callerUid = Process.myUid(), +                resultSender = pi.intentSender, +                intentDispatcher = intentDispatcher +            ) + +        resultSender.onActionSelected(ShareAction.SYSTEM_COPY) +        runCurrent() + +        // No result should have been sent, this should never complete +        assertWithMessage("deferred result isComplete").that(deferred.isCompleted).isFalse() +    } +}  |