From 7f2fc4418c68dbdcf8f6766bde2180749150d549 Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Mon, 1 Apr 2024 17:31:07 -0400 Subject: Add @Broadcast Handler, broadcastFlow() -> BroadcastSubscriber Provides a background handler (thread) for Broadcast delivery. Currently, broadcasts are flowed on a @Background CoroutineDispatcher but before that, they are delivered to the app via the Main thread Handler. This means broadcasts being processed during startup will get deferred until Main thread idle (generally, right after onResume) This change improves on broadcastFlow, providing a broadcast flow factory (BroadcastSubscriber), which registers Receivers with a singleton @Broadcast Handler. This makes broadcast delivery much more reliable and predictable. Test: manually. Observe broadcasts received while the process is frozen are now handled immediately after the unfreeze, and no longer blocked until after onResume. Bug: 330561320 Flag: ACONFIG intentresolver/com.android.intentresolver.enable_private_profile Change-Id: I8ecf151d3bf7627d9e3917fb9fecd78c1e201521 --- .../intentresolver/inject/ConcurrencyModule.kt | 29 +++++++++ .../android/intentresolver/inject/Qualifiers.kt | 2 + .../intentresolver/v2/data/BroadcastFlow.kt | 46 -------------- .../intentresolver/v2/data/BroadcastSubscriber.kt | 73 ++++++++++++++++++++++ .../v2/data/repository/UserRepository.kt | 60 +++++++++--------- 5 files changed, 134 insertions(+), 76 deletions(-) delete mode 100644 java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt create mode 100644 java/src/com/android/intentresolver/v2/data/BroadcastSubscriber.kt (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt index e0f8e88b..5fbdf090 100644 --- a/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt +++ b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt @@ -16,6 +16,10 @@ package com.android.intentresolver.inject +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.Process import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -26,6 +30,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +// thread +private const val BROADCAST_SLOW_DISPATCH_THRESHOLD = 1000L +private const val BROADCAST_SLOW_DELIVERY_THRESHOLD = 1000L + @Module @InstallIn(SingletonComponent::class) object ConcurrencyModule { @@ -40,4 +48,25 @@ object ConcurrencyModule { CoroutineScope(SupervisorJob() + mainDispatcher) @Provides @Background fun backgroundDispatcher(): CoroutineDispatcher = Dispatchers.IO + + @Provides + @Singleton + @Broadcast + fun provideBroadcastLooper(): Looper { + val thread = HandlerThread("BroadcastReceiver", Process.THREAD_PRIORITY_BACKGROUND) + thread.start() + thread.looper.setSlowLogThresholdMs( + BROADCAST_SLOW_DISPATCH_THRESHOLD, + BROADCAST_SLOW_DELIVERY_THRESHOLD + ) + return thread.looper + } + + /** Provide a BroadcastReceiver Executor (for sending and receiving broadcasts). */ + @Provides + @Singleton + @Broadcast + fun provideBroadcastHandler(@Broadcast looper: Looper): Handler { + return Handler(looper) + } } diff --git a/java/src/com/android/intentresolver/inject/Qualifiers.kt b/java/src/com/android/intentresolver/inject/Qualifiers.kt index f267328b..77315cac 100644 --- a/java/src/com/android/intentresolver/inject/Qualifiers.kt +++ b/java/src/com/android/intentresolver/inject/Qualifiers.kt @@ -35,6 +35,8 @@ annotation class ApplicationOwned @Retention(AnnotationRetention.RUNTIME) annotation class ApplicationUser +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Broadcast + @Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ProfileParent @Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Background diff --git a/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt b/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt deleted file mode 100644 index 1a58afcb..00000000 --- a/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.android.intentresolver.v2.data - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.UserHandle -import android.util.Log -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.onFailure -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -private const val TAG = "BroadcastFlow" - -/** - * Returns a [callbackFlow] that, when collected, registers a broadcast receiver and emits a new - * value whenever broadcast matching _filter_ is received. The result value will be computed using - * [transform] and emitted if non-null. - */ -internal fun broadcastFlow( - context: Context, - filter: IntentFilter, - user: UserHandle, - transform: (Intent) -> T? -): Flow = callbackFlow { - val receiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - transform(intent)?.also { result -> - trySend(result).onFailure { Log.e(TAG, "Failed to send $result", it) } - } - ?: Log.w(TAG, "Ignored broadcast $intent") - } - } - - context.registerReceiverAsUser( - receiver, - user, - IntentFilter(filter), - null, - null, - Context.RECEIVER_NOT_EXPORTED - ) - awaitClose { context.unregisterReceiver(receiver) } -} diff --git a/java/src/com/android/intentresolver/v2/data/BroadcastSubscriber.kt b/java/src/com/android/intentresolver/v2/data/BroadcastSubscriber.kt new file mode 100644 index 00000000..f3013246 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/BroadcastSubscriber.kt @@ -0,0 +1,73 @@ +/* + * 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.data + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Handler +import android.os.UserHandle +import android.util.Log +import com.android.intentresolver.inject.Broadcast +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.onFailure +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +private const val TAG = "BroadcastSubscriber" + +class BroadcastSubscriber +@Inject +constructor( + @ApplicationContext private val context: Context, + @Broadcast private val handler: Handler +) { + /** + * Returns a [callbackFlow] that, when collected, registers a broadcast receiver and emits a new + * value whenever broadcast matching _filter_ is received. The result value will be computed + * using [transform] and emitted if non-null. + */ + fun createFlow( + filter: IntentFilter, + user: UserHandle, + transform: (Intent) -> T?, + ): Flow = callbackFlow { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + transform(intent)?.also { result -> + trySend(result).onFailure { Log.e(TAG, "Failed to send $result", it) } + } + ?: Log.w(TAG, "Ignored broadcast $intent") + } + } + + @Suppress("MissingPermission") + context.registerReceiverAsUser( + receiver, + user, + IntentFilter(filter), + null, + handler, + Context.RECEIVER_NOT_EXPORTED + ) + awaitClose { context.unregisterReceiver(receiver) } + } +} diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt index 40672249..d196d3e6 100644 --- a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt +++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt @@ -16,7 +16,6 @@ package com.android.intentresolver.v2.data.repository -import android.content.Context import android.content.Intent import android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE import android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE @@ -36,9 +35,8 @@ import androidx.annotation.VisibleForTesting import com.android.intentresolver.inject.Background import com.android.intentresolver.inject.Main import com.android.intentresolver.inject.ProfileParent -import com.android.intentresolver.v2.data.broadcastFlow +import com.android.intentresolver.v2.data.BroadcastSubscriber import com.android.intentresolver.v2.shared.model.User -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -88,6 +86,23 @@ internal data class UserWithState(val user: User, val available: Boolean) internal typealias UserStates = List +internal val userBroadcastActions = + setOf( + ACTION_PROFILE_ADDED, + ACTION_PROFILE_REMOVED, + + // Quiet mode enabled/disabled for managed + // From: UserController.broadcastProfileAvailabilityChanges + // In response to setQuietModeEnabled + ACTION_MANAGED_PROFILE_AVAILABLE, // quiet mode, sent for manage profiles only + ACTION_MANAGED_PROFILE_UNAVAILABLE, // quiet mode, sent for manage profiles only + + // Quiet mode toggled for profile type, requires flag 'android.os.allow_private_profile + // true' + ACTION_PROFILE_AVAILABLE, // quiet mode, + ACTION_PROFILE_UNAVAILABLE, // quiet mode, sent for any profile type + ) + /** Tracks and publishes state for the parent user and associated profiles. */ class UserRepositoryImpl @VisibleForTesting @@ -97,21 +112,26 @@ constructor( /** A flow of events which represent user-state changes from [UserManager]. */ private val userEvents: Flow, scope: CoroutineScope, - private val backgroundDispatcher: CoroutineDispatcher + private val backgroundDispatcher: CoroutineDispatcher, ) : UserRepository { @Inject constructor( - @ApplicationContext context: Context, @ProfileParent profileParent: UserHandle, userManager: UserManager, @Main scope: CoroutineScope, - @Background background: CoroutineDispatcher + @Background background: CoroutineDispatcher, + broadcastSubscriber: BroadcastSubscriber, ) : this( profileParent, userManager, - userEvents = userBroadcastFlow(context, profileParent), + userEvents = + broadcastSubscriber.createFlow( + createFilter(userBroadcastActions), + profileParent, + Intent::toUserEvent + ), scope, - background + background, ) private fun debugLog(msg: () -> String) { @@ -264,7 +284,7 @@ data class UnknownEvent( ) : UserEvent /** Used with [broadcastFlow] to transform a UserManager broadcast action into a [UserEvent]. */ -private fun Intent.toUserEvent(): UserEvent { +internal fun Intent.toUserEvent(): UserEvent { val action = action val user = extras?.getParcelable(EXTRA_USER, UserHandle::class.java) val quietMode = extras?.getBoolean(EXTRA_QUIET_MODE, false) @@ -280,30 +300,10 @@ private fun Intent.toUserEvent(): UserEvent { } } -private fun createFilter(actions: Iterable): IntentFilter { +internal fun createFilter(actions: Iterable): IntentFilter { return IntentFilter().apply { actions.forEach(::addAction) } } internal fun UserInfo?.isAvailable(): Boolean { return this?.isQuietModeEnabled != true } - -internal fun userBroadcastFlow(context: Context, profileParent: UserHandle): Flow { - val userActions = - setOf( - ACTION_PROFILE_ADDED, - ACTION_PROFILE_REMOVED, - - // Quiet mode enabled/disabled for managed - // From: UserController.broadcastProfileAvailabilityChanges - // In response to setQuietModeEnabled - ACTION_MANAGED_PROFILE_AVAILABLE, // quiet mode, sent for manage profiles only - ACTION_MANAGED_PROFILE_UNAVAILABLE, // quiet mode, sent for manage profiles only - - // Quiet mode toggled for profile type, requires flag 'android.os.allow_private_profile - // true' - ACTION_PROFILE_AVAILABLE, // quiet mode, - ACTION_PROFILE_UNAVAILABLE, // quiet mode, sent for any profile type - ) - return broadcastFlow(context, createFilter(userActions), profileParent, Intent::toUserEvent) -} -- cgit v1.2.3-59-g8ed1b From 8e7e81138fc72d5df530cb77032f9aae1f18cb2c Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Tue, 2 Apr 2024 10:54:45 -0400 Subject: Simplify startup and Profile flow handling Instead of blocking to collect a set of intermediate values in ChooserHelper, simply allow the @JavaInterop classes to do this work instead. This greatly simplifies the startup code run from onCreate but does not affect latency, as the flows are still collected within the same call stack (onCreate) when needed. This change also removes an unneccesary intermediate StateFlow from ProfileAvailability. The flow was only used internally to collect the current value. Since ProfileAvailability is a @JavaInterop class (bridging blocking/suspending code), used from the Main thread only, we can simplify to `runBlocking { flow.first() }` where an immediate value is needed. Bug: 330561320 Test: atest IntentResolver-tests-activity Flag: ACONFIG intentresolver/com.android.intentresolver.enable_private_profile Change-Id: I88c5a96a57db32bb5eee90f5d94e1d56b224aa63 --- .../android/intentresolver/v2/ChooserActivity.java | 19 +- .../com/android/intentresolver/v2/ChooserHelper.kt | 40 +---- .../intentresolver/v2/ProfileAvailability.kt | 23 ++- .../com/android/intentresolver/v2/ProfileHelper.kt | 19 +- .../intentresolver/v2/ProfileAvailabilityTest.kt | 8 +- .../android/intentresolver/v2/ProfileHelperTest.kt | 197 ++++++++++----------- 6 files changed, 140 insertions(+), 166 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index d624c9e4..5f3129f8 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -124,6 +124,7 @@ import com.android.intentresolver.emptystate.EmptyState; import com.android.intentresolver.emptystate.EmptyStateProvider; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.icons.TargetDataLoader; +import com.android.intentresolver.inject.Background; import com.android.intentresolver.logging.EventLog; import com.android.intentresolver.measurements.Tracer; import com.android.intentresolver.model.AbstractResolverComparator; @@ -185,6 +186,8 @@ import java.util.function.Supplier; import javax.inject.Inject; +import kotlinx.coroutines.CoroutineDispatcher; + /** * The Chooser Activity handles intent resolution specifically for sharing intents - * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}. @@ -265,6 +268,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2; @Inject public UserInteractor mUserInteractor; + @Inject @Background public CoroutineDispatcher mBackgroundDispatcher; @Inject public ChooserHelper mChooserHelper; @Inject public FeatureFlags mFeatureFlags; @Inject public android.service.chooser.FeatureFlags mChooserServiceFeatureFlags; @@ -352,7 +356,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements setTheme(R.style.Theme_DeviceDefault_Chooser); // Initializer is invoked when this function returns, via Lifecycle. - mChooserHelper.setInitializer(this::initializeWith); + mChooserHelper.setInitializer(this::initialize); if (mChooserServiceFeatureFlags.chooserPayloadToggling()) { mChooserHelper.setOnChooserRequestChanged(this::onChooserRequestChanged); } @@ -467,8 +471,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } /** DO NOT CALL. Only for use from ChooserHelper as a callback. */ - private void initializeWith(InitialState initialState) { - Log.d(TAG, "initializeWith: " + initialState); + private void initialize() { mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class); mRequest = mViewModel.getRequest().getValue(); @@ -476,14 +479,14 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mProfiles = new ProfileHelper( mUserInteractor, - mFeatureFlags, - initialState.getProfiles(), - initialState.getLaunchedAs()); + getCoroutineScope(getLifecycle()), + mBackgroundDispatcher, + mFeatureFlags); mProfileAvailability = new ProfileAvailability( - getCoroutineScope(getLifecycle()), mUserInteractor, - initialState.getAvailability()); + getCoroutineScope(getLifecycle()), + mBackgroundDispatcher); mProfileAvailability.setOnProfileStatusChange(this::onWorkProfileStatusUpdated); diff --git a/java/src/com/android/intentresolver/v2/ChooserHelper.kt b/java/src/com/android/intentresolver/v2/ChooserHelper.kt index 503e46d8..9da0d605 100644 --- a/java/src/com/android/intentresolver/v2/ChooserHelper.kt +++ b/java/src/com/android/intentresolver/v2/ChooserHelper.kt @@ -31,7 +31,6 @@ import com.android.intentresolver.inject.Background import com.android.intentresolver.v2.annotation.JavaInterop import com.android.intentresolver.v2.data.model.ChooserRequest import com.android.intentresolver.v2.domain.interactor.UserInteractor -import com.android.intentresolver.v2.shared.model.Profile import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel import com.android.intentresolver.v2.validation.Invalid import com.android.intentresolver.v2.validation.Valid @@ -43,34 +42,9 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking private const val TAG: String = "ChooserHelper" -/** - * Provides initial values to ChooserActivity and completes initialization from onCreate. - * - * This information is collected and provided on behalf of ChooserActivity to eliminate the need for - * suspending functions within remaining synchronous startup code. - */ -@JavaInterop -fun interface ChooserInitializer { - /** @param initialState the initial state to provide to initialization */ - fun initializeWith(initialState: InitialState) -} - -/** - * A parameter object for Initialize which contains all the values which are required "early", on - * the main thread and outside of any coroutines. This supports code which expects to be called by - * the system on the main thread only. (This includes everything originally called from onCreate). - */ -@JavaInterop -data class InitialState( - val profiles: List, - val availability: Map, - val launchedAs: Profile -) - /** * __Purpose__ * @@ -113,7 +87,7 @@ constructor( private val activity: ComponentActivity = hostActivity as ComponentActivity private val viewModel by activity.viewModels() - private lateinit var activityInitializer: ChooserInitializer + private lateinit var activityInitializer: Runnable var onChooserRequestChanged: Consumer = Consumer {} @@ -126,7 +100,7 @@ constructor( * * This _must_ be called from [ChooserActivity.onCreate]. */ - fun setInitializer(initializer: ChooserInitializer) { + fun setInitializer(initializer: Runnable) { check(activity.lifecycle.currentState == Lifecycle.State.INITIALIZED) { "setInitializer must be called before onCreate returns" } @@ -189,14 +163,6 @@ constructor( private fun initializeActivity(request: Valid) { request.warnings.forEach { it.log(TAG) } - - val initialState = - runBlocking(background) { - val initialProfiles = userInteractor.profiles.first() - val initialAvailability = userInteractor.availability.first() - val launchedAsProfile = userInteractor.launchedAsProfile.first() - InitialState(initialProfiles, initialAvailability, launchedAsProfile) - } - activityInitializer.initializeWith(initialState) + activityInitializer.run() } } diff --git a/java/src/com/android/intentresolver/v2/ProfileAvailability.kt b/java/src/com/android/intentresolver/v2/ProfileAvailability.kt index ddb57991..27d8c6bb 100644 --- a/java/src/com/android/intentresolver/v2/ProfileAvailability.kt +++ b/java/src/com/android/intentresolver/v2/ProfileAvailability.kt @@ -16,27 +16,26 @@ package com.android.intentresolver.v2 +import androidx.annotation.MainThread import com.android.intentresolver.v2.annotation.JavaInterop import com.android.intentresolver.v2.domain.interactor.UserInteractor import com.android.intentresolver.v2.shared.model.Profile +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking /** Provides availability status for profiles */ @JavaInterop class ProfileAvailability( - private val scope: CoroutineScope, private val userInteractor: UserInteractor, - initialState: Map + private val scope: CoroutineScope, + private val background: CoroutineDispatcher, ) { - private val availability = - userInteractor.availability.stateIn(scope, SharingStarted.Eagerly, initialState) - /** Used by WorkProfilePausedEmptyStateProvider */ var waitingToEnableProfile = false private set @@ -45,8 +44,14 @@ class ProfileAvailability( var onProfileStatusChange: Runnable? = null private var waitJob: Job? = null + /** Query current profile availability. An unavailable profile is one which is not active. */ - fun isAvailable(profile: Profile) = availability.value[profile] ?: false + @MainThread + fun isAvailable(profile: Profile): Boolean { + return runBlocking(background) { + userInteractor.availability.map { it[profile] == true }.first() + } + } /** Used by WorkProfilePausedEmptyStateProvider */ fun requestQuietModeState(profile: Profile, quietMode: Boolean) { @@ -65,7 +70,7 @@ class ProfileAvailability( val job = scope.launch { // Wait for the profile to become available - availability.filter { it[profile] == true }.first() + userInteractor.availability.filter { it[profile] == true }.first() } job.invokeOnCompletion { waitingToEnableProfile = false diff --git a/java/src/com/android/intentresolver/v2/ProfileHelper.kt b/java/src/com/android/intentresolver/v2/ProfileHelper.kt index 8a8e6b54..87948150 100644 --- a/java/src/com/android/intentresolver/v2/ProfileHelper.kt +++ b/java/src/com/android/intentresolver/v2/ProfileHelper.kt @@ -17,29 +17,40 @@ package com.android.intentresolver.v2 import android.os.UserHandle +import androidx.annotation.MainThread import com.android.intentresolver.inject.IntentResolverFlags import com.android.intentresolver.v2.annotation.JavaInterop import com.android.intentresolver.v2.domain.interactor.UserInteractor import com.android.intentresolver.v2.shared.model.Profile import com.android.intentresolver.v2.shared.model.User import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking @JavaInterop +@MainThread class ProfileHelper @Inject constructor( interactor: UserInteractor, + private val scope: CoroutineScope, + private val background: CoroutineDispatcher, private val flags: IntentResolverFlags, - val profiles: List, - val launchedAsProfile: Profile, ) { private val launchedByHandle: UserHandle = interactor.launchedAs + val launchedAsProfile by lazy { + runBlocking(background) { interactor.launchedAsProfile.first() } + } + val profiles by lazy { runBlocking(background) { interactor.profiles.first() } } + // Map UserHandle back to a user within launchedByProfile - private val launchedByUser = + private val launchedByUser: User = when (launchedByHandle) { launchedAsProfile.primary.handle -> launchedAsProfile.primary - launchedAsProfile.clone?.handle -> launchedAsProfile.clone + launchedAsProfile.clone?.handle -> requireNotNull(launchedAsProfile.clone) else -> error("launchedByUser must be a member of launchedByProfile") } val launchedAsProfileType: Profile.Type = launchedAsProfile.type diff --git a/tests/unit/src/com/android/intentresolver/v2/ProfileAvailabilityTest.kt b/tests/unit/src/com/android/intentresolver/v2/ProfileAvailabilityTest.kt index 9f2b3e0f..c0d5ed4e 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ProfileAvailabilityTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ProfileAvailabilityTest.kt @@ -22,6 +22,7 @@ import com.android.intentresolver.v2.domain.interactor.UserInteractor import com.android.intentresolver.v2.shared.model.Profile import com.android.intentresolver.v2.shared.model.User import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -40,8 +41,7 @@ class ProfileAvailabilityTest { @Test fun testProfileAvailable() = runTest { - val availability = ProfileAvailability(backgroundScope, interactor, mapOf()) - runCurrent() + val availability = ProfileAvailability(interactor, this, Dispatchers.IO) assertThat(availability.isAvailable(personalProfile)).isTrue() assertThat(availability.isAvailable(workProfile)).isTrue() @@ -59,8 +59,7 @@ class ProfileAvailabilityTest { @Test fun waitingToEnableProfile() = runTest { - val availability = ProfileAvailability(backgroundScope, interactor, mapOf()) - runCurrent() + val availability = ProfileAvailability(interactor, this, Dispatchers.IO) availability.requestQuietModeState(workProfile, true) assertThat(availability.waitingToEnableProfile).isFalse() @@ -68,7 +67,6 @@ class ProfileAvailabilityTest { availability.requestQuietModeState(workProfile, false) assertThat(availability.waitingToEnableProfile).isTrue() - runCurrent() assertThat(availability.waitingToEnableProfile).isFalse() diff --git a/tests/unit/src/com/android/intentresolver/v2/ProfileHelperTest.kt b/tests/unit/src/com/android/intentresolver/v2/ProfileHelperTest.kt index cb4b1d0a..06d795fe 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ProfileHelperTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ProfileHelperTest.kt @@ -17,20 +17,15 @@ package com.android.intentresolver.v2 import com.android.intentresolver.Flags.FLAG_ENABLE_PRIVATE_PROFILE -import com.android.intentresolver.inject.FakeChooserServiceFlags import com.android.intentresolver.inject.FakeIntentResolverFlags -import com.android.intentresolver.inject.IntentResolverFlags import com.android.intentresolver.v2.annotation.JavaInterop import com.android.intentresolver.v2.data.repository.FakeUserRepository import com.android.intentresolver.v2.domain.interactor.UserInteractor import com.android.intentresolver.v2.shared.model.Profile import com.android.intentresolver.v2.shared.model.User -import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest -import org.junit.Assert.* - import org.junit.Test @OptIn(JavaInterop::class) @@ -48,67 +43,69 @@ class ProfileHelperTest { private val privateUser = User(12, User.Role.PRIVATE) private val privateProfile = Profile(Profile.Type.PRIVATE, privateUser) - private val flags = FakeIntentResolverFlags().apply { - setFlag(FLAG_ENABLE_PRIVATE_PROFILE, true) - } + private val flags = + FakeIntentResolverFlags().apply { setFlag(FLAG_ENABLE_PRIVATE_PROFILE, true) } private fun assertProfiles( helper: ProfileHelper, personalProfile: Profile, workProfile: Profile? = null, - privateProfile: Profile? = null) { - + privateProfile: Profile? = null + ) { assertThat(helper.personalProfile).isEqualTo(personalProfile) assertThat(helper.personalHandle).isEqualTo(personalProfile.primary.handle) personalProfile.clone?.also { assertThat(helper.cloneUserPresent).isTrue() assertThat(helper.cloneHandle).isEqualTo(it.handle) - } ?: { - assertThat(helper.cloneUserPresent).isFalse() - assertThat(helper.cloneHandle).isNull() } + ?: { + assertThat(helper.cloneUserPresent).isFalse() + assertThat(helper.cloneHandle).isNull() + } workProfile?.also { assertThat(helper.workProfilePresent).isTrue() assertThat(helper.workProfile).isEqualTo(it) assertThat(helper.workHandle).isEqualTo(it.primary.handle) - } ?: { - assertThat(helper.workProfilePresent).isFalse() - assertThat(helper.workProfile).isNull() - assertThat(helper.workHandle).isNull() } + ?: { + assertThat(helper.workProfilePresent).isFalse() + assertThat(helper.workProfile).isNull() + assertThat(helper.workHandle).isNull() + } privateProfile?.also { assertThat(helper.privateProfilePresent).isTrue() assertThat(helper.privateProfile).isEqualTo(it) assertThat(helper.privateHandle).isEqualTo(it.primary.handle) - } ?: { - assertThat(helper.privateProfilePresent).isFalse() - assertThat(helper.privateProfile).isNull() - assertThat(helper.privateHandle).isNull() } + ?: { + assertThat(helper.privateProfilePresent).isFalse() + assertThat(helper.privateProfile).isNull() + assertThat(helper.privateHandle).isNull() + } } - @Test fun launchedByPersonal() = runTest { val repository = FakeUserRepository(listOf(personalUser)) val interactor = UserInteractor(repository, launchedAs = personalUser.handle) - val launchedBy = interactor.launchedAsProfile.first() - val helper = ProfileHelper( - interactor = interactor, - flags = flags, - profiles = interactor.profiles.first(), - launchedAsProfile = launchedBy) + val helper = + ProfileHelper( + interactor = interactor, + scope = this, + background = Dispatchers.Unconfined, + flags = flags + ) assertProfiles(helper, personalProfile) assertThat(helper.isLaunchedAsCloneProfile).isFalse() assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL) assertThat(helper.getQueryIntentsHandle(personalUser.handle)) - .isEqualTo(personalProfile.primary.handle) + .isEqualTo(personalProfile.primary.handle) assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle) } @@ -116,13 +113,14 @@ class ProfileHelperTest { fun launchedByPersonal_withClone() = runTest { val repository = FakeUserRepository(listOf(personalUser, cloneUser)) val interactor = UserInteractor(repository, launchedAs = personalUser.handle) - val launchedBy = interactor.launchedAsProfile.first() - val helper = ProfileHelper( - interactor = interactor, - flags = flags, - profiles = interactor.profiles.first(), - launchedAsProfile = launchedBy) + val helper = + ProfileHelper( + interactor = interactor, + scope = this, + background = Dispatchers.Unconfined, + flags = flags + ) assertProfiles(helper, personalWithCloneProfile) @@ -136,47 +134,46 @@ class ProfileHelperTest { fun launchedByClone() = runTest { val repository = FakeUserRepository(listOf(personalUser, cloneUser)) val interactor = UserInteractor(repository, launchedAs = cloneUser.handle) - val launchedBy = interactor.launchedAsProfile.first() - val helper = ProfileHelper( - interactor = interactor, - flags = flags, - profiles = interactor.profiles.first(), - launchedAsProfile = launchedBy) + val helper = + ProfileHelper( + interactor = interactor, + scope = this, + background = Dispatchers.Unconfined, + flags = flags + ) assertProfiles(helper, personalWithCloneProfile) assertThat(helper.isLaunchedAsCloneProfile).isTrue() assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL) assertThat(helper.getQueryIntentsHandle(personalWithCloneProfile.primary.handle)) - .isEqualTo(personalWithCloneProfile.clone?.handle) + .isEqualTo(personalWithCloneProfile.clone?.handle) assertThat(helper.tabOwnerUserHandleForLaunch) - .isEqualTo(personalWithCloneProfile.primary.handle) + .isEqualTo(personalWithCloneProfile.primary.handle) } @Test fun launchedByPersonal_withWork() = runTest { val repository = FakeUserRepository(listOf(personalUser, workUser)) val interactor = UserInteractor(repository, launchedAs = personalUser.handle) - val launchedBy = interactor.launchedAsProfile.first() - val helper = ProfileHelper( - interactor = interactor, - flags = flags, - profiles = interactor.profiles.first(), - launchedAsProfile = launchedBy) + val helper = + ProfileHelper( + interactor = interactor, + scope = this, + background = Dispatchers.Unconfined, + flags = flags + ) - - assertProfiles(helper, - personalProfile = personalProfile, - workProfile = workProfile) + assertProfiles(helper, personalProfile = personalProfile, workProfile = workProfile) assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL) assertThat(helper.isLaunchedAsCloneProfile).isFalse() assertThat(helper.getQueryIntentsHandle(personalUser.handle)) - .isEqualTo(personalProfile.primary.handle) + .isEqualTo(personalProfile.primary.handle) assertThat(helper.getQueryIntentsHandle(workUser.handle)) - .isEqualTo(workProfile.primary.handle) + .isEqualTo(workProfile.primary.handle) assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle) } @@ -184,50 +181,47 @@ class ProfileHelperTest { fun launchedByWork() = runTest { val repository = FakeUserRepository(listOf(personalUser, workUser)) val interactor = UserInteractor(repository, launchedAs = workUser.handle) - val launchedBy = interactor.launchedAsProfile.first() - val helper = ProfileHelper( - interactor = interactor, - flags = flags, - profiles = interactor.profiles.first(), - launchedAsProfile = launchedBy) + val helper = + ProfileHelper( + interactor = interactor, + scope = this, + background = Dispatchers.Unconfined, + flags = flags + ) - assertProfiles(helper, - personalProfile = personalProfile, - workProfile = workProfile) + assertProfiles(helper, personalProfile = personalProfile, workProfile = workProfile) assertThat(helper.isLaunchedAsCloneProfile).isFalse() assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.WORK) assertThat(helper.getQueryIntentsHandle(personalProfile.primary.handle)) - .isEqualTo(personalProfile.primary.handle) + .isEqualTo(personalProfile.primary.handle) assertThat(helper.getQueryIntentsHandle(workProfile.primary.handle)) - .isEqualTo(workProfile.primary.handle) - assertThat(helper.tabOwnerUserHandleForLaunch) - .isEqualTo(workProfile.primary.handle) + .isEqualTo(workProfile.primary.handle) + assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(workProfile.primary.handle) } @Test fun launchedByPersonal_withPrivate() = runTest { val repository = FakeUserRepository(listOf(personalUser, privateUser)) val interactor = UserInteractor(repository, launchedAs = personalUser.handle) - val launchedBy = interactor.launchedAsProfile.first() - val helper = ProfileHelper( - interactor = interactor, - flags = flags, - profiles = interactor.profiles.first(), - launchedAsProfile = launchedBy) + val helper = + ProfileHelper( + interactor = interactor, + scope = this, + background = Dispatchers.Unconfined, + flags = flags + ) - assertProfiles(helper, - personalProfile = personalProfile, - privateProfile = privateProfile) + assertProfiles(helper, personalProfile = personalProfile, privateProfile = privateProfile) assertThat(helper.isLaunchedAsCloneProfile).isFalse() assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL) assertThat(helper.getQueryIntentsHandle(personalProfile.primary.handle)) - .isEqualTo(personalProfile.primary.handle) + .isEqualTo(personalProfile.primary.handle) assertThat(helper.getQueryIntentsHandle(privateProfile.primary.handle)) - .isEqualTo(privateProfile.primary.handle) + .isEqualTo(privateProfile.primary.handle) assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle) } @@ -235,25 +229,23 @@ class ProfileHelperTest { fun launchedByPrivate() = runTest { val repository = FakeUserRepository(listOf(personalUser, privateUser)) val interactor = UserInteractor(repository, launchedAs = privateUser.handle) - val launchedBy = interactor.launchedAsProfile.first() - - val helper = ProfileHelper( - interactor = interactor, - flags = flags, - profiles = interactor.profiles.first(), - launchedAsProfile = launchedBy) + val helper = + ProfileHelper( + interactor = interactor, + scope = this, + background = Dispatchers.Unconfined, + flags = flags + ) - assertProfiles(helper, - personalProfile = personalProfile, - privateProfile = privateProfile) + assertProfiles(helper, personalProfile = personalProfile, privateProfile = privateProfile) assertThat(helper.isLaunchedAsCloneProfile).isFalse() assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PRIVATE) assertThat(helper.getQueryIntentsHandle(personalProfile.primary.handle)) - .isEqualTo(personalProfile.primary.handle) + .isEqualTo(personalProfile.primary.handle) assertThat(helper.getQueryIntentsHandle(privateProfile.primary.handle)) - .isEqualTo(privateProfile.primary.handle) + .isEqualTo(privateProfile.primary.handle) assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(privateProfile.primary.handle) } @@ -263,22 +255,21 @@ class ProfileHelperTest { val repository = FakeUserRepository(listOf(personalUser, privateUser)) val interactor = UserInteractor(repository, launchedAs = personalUser.handle) - val launchedBy = interactor.launchedAsProfile.first() - val helper = ProfileHelper( - interactor = interactor, - flags = flags, - profiles = interactor.profiles.first(), - launchedAsProfile = launchedBy) + val helper = + ProfileHelper( + interactor = interactor, + scope = this, + background = Dispatchers.Unconfined, + flags = flags + ) - assertProfiles(helper, - personalProfile = personalProfile, - privateProfile = null) + assertProfiles(helper, personalProfile = personalProfile, privateProfile = null) assertThat(helper.isLaunchedAsCloneProfile).isFalse() assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL) assertThat(helper.getQueryIntentsHandle(personalProfile.primary.handle)) - .isEqualTo(personalProfile.primary.handle) + .isEqualTo(personalProfile.primary.handle) assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle) } -} \ No newline at end of file +} -- cgit v1.2.3-59-g8ed1b