/* * 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.ActivityOptions import android.app.Application import android.content.Context import android.content.Intent import android.content.IntentSender import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.Message import android.os.ResultReceiver import androidx.lifecycle.Observer import androidx.test.annotation.UiThreadTest import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.intentresolver.ChooserRefinementManager.RefinementCompletion import com.android.intentresolver.ChooserRefinementManager.RefinementType import com.android.intentresolver.chooser.ImmutableTargetInfo import com.google.common.truth.Truth.assertThat import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) @UiThreadTest class ChooserRefinementManagerTest { private val refinementManager = ChooserRefinementManager() private val intentSender = mock() private val application = mock() private val exampleSourceIntents = listOf(Intent(Intent.ACTION_VIEW), Intent(Intent.ACTION_EDIT)) private val exampleTargetInfo = ImmutableTargetInfo.newBuilder().setAllSourceIntents(exampleSourceIntents).build() private val completionObserver = object : Observer { val failureCountDown = CountDownLatch(1) val successCountDown = CountDownLatch(1) var latestRefinedIntent: Intent? = null override fun onChanged(completion: RefinementCompletion) { if (completion.consume()) { val refinedIntent = completion.refinedIntent if (refinedIntent == null) { failureCountDown.countDown() } else { latestRefinedIntent = refinedIntent successCountDown.countDown() } } } } /** Synchronously executes post() calls. */ private class FakeHandler(looper: Looper) : Handler(looper) { override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean { dispatchMessage(msg) return true } } @Before fun setup() { refinementManager.refinementCompletion.observeForever(completionObserver) } @Test fun testTypicalRefinementFlow() { assertThat( refinementManager.maybeHandleSelection( exampleTargetInfo, intentSender, application, FakeHandler(checkNotNull(Looper.myLooper())) ) ) .isTrue() val intentCaptor = argumentCaptor() val optionsCaptor = argumentCaptor() verify(intentSender) .sendIntent( anyOrNull(), eq(0), intentCaptor.capture(), eq(null), optionsCaptor.capture(), eq(null), eq(null), ) val intent = intentCaptor.firstValue assertThat(intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)) .isEqualTo(exampleSourceIntents[0]) val options = optionsCaptor.firstValue assertThat( ActivityOptions.fromBundle(options).getPendingIntentBackgroundActivityStartMode() ) .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) val alternates = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS, Intent::class.java) assertThat(alternates?.size).isEqualTo(1) assertThat(alternates?.get(0)).isEqualTo(exampleSourceIntents[1]) // Complete the refinement val receiver = intent.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java) val bundle = Bundle().apply { putParcelable(Intent.EXTRA_INTENT, exampleSourceIntents[0]) } receiver?.send(Activity.RESULT_OK, bundle) assertThat(completionObserver.successCountDown.await(1000, TimeUnit.MILLISECONDS)).isTrue() assertThat(completionObserver.latestRefinedIntent?.action).isEqualTo(Intent.ACTION_VIEW) } @Test fun testRefinementCancelled() { assertThat( refinementManager.maybeHandleSelection( exampleTargetInfo, intentSender, application, FakeHandler(checkNotNull(Looper.myLooper())) ) ) .isTrue() val intentCaptor = argumentCaptor() val optionsCaptor = argumentCaptor() verify(intentSender) .sendIntent( anyOrNull(), eq(0), intentCaptor.capture(), eq(null), optionsCaptor.capture(), eq(null), eq(null), ) val intent = intentCaptor.firstValue val options = optionsCaptor.firstValue assertThat( ActivityOptions.fromBundle(options).getPendingIntentBackgroundActivityStartMode() ) .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) // Complete the refinement val receiver = intent?.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java) val bundle = Bundle().apply { putParcelable(Intent.EXTRA_INTENT, exampleSourceIntents[0]) } receiver?.send(Activity.RESULT_CANCELED, bundle) assertThat(completionObserver.failureCountDown.await(1000, TimeUnit.MILLISECONDS)).isTrue() } @Test fun testMaybeHandleSelection_noSourceIntents() { assertThat( refinementManager.maybeHandleSelection( ImmutableTargetInfo.newBuilder().build(), intentSender, application, FakeHandler(checkNotNull(Looper.myLooper())) ) ) .isFalse() } @Test fun testMaybeHandleSelection_suspended() { val targetInfo = ImmutableTargetInfo.newBuilder() .setAllSourceIntents(exampleSourceIntents) .setIsSuspended(true) .build() assertThat( refinementManager.maybeHandleSelection( targetInfo, intentSender, application, FakeHandler(checkNotNull(Looper.myLooper())) ) ) .isFalse() } @Test fun testMaybeHandleSelection_noIntentSender() { assertThat( refinementManager.maybeHandleSelection( exampleTargetInfo, /* IntentSender */ null, application, FakeHandler(checkNotNull(Looper.myLooper())) ) ) .isFalse() } @Test fun testConfigurationChangeDuringRefinement() { assertThat( refinementManager.maybeHandleSelection( exampleTargetInfo, intentSender, application, FakeHandler(checkNotNull(Looper.myLooper())) ) ) .isTrue() refinementManager.onActivityStop(/* config changing = */ true) refinementManager.onActivityResume() assertThat(completionObserver.failureCountDown.count).isEqualTo(1) } @Test fun testResumeDuringRefinement() { assertThat( refinementManager.maybeHandleSelection( exampleTargetInfo, intentSender, application, FakeHandler(checkNotNull(Looper.myLooper())!!) ) ) .isTrue() refinementManager.onActivityStop(/* config changing = */ false) // Resume during refinement but not during a config change, so finish the activity. refinementManager.onActivityResume() // Call should be synchronous, don't need to await for this one. assertThat(completionObserver.failureCountDown.count).isEqualTo(0) } @Test fun testRefinementCompletion() { val refinementCompletion = RefinementCompletion(RefinementType.TARGET_INFO, exampleTargetInfo, null) assertThat(refinementCompletion.originalTargetInfo).isEqualTo(exampleTargetInfo) assertThat(refinementCompletion.consume()).isTrue() assertThat(refinementCompletion.originalTargetInfo).isEqualTo(exampleTargetInfo) // can only consume once. assertThat(refinementCompletion.consume()).isFalse() } }