1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * https://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver.interactive.domain.interactor 18 19 import android.content.ComponentName 20 import android.content.Intent 21 import android.content.Intent.ACTION_QUICK_VIEW 22 import android.content.Intent.ACTION_RUN 23 import android.content.Intent.ACTION_SEND 24 import android.content.Intent.ACTION_VIEW 25 import android.content.Intent.EXTRA_ALTERNATE_INTENTS 26 import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER 27 import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER 28 import android.content.Intent.EXTRA_CHOOSER_TARGETS 29 import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS 30 import android.content.Intent.EXTRA_INITIAL_INTENTS 31 import android.content.Intent.EXTRA_REPLACEMENT_EXTRAS 32 import android.content.IntentSender 33 import android.os.Binder 34 import android.os.IBinder 35 import android.os.IBinder.DeathRecipient 36 import android.os.IInterface 37 import android.os.Parcel 38 import android.os.ResultReceiver 39 import android.os.ShellCallback 40 import android.service.chooser.ChooserTarget 41 import androidx.core.os.bundleOf 42 import androidx.lifecycle.SavedStateHandle 43 import com.android.intentresolver.IChooserController 44 import com.android.intentresolver.IChooserInteractiveSessionCallback 45 import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository 46 import com.android.intentresolver.data.model.ChooserRequest 47 import com.android.intentresolver.data.repository.ActivityModelRepository 48 import com.android.intentresolver.data.repository.ChooserRequestRepository 49 import com.android.intentresolver.interactive.data.repository.InteractiveSessionCallbackRepository 50 import com.android.intentresolver.shared.model.ActivityModel 51 import com.google.common.truth.Correspondence 52 import com.google.common.truth.Truth.assertThat 53 import java.io.FileDescriptor 54 import kotlinx.coroutines.launch 55 import kotlinx.coroutines.test.runTest 56 import org.junit.Test 57 58 class InteractiveSessionInteractorTest { 59 private val activityModelRepo = <lambda>null60 ActivityModelRepository().apply { 61 initialize { 62 ActivityModel( 63 intent = Intent(), 64 launchedFromUid = 12345, 65 launchedFromPackage = "org.client.package", 66 referrer = null, 67 isTaskRoot = false, 68 ) 69 } 70 } 71 private val interactiveSessionCallback = FakeChooserInteractiveSessionCallback() 72 private val pendingSelectionCallbackRepo = PendingSelectionCallbackRepository() 73 private val savedStateHandle = SavedStateHandle() 74 private val interactiveCallbackRepo = InteractiveSessionCallbackRepository(savedStateHandle) 75 76 @Test <lambda>null77 fun testChooserLaunchedInNewTask_sessionClosed() = runTest { 78 val activityModelRepo = 79 ActivityModelRepository().apply { 80 initialize { 81 ActivityModel( 82 intent = Intent(), 83 launchedFromUid = 12345, 84 launchedFromPackage = "org.client.package", 85 referrer = null, 86 isTaskRoot = true, 87 ) 88 } 89 } 90 val chooserRequestRepository = 91 ChooserRequestRepository( 92 initialRequest = 93 ChooserRequest( 94 targetIntent = Intent(ACTION_SEND), 95 interactiveSessionCallback = interactiveSessionCallback, 96 launchedFromPackage = activityModelRepo.value.launchedFromPackage, 97 ), 98 initialActions = emptyList(), 99 ) 100 val testSubject = 101 InteractiveSessionInteractor( 102 activityModelRepo = activityModelRepo, 103 chooserRequestRepository = chooserRequestRepository, 104 pendingSelectionCallbackRepo, 105 interactiveCallbackRepo, 106 ) 107 108 testSubject.activate() 109 110 assertThat(interactiveSessionCallback.registeredIntentUpdaters).containsExactly(null) 111 } 112 113 @Test <lambda>null114 fun testDeadBinder_sessionEnd() = runTest { 115 interactiveSessionCallback.isAlive = false 116 val chooserRequestRepository = 117 ChooserRequestRepository( 118 initialRequest = 119 ChooserRequest( 120 targetIntent = Intent(ACTION_SEND), 121 interactiveSessionCallback = interactiveSessionCallback, 122 launchedFromPackage = activityModelRepo.value.launchedFromPackage, 123 ), 124 initialActions = emptyList(), 125 ) 126 val testSubject = 127 InteractiveSessionInteractor( 128 activityModelRepo = activityModelRepo, 129 chooserRequestRepository = chooserRequestRepository, 130 pendingSelectionCallbackRepo, 131 interactiveCallbackRepo, 132 ) 133 134 backgroundScope.launch { testSubject.activate() } 135 this.testScheduler.runCurrent() 136 137 assertThat(testSubject.isSessionActive.value).isFalse() 138 } 139 140 @Test <lambda>null141 fun testBinderDies_sessionEnd() = runTest { 142 val chooserRequestRepository = 143 ChooserRequestRepository( 144 initialRequest = 145 ChooserRequest( 146 targetIntent = Intent(ACTION_SEND), 147 interactiveSessionCallback = interactiveSessionCallback, 148 launchedFromPackage = activityModelRepo.value.launchedFromPackage, 149 ), 150 initialActions = emptyList(), 151 ) 152 val testSubject = 153 InteractiveSessionInteractor( 154 activityModelRepo = activityModelRepo, 155 chooserRequestRepository = chooserRequestRepository, 156 pendingSelectionCallbackRepo, 157 interactiveCallbackRepo, 158 ) 159 160 backgroundScope.launch { testSubject.activate() } 161 this.testScheduler.runCurrent() 162 163 assertThat(testSubject.isSessionActive.value).isTrue() 164 assertThat(interactiveSessionCallback.linkedDeathRecipients).hasSize(1) 165 166 interactiveSessionCallback.linkedDeathRecipients[0].binderDied() 167 168 assertThat(testSubject.isSessionActive.value).isFalse() 169 } 170 171 @Test <lambda>null172 fun testScopeCancelled_unsubscribeFromBinder() = runTest { 173 val chooserRequestRepository = 174 ChooserRequestRepository( 175 initialRequest = 176 ChooserRequest( 177 targetIntent = Intent(ACTION_SEND), 178 interactiveSessionCallback = interactiveSessionCallback, 179 launchedFromPackage = activityModelRepo.value.launchedFromPackage, 180 ), 181 initialActions = emptyList(), 182 ) 183 val testSubject = 184 InteractiveSessionInteractor( 185 activityModelRepo = activityModelRepo, 186 chooserRequestRepository = chooserRequestRepository, 187 pendingSelectionCallbackRepo, 188 interactiveCallbackRepo, 189 ) 190 191 val job = backgroundScope.launch { testSubject.activate() } 192 testScheduler.runCurrent() 193 194 assertThat(interactiveSessionCallback.linkedDeathRecipients).hasSize(1) 195 assertThat(interactiveSessionCallback.unlinkedDeathRecipients).hasSize(0) 196 197 job.cancel() 198 testScheduler.runCurrent() 199 200 assertThat(interactiveSessionCallback.unlinkedDeathRecipients).hasSize(1) 201 } 202 203 @Test <lambda>null204 fun endSession_intentUpdaterCallbackReset() = runTest { 205 val chooserRequestRepository = 206 ChooserRequestRepository( 207 initialRequest = 208 ChooserRequest( 209 targetIntent = Intent(ACTION_SEND), 210 interactiveSessionCallback = interactiveSessionCallback, 211 launchedFromPackage = activityModelRepo.value.launchedFromPackage, 212 ), 213 initialActions = emptyList(), 214 ) 215 val testSubject = 216 InteractiveSessionInteractor( 217 activityModelRepo = activityModelRepo, 218 chooserRequestRepository = chooserRequestRepository, 219 pendingSelectionCallbackRepo, 220 interactiveCallbackRepo, 221 ) 222 223 backgroundScope.launch { testSubject.activate() } 224 testScheduler.runCurrent() 225 226 assertThat(interactiveSessionCallback.registeredIntentUpdaters).hasSize(1) 227 228 testSubject.endSession() 229 230 assertThat(interactiveSessionCallback.registeredIntentUpdaters).hasSize(2) 231 assertThat(interactiveSessionCallback.registeredIntentUpdaters[1]).isNull() 232 } 233 234 @Test <lambda>null235 fun nullChooserIntentReceived_sessionEnds() = runTest { 236 val chooserRequestRepository = 237 ChooserRequestRepository( 238 initialRequest = 239 ChooserRequest( 240 targetIntent = Intent(ACTION_SEND), 241 interactiveSessionCallback = interactiveSessionCallback, 242 launchedFromPackage = activityModelRepo.value.launchedFromPackage, 243 ), 244 initialActions = emptyList(), 245 ) 246 val testSubject = 247 InteractiveSessionInteractor( 248 activityModelRepo = activityModelRepo, 249 chooserRequestRepository = chooserRequestRepository, 250 pendingSelectionCallbackRepo, 251 interactiveCallbackRepo, 252 ) 253 254 backgroundScope.launch { testSubject.activate() } 255 testScheduler.runCurrent() 256 257 assertThat(interactiveSessionCallback.registeredIntentUpdaters).hasSize(1) 258 interactiveSessionCallback.registeredIntentUpdaters[0]!!.updateIntent(null) 259 testScheduler.runCurrent() 260 261 assertThat(testSubject.isSessionActive.value).isFalse() 262 } 263 264 @Test <lambda>null265 fun invalidChooserIntentReceived_intentIgnored() = runTest { 266 val chooserRequestRepository = 267 ChooserRequestRepository( 268 initialRequest = 269 ChooserRequest( 270 targetIntent = Intent(ACTION_SEND), 271 interactiveSessionCallback = interactiveSessionCallback, 272 launchedFromPackage = activityModelRepo.value.launchedFromPackage, 273 ), 274 initialActions = emptyList(), 275 ) 276 val testSubject = 277 InteractiveSessionInteractor( 278 activityModelRepo = activityModelRepo, 279 chooserRequestRepository = chooserRequestRepository, 280 pendingSelectionCallbackRepo, 281 interactiveCallbackRepo, 282 ) 283 284 backgroundScope.launch { testSubject.activate() } 285 testScheduler.runCurrent() 286 287 assertThat(interactiveSessionCallback.registeredIntentUpdaters).hasSize(1) 288 interactiveSessionCallback.registeredIntentUpdaters[0]!!.updateIntent(Intent()) 289 testScheduler.runCurrent() 290 291 assertThat(testSubject.isSessionActive.value).isTrue() 292 assertThat(chooserRequestRepository.chooserRequest.value) 293 .isEqualTo(chooserRequestRepository.initialRequest) 294 } 295 296 @Test <lambda>null297 fun validChooserIntentReceived_chooserRequestUpdated() = runTest { 298 val chooserRequestRepository = 299 ChooserRequestRepository( 300 initialRequest = 301 ChooserRequest( 302 targetIntent = Intent(ACTION_SEND), 303 interactiveSessionCallback = interactiveSessionCallback, 304 launchedFromPackage = activityModelRepo.value.launchedFromPackage, 305 ), 306 initialActions = emptyList(), 307 ) 308 val testSubject = 309 InteractiveSessionInteractor( 310 activityModelRepo = activityModelRepo, 311 chooserRequestRepository = chooserRequestRepository, 312 pendingSelectionCallbackRepo, 313 interactiveCallbackRepo, 314 ) 315 316 backgroundScope.launch { testSubject.activate() } 317 testScheduler.runCurrent() 318 319 assertThat(interactiveSessionCallback.registeredIntentUpdaters).hasSize(1) 320 val newTargetIntent = Intent(ACTION_VIEW).apply { type = "image/png" } 321 val newFilteredComponents = arrayOf(ComponentName.unflattenFromString("com.app/.MainA")) 322 val newCallerTargets = 323 arrayOf( 324 ChooserTarget( 325 "A", 326 null, 327 0.5f, 328 ComponentName.unflattenFromString("org.pkg/.Activity"), 329 null, 330 ) 331 ) 332 val newAdditionalIntents = arrayOf(Intent(ACTION_RUN)) 333 val newReplacementExtras = bundleOf("ONE" to 1, "TWO" to 2) 334 val newInitialIntents = arrayOf(Intent(ACTION_QUICK_VIEW)) 335 val newResultSender = IntentSender(Binder()) 336 val newRefinementSender = IntentSender(Binder()) 337 interactiveSessionCallback.registeredIntentUpdaters[0]!!.updateIntent( 338 Intent.createChooser(newTargetIntent, "").apply { 339 putExtra(EXTRA_EXCLUDE_COMPONENTS, newFilteredComponents) 340 putExtra(EXTRA_CHOOSER_TARGETS, newCallerTargets) 341 putExtra(EXTRA_ALTERNATE_INTENTS, newAdditionalIntents) 342 putExtra(EXTRA_REPLACEMENT_EXTRAS, newReplacementExtras) 343 putExtra(EXTRA_INITIAL_INTENTS, newInitialIntents) 344 putExtra(EXTRA_CHOOSER_RESULT_INTENT_SENDER, newResultSender) 345 putExtra(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER, newRefinementSender) 346 } 347 ) 348 testScheduler.runCurrent() 349 350 assertThat(testSubject.isSessionActive.value).isTrue() 351 val updatedRequest = chooserRequestRepository.chooserRequest.value 352 assertThat(updatedRequest.targetAction).isEqualTo(newTargetIntent.action) 353 assertThat(updatedRequest.targetType).isEqualTo(newTargetIntent.type) 354 assertThat(updatedRequest.filteredComponentNames).containsExactly(newFilteredComponents[0]) 355 assertThat(updatedRequest.callerChooserTargets).containsExactly(newCallerTargets[0]) 356 assertThat(updatedRequest.additionalTargets) 357 .comparingElementsUsing<Intent, String>( 358 Correspondence.transforming({ it.action }, "action") 359 ) 360 .containsExactly(newAdditionalIntents[0].action) 361 assertThat(updatedRequest.replacementExtras!!.keySet()) 362 .containsExactlyElementsIn(newReplacementExtras.keySet()) 363 assertThat(updatedRequest.initialIntents) 364 .comparingElementsUsing<Intent, String>( 365 Correspondence.transforming({ it.action }, "action") 366 ) 367 .containsExactly(newInitialIntents[0].action) 368 assertThat(updatedRequest.chosenComponentSender).isEqualTo(newResultSender) 369 assertThat(updatedRequest.refinementIntentSender).isEqualTo(newRefinementSender) 370 } 371 } 372 373 private class FakeChooserInteractiveSessionCallback : 374 IChooserInteractiveSessionCallback, IBinder, IInterface { 375 var isAlive = true 376 val registeredIntentUpdaters = ArrayList<IChooserController?>() 377 val linkedDeathRecipients = ArrayList<DeathRecipient>() 378 val unlinkedDeathRecipients = ArrayList<DeathRecipient>() 379 registerChooserControllernull380 override fun registerChooserController(intentUpdater: IChooserController?) { 381 registeredIntentUpdaters.add(intentUpdater) 382 } 383 onDrawerVerticalOffsetChangednull384 override fun onDrawerVerticalOffsetChanged(offset: Int) {} 385 asBindernull386 override fun asBinder() = this 387 388 override fun getInterfaceDescriptor() = "" 389 390 override fun pingBinder() = true 391 392 override fun isBinderAlive() = isAlive 393 394 override fun queryLocalInterface(descriptor: String): IInterface = 395 this@FakeChooserInteractiveSessionCallback 396 397 override fun dump(fd: FileDescriptor, args: Array<out String>?) = Unit 398 399 override fun dumpAsync(fd: FileDescriptor, args: Array<out String>?) = Unit 400 401 override fun shellCommand( 402 `in`: FileDescriptor?, 403 out: FileDescriptor?, 404 err: FileDescriptor?, 405 args: Array<out String>, 406 shellCallback: ShellCallback?, 407 resultReceiver: ResultReceiver, 408 ) = Unit 409 410 override fun transact(code: Int, data: Parcel, reply: Parcel?, flags: Int) = true 411 412 override fun linkToDeath(recipient: DeathRecipient, flags: Int) { 413 linkedDeathRecipients.add(recipient) 414 } 415 unlinkToDeathnull416 override fun unlinkToDeath(recipient: DeathRecipient, flags: Int): Boolean { 417 unlinkedDeathRecipients.add(recipient) 418 return true 419 } 420 } 421