Readability clean up for UserRepository

Use a sealed interface for events to eliminate unused properties.
Extract the event branching from the flow statement.
Improve logging and extract to a simple method.

Bug: NA
Test: atest IntentResolver-tests-unit
Flag: NA
Change-Id: I348558b3ca1506b7ece1c19295263e8824ac2575
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 4c42e2c..4067224 100644
--- a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
@@ -28,6 +28,7 @@
 import android.content.Intent.EXTRA_USER
 import android.content.IntentFilter
 import android.content.pm.UserInfo
+import android.os.Build
 import android.os.UserHandle
 import android.os.UserManager
 import android.util.Log
@@ -36,7 +37,6 @@
 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.repository.UserRepositoryImpl.UserEvent
 import com.android.intentresolver.v2.shared.model.User
 import dagger.hilt.android.qualifiers.ApplicationContext
 import javax.inject.Inject
@@ -73,7 +73,7 @@
      * stopping a profile user (along with their many associated processes).
      *
      * If successful, the change will be applied after the call returns and can be observed using
-     * [UserRepository.isAvailable] for the given user.
+     * [UserRepository.availability] for the given user.
      *
      * No actions are taken if the user is already in requested state.
      *
@@ -84,9 +84,9 @@
 
 private const val TAG = "UserRepository"
 
-private data class UserWithState(val user: User, val available: Boolean)
+internal data class UserWithState(val user: User, val available: Boolean)
 
-private typealias UserStates = List<UserWithState>
+internal typealias UserStates = List<UserWithState>
 
 /** Tracks and publishes state for the parent user and associated profiles. */
 class UserRepositoryImpl
@@ -114,13 +114,21 @@
         background
     )
 
-    data class UserEvent(val action: String, val user: UserHandle, val quietMode: Boolean = false)
+    private fun debugLog(msg: () -> String) {
+        if (Build.IS_USERDEBUG || Build.IS_ENG) {
+            Log.d(TAG, msg())
+        }
+    }
+
+    private fun errorLog(msg: String, caught: Throwable? = null) {
+        Log.e(TAG, msg, caught)
+    }
 
     /**
      * An exception which indicates that an inconsistency exists between the user state map and the
      * rest of the system.
      */
-    internal class UserStateException(
+    private class UserStateException(
         override val message: String,
         val event: UserEvent,
         override val cause: Throwable? = null
@@ -129,35 +137,34 @@
     private val sharingScope = CoroutineScope(scope.coroutineContext + backgroundDispatcher)
     private val usersWithState: Flow<UserStates> =
         userEvents
-            .onStart { emit(UserEvent(INITIALIZE, profileParent)) }
-            .onEach { Log.i(TAG, "userEvent: $it") }
-            .runningFold<UserEvent, UserStates>(emptyList()) { users, event ->
-                try {
-                    // Handle an action by performing some operation, then returning a new map
-                    when (event.action) {
-                        INITIALIZE -> createNewUserStates(profileParent)
-                        ACTION_PROFILE_ADDED -> handleProfileAdded(event, users)
-                        ACTION_PROFILE_REMOVED -> handleProfileRemoved(event, users)
-                        ACTION_MANAGED_PROFILE_UNAVAILABLE,
-                        ACTION_MANAGED_PROFILE_AVAILABLE,
-                        ACTION_PROFILE_AVAILABLE,
-                        ACTION_PROFILE_UNAVAILABLE -> handleAvailability(event, users)
-                        else -> {
-                            Log.w(TAG, "Unhandled event: $event)")
-                            users
-                        }
-                    }
-                } catch (e: UserStateException) {
-                    Log.e(TAG, "An error occurred handling an event: ${e.event}", e)
-                    Log.e(TAG, "Attempting to recover...")
-                    createNewUserStates(profileParent)
-                }
-            }
+            .onStart { emit(Initialize) }
+            .onEach { debugLog { "userEvent: $it" } }
+            .runningFold(emptyList(), ::handleEvent)
             .distinctUntilChanged()
-            .onEach { Log.i(TAG, "userStateList: $it") }
+            .onEach { debugLog { "userStateList: $it" } }
             .stateIn(sharingScope, SharingStarted.Eagerly, emptyList())
             .filterNot { it.isEmpty() }
 
+    private suspend fun handleEvent(users: UserStates, event: UserEvent): UserStates {
+        return try {
+            // Handle an action by performing some operation, then returning a new map
+            when (event) {
+                is Initialize -> createNewUserStates(profileParent)
+                is ProfileAdded -> handleProfileAdded(event, users)
+                is ProfileRemoved -> handleProfileRemoved(event, users)
+                is AvailabilityChange -> handleAvailability(event, users)
+                is UnknownEvent -> {
+                    debugLog { "Unhandled event: $event)" }
+                    users
+                }
+            }
+        } catch (e: UserStateException) {
+            errorLog("An error occurred handling an event: ${e.event}")
+            errorLog("Attempting to recover...", e)
+            createNewUserStates(profileParent)
+        }
+    }
+
     override val users: Flow<List<User>> =
         usersWithState.map { userStateMap -> userStateMap.map { it.user } }.distinctUntilChanged()
 
@@ -168,7 +175,7 @@
 
     override suspend fun requestState(user: User, available: Boolean) {
         return withContext(backgroundDispatcher) {
-            Log.i(TAG, "requestQuietModeEnabled: ${!available} for user $user")
+            debugLog { "requestQuietModeEnabled: ${!available} for user $user" }
             userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user.handle)
         }
     }
@@ -176,28 +183,28 @@
     private fun List<UserWithState>.update(handle: UserHandle, user: UserWithState) =
         filter { it.user.id != handle.identifier } + user
 
-    private fun handleAvailability(event: UserEvent, current: UserStates): UserStates {
+    private fun handleAvailability(event: AvailabilityChange, current: UserStates): UserStates {
         val userEntry =
             current.firstOrNull { it.user.id == event.user.identifier }
                 ?: throw UserStateException("User was not present in the map", event)
         return current.update(event.user, userEntry.copy(available = !event.quietMode))
     }
 
-    private fun handleProfileRemoved(event: UserEvent, current: UserStates): UserStates {
+    private fun handleProfileRemoved(event: ProfileRemoved, current: UserStates): UserStates {
         if (!current.any { it.user.id == event.user.identifier }) {
             throw UserStateException("User was not present in the map", event)
         }
         return current.filter { it.user.id != event.user.identifier }
     }
 
-    private suspend fun handleProfileAdded(event: UserEvent, current: UserStates): UserStates {
+    private suspend fun handleProfileAdded(event: ProfileAdded, current: UserStates): UserStates {
         val user =
             try {
                 requireNotNull(readUser(event.user))
             } catch (e: Exception) {
                 throw UserStateException("Failed to read user from UserManager", event, e)
             }
-        return current + UserWithState(user, !event.quietMode)
+        return current + UserWithState(user, true)
     }
 
     private suspend fun createNewUserStates(user: UserHandle): UserStates {
@@ -224,29 +231,64 @@
     }
 }
 
+/** A Model representing changes to profiles and availability */
+sealed interface UserEvent
+
+/** Used as a an initial value to trigger a fetch of all profile data. */
+data object Initialize : UserEvent
+
+/** A profile was added to the profile group. */
+data class ProfileAdded(
+    /** The handle for the added profile. */
+    val user: UserHandle,
+) : UserEvent
+
+/** A profile was removed from the profile group. */
+data class ProfileRemoved(
+    /** The handle for the removed profile. */
+    val user: UserHandle,
+) : UserEvent
+
+/** A profile has changed availability. */
+data class AvailabilityChange(
+    /** THe handle for the profile with availability change. */
+    val user: UserHandle,
+    /** The new quietMode state. */
+    val quietMode: Boolean = false,
+) : UserEvent
+
+/** An unhandled event, logged and ignored. */
+data class UnknownEvent(
+    /** The broadcast intent action received */
+    val action: String?,
+) : UserEvent
+
 /** Used with [broadcastFlow] to transform a UserManager broadcast action into a [UserEvent]. */
-private fun Intent.toUserEvent(): UserEvent? {
+private fun Intent.toUserEvent(): UserEvent {
     val action = action
     val user = extras?.getParcelable(EXTRA_USER, UserHandle::class.java)
-    val quietMode = extras?.getBoolean(EXTRA_QUIET_MODE, false) ?: false
-    return if (user == null || action == null) {
-        null
-    } else {
-        UserEvent(action, user, quietMode)
+    val quietMode = extras?.getBoolean(EXTRA_QUIET_MODE, false)
+    return when (action) {
+        ACTION_PROFILE_ADDED -> ProfileAdded(requireNotNull(user))
+        ACTION_PROFILE_REMOVED -> ProfileRemoved(requireNotNull(user))
+        ACTION_MANAGED_PROFILE_UNAVAILABLE,
+        ACTION_MANAGED_PROFILE_AVAILABLE,
+        ACTION_PROFILE_AVAILABLE,
+        ACTION_PROFILE_UNAVAILABLE ->
+            AvailabilityChange(requireNotNull(user), requireNotNull(quietMode))
+        else -> UnknownEvent(action)
     }
 }
 
-const val INITIALIZE = "INITIALIZE"
-
 private fun createFilter(actions: Iterable<String>): IntentFilter {
     return IntentFilter().apply { actions.forEach(::addAction) }
 }
 
-private fun UserInfo?.isAvailable(): Boolean {
+internal fun UserInfo?.isAvailable(): Boolean {
     return this?.isQuietModeEnabled != true
 }
 
-private fun userBroadcastFlow(context: Context, profileParent: UserHandle): Flow<UserEvent> {
+internal fun userBroadcastFlow(context: Context, profileParent: UserHandle): Flow<UserEvent> {
     val userActions =
         setOf(
             ACTION_PROFILE_ADDED,
diff --git a/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt b/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt
index 370e5a0..d1b56d5 100644
--- a/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt
+++ b/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt
@@ -1,12 +1,6 @@
 package com.android.intentresolver.v2.platform
 
 import android.content.Context
-import android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE
-import android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE
-import android.content.Intent.ACTION_PROFILE_ADDED
-import android.content.Intent.ACTION_PROFILE_AVAILABLE
-import android.content.Intent.ACTION_PROFILE_REMOVED
-import android.content.Intent.ACTION_PROFILE_UNAVAILABLE
 import android.content.pm.UserInfo
 import android.content.pm.UserInfo.FLAG_FULL
 import android.content.pm.UserInfo.FLAG_INITIALIZED
@@ -18,7 +12,10 @@
 import androidx.annotation.NonNull
 import com.android.intentresolver.THROWS_EXCEPTION
 import com.android.intentresolver.mock
-import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent
+import com.android.intentresolver.v2.data.repository.AvailabilityChange
+import com.android.intentresolver.v2.data.repository.ProfileAdded
+import com.android.intentresolver.v2.data.repository.ProfileRemoved
+import com.android.intentresolver.v2.data.repository.UserEvent
 import com.android.intentresolver.v2.platform.FakeUserManager.State
 import com.android.intentresolver.whenever
 import kotlin.random.Random
@@ -155,21 +152,7 @@
                     } else {
                         it.flags and UserInfo.FLAG_QUIET_MODE.inv()
                     }
-                val actions = mutableListOf<String>()
-                if (quietMode) {
-                    actions += ACTION_PROFILE_UNAVAILABLE
-                    if (it.isManagedProfile) {
-                        actions += ACTION_MANAGED_PROFILE_UNAVAILABLE
-                    }
-                } else {
-                    actions += ACTION_PROFILE_AVAILABLE
-                    if (it.isManagedProfile) {
-                        actions += ACTION_MANAGED_PROFILE_AVAILABLE
-                    }
-                }
-                actions.forEach { action ->
-                    eventChannel.trySend(UserEvent(action, user, quietMode))
-                }
+                eventChannel.trySend(AvailabilityChange(user, quietMode))
             }
         }
 
@@ -187,7 +170,7 @@
                     profileGroupId = parentUser.profileGroupId
                 }
             userInfoMap[userInfo.userHandle] = userInfo
-            eventChannel.trySend(UserEvent(ACTION_PROFILE_ADDED, userInfo.userHandle))
+            eventChannel.trySend(ProfileAdded(userInfo.userHandle))
             return userInfo.userHandle
         }
 
@@ -195,7 +178,7 @@
             return userInfoMap[handle]?.let { user ->
                 require(user.isProfile) { "Only profiles can be removed" }
                 userInfoMap.remove(user.userHandle)
-                eventChannel.trySend(UserEvent(ACTION_PROFILE_REMOVED, user.userHandle))
+                eventChannel.trySend(ProfileRemoved(user.userHandle))
                 return true
             }
                 ?: false
diff --git a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt b/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
index 8628bd1..3fcc4c8 100644
--- a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
+++ b/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
@@ -1,6 +1,5 @@
 package com.android.intentresolver.v2.data.repository
 
-import android.content.Intent
 import android.content.pm.UserInfo
 import android.os.UserHandle
 import android.os.UserHandle.SYSTEM
@@ -122,13 +121,7 @@
     fun recovers_from_invalid_profile_added_event() = runTest {
         val userManager =
             mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL)
-        val events =
-            flowOf(
-                UserRepositoryImpl.UserEvent(
-                    Intent.ACTION_PROFILE_ADDED,
-                    UserHandle.of(UserHandle.USER_NULL)
-                )
-            )
+        val events = flowOf(ProfileAdded(UserHandle.of(UserHandle.USER_NULL)))
         val repo =
             UserRepositoryImpl(
                 profileParent = SYSTEM,
@@ -147,13 +140,7 @@
     fun recovers_from_invalid_profile_removed_event() = runTest {
         val userManager =
             mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL)
-        val events =
-            flowOf(
-                UserRepositoryImpl.UserEvent(
-                    Intent.ACTION_PROFILE_REMOVED,
-                    UserHandle.of(UserHandle.USER_NULL)
-                )
-            )
+        val events = flowOf(ProfileRemoved(UserHandle.of(UserHandle.USER_NULL)))
         val repo =
             UserRepositoryImpl(
                 profileParent = SYSTEM,
@@ -172,13 +159,7 @@
     fun recovers_from_invalid_profile_available_event() = runTest {
         val userManager =
             mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL)
-        val events =
-            flowOf(
-                UserRepositoryImpl.UserEvent(
-                    Intent.ACTION_PROFILE_AVAILABLE,
-                    UserHandle.of(UserHandle.USER_NULL)
-                )
-            )
+        val events = flowOf(AvailabilityChange(UserHandle.of(UserHandle.USER_NULL)))
         val repo =
             UserRepositoryImpl(SYSTEM, userManager, events, backgroundScope, Dispatchers.Unconfined)
         val users by collectLastValue(repo.users)
@@ -191,10 +172,7 @@
     fun recovers_from_unknown_event() = runTest {
         val userManager =
             mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL)
-        val events =
-            flowOf(
-                UserRepositoryImpl.UserEvent("UNKNOWN_EVENT", UserHandle.of(UserHandle.USER_NULL))
-            )
+        val events = flowOf(UnknownEvent("UNKNOWN_EVENT"))
         val repo =
             UserRepositoryImpl(
                 profileParent = SYSTEM,