diff options
| author | 2024-04-03 20:27:47 +0000 | |
|---|---|---|
| committer | 2024-04-03 20:27:47 +0000 | |
| commit | fd12143a9536509117ce3b78ff4d358d210a96ab (patch) | |
| tree | 7d542d8123c425286e747492c26239c52f97d3c6 /java/src | |
| parent | 931f6f7500af03b0ccbc1842983f655230441181 (diff) | |
| parent | 8e7e81138fc72d5df530cb77032f9aae1f18cb2c (diff) | |
Merge changes from topic "ir_user_repo_flows" into main
* changes:
Simplify startup and Profile flow handling
Add @Broadcast Handler, broadcastFlow() -> BroadcastSubscriber
Diffstat (limited to 'java/src')
9 files changed, 177 insertions, 134 deletions
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/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,35 +42,10 @@ 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<Profile>, - val availability: Map<Profile, Boolean>, - val launchedAs: Profile -) - -/** * __Purpose__ * * Cleanup aid. Provides a pathway to cleaner code. @@ -113,7 +87,7 @@ constructor( private val activity: ComponentActivity = hostActivity as ComponentActivity private val viewModel by activity.viewModels<ChooserViewModel>() - private lateinit var activityInitializer: ChooserInitializer + private lateinit var activityInitializer: Runnable var onChooserRequestChanged: Consumer<ChooserRequest> = 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<ChooserRequest>) { 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<Profile, Boolean> + 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<Profile>, - 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/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 <T> broadcastFlow( - context: Context, - filter: IntentFilter, - user: UserHandle, - transform: (Intent) -> T? -): Flow<T> = 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 <T> createFlow( + filter: IntentFilter, + user: UserHandle, + transform: (Intent) -> T?, + ): Flow<T> = 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<UserWithState> +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<UserEvent>, 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<String>): IntentFilter { +internal fun createFilter(actions: Iterable<String>): IntentFilter { return IntentFilter().apply { actions.forEach(::addAction) } } internal fun UserInfo?.isAvailable(): Boolean { return this?.isQuietModeEnabled != true } - -internal fun userBroadcastFlow(context: Context, profileParent: UserHandle): Flow<UserEvent> { - 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) -} |