Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f3a383c6 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Remove the dependency on Mockito to create PeopleSpace fakes (1/2)

This CL refactors PeopleViewModel so that it can be created easily with
fake values in the Gallery app and screenshot tests. That way, we don't
need to depend on Mockito to create fake dependencies of the ViewModel,
which is necessary for deviceless RNG tests (and better architecture)
and Compose @Previews.

This is a pure refactoring that doesn't change any logic in the code.

Bug: 304719047
Test: atest PeopleSpaceScreenshotTest
Change-Id: I701a2fa6b5dd933c9be9d6b0d97deb62b75ba3d8
parent 1298fe85
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -47,10 +47,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.res.R
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.res.R

/**
 * Compose the screen associated to a [PeopleViewModel].
@@ -86,9 +86,9 @@ fun PeopleScreen(
        modifier = Modifier.fillMaxSize(),
    ) {
        if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
            PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel::onTileClicked)
            PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel.onTileClicked)
        } else {
            PeopleScreenEmpty(viewModel::onUserJourneyCancelled)
            PeopleScreenEmpty(viewModel.onUserJourneyCancelled)
        }
    }
}
+3 −3
Original line number Diff line number Diff line
@@ -31,10 +31,10 @@ import androidx.lifecycle.Lifecycle.State.CREATED
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.res.R
import com.android.systemui.people.PeopleSpaceTileView
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.res.R
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@@ -101,10 +101,10 @@ object PeopleViewBinder {
                                view,
                                priorityTiles,
                                recentTiles,
                                viewModel::onTileClicked,
                                viewModel.onTileClicked,
                            )
                        } else {
                            setNoConversationsContent(view, viewModel::onUserJourneyCancelled)
                            setNoConversationsContent(view, viewModel.onUserJourneyCancelled)
                        }
                    }
            }
+95 −69
Original line number Diff line number Diff line
@@ -23,35 +23,32 @@ import android.content.Intent
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.systemui.res.R
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.people.PeopleSpaceUtils
import com.android.systemui.people.PeopleTileViewHelper
import com.android.systemui.people.data.model.PeopleTileModel
import com.android.systemui.people.data.repository.PeopleTileRepository
import com.android.systemui.people.data.repository.PeopleWidgetRepository
import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

private const val TAG = "PeopleViewModel"

/**
 * Models UI state for the people space, allowing the user to select which conversation should be
 * associated to a new or existing Conversation widget.
 */
class PeopleViewModel(
    @Application private val context: Context,
    private val tileRepository: PeopleTileRepository,
    private val widgetRepository: PeopleWidgetRepository,
) : ViewModel() {
    /**
     * The list of the priority tiles/conversations.
     *
     * Important: Even though this is a Flow, the underlying API used to populate this Flow is not
     * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
     */
    private val _priorityTiles = MutableStateFlow(priorityTiles())
    val priorityTiles: StateFlow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
    val priorityTiles: StateFlow<List<PeopleTileViewModel>>,

    /**
     * The list of the priority tiles/conversations.
@@ -59,71 +56,124 @@ class PeopleViewModel(
     * Important: Even though this is a Flow, the underlying API used to populate this Flow is not
     * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
     */
    private val _recentTiles = MutableStateFlow(recentTiles())
    val recentTiles: StateFlow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
    val recentTiles: StateFlow<List<PeopleTileViewModel>>,

    /** The ID of the widget currently being edited/added. */
    private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
    val appWidgetId: StateFlow<Int> = _appWidgetId.asStateFlow()
    val appWidgetId: StateFlow<Int>,

    /** The result of this user journey. */
    private val _result = MutableStateFlow<Result?>(null)
    val result: StateFlow<Result?> = _result.asStateFlow()
    val result: StateFlow<Result?>,

    /** Refresh the [priorityTiles] and [recentTiles]. */
    fun onTileRefreshRequested() {
        _priorityTiles.value = priorityTiles()
        _recentTiles.value = recentTiles()
    }
    val onTileRefreshRequested: () -> Unit,

    /** Called when the [appWidgetId] should be changed to [widgetId]. */
    fun onWidgetIdChanged(widgetId: Int) {
        _appWidgetId.value = widgetId
    }
    val onWidgetIdChanged: (widgetId: Int) -> Unit,

    /** Clear [result], setting it to null. */
    fun clearResult() {
        _result.value = null
    }
    val clearResult: () -> Unit,

    /** Called when a tile is clicked. */
    fun onTileClicked(tile: PeopleTileViewModel) {
        val widgetId = _appWidgetId.value
        if (PeopleSpaceUtils.DEBUG) {
            Log.d(
                TAG,
                "Put ${tile.username}'s shortcut ID: ${tile.key.shortcutId} for widget ID $widgetId"
            )
    val onTileClicked: (tile: PeopleTileViewModel) -> Unit,

    /** Called when this user journey is cancelled. */
    val onUserJourneyCancelled: () -> Unit,
) : ViewModel() {
    /** The Factory that should be used to create a [PeopleViewModel]. */
    class Factory
    @Inject
    constructor(
        @Application private val context: Context,
        private val tileRepository: PeopleTileRepository,
        private val widgetRepository: PeopleWidgetRepository,
    ) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            check(modelClass == PeopleViewModel::class.java)
            return PeopleViewModel(context, tileRepository, widgetRepository) as T
        }
        widgetRepository.setWidgetTile(widgetId, tile.key)
        _result.value =
            Result.Success(Intent().apply { putExtra(EXTRA_APPWIDGET_ID, appWidgetId.value) })
    }

    /** Called when this user journey is cancelled. */
    fun onUserJourneyCancelled() {
        _result.value = Result.Cancelled
    sealed class Result {
        class Success(val data: Intent) : Result()

        object Cancelled : Result()
    }
}

    private fun priorityTiles(): List<PeopleTileViewModel> {
private fun PeopleViewModel(
    @Application context: Context,
    tileRepository: PeopleTileRepository,
    widgetRepository: PeopleWidgetRepository,
): PeopleViewModel {
    fun priorityTiles(): List<PeopleTileViewModel> {
        return try {
            tileRepository.priorityTiles().map { it.toViewModel() }
            tileRepository.priorityTiles().map { it.toViewModel(context) }
        } catch (e: Exception) {
            Log.e(TAG, "Couldn't retrieve priority conversations", e)
            emptyList()
        }
    }

    private fun recentTiles(): List<PeopleTileViewModel> {
    fun recentTiles(): List<PeopleTileViewModel> {
        return try {
            tileRepository.recentTiles().map { it.toViewModel() }
            tileRepository.recentTiles().map { it.toViewModel(context) }
        } catch (e: Exception) {
            Log.e(TAG, "Couldn't retrieve recent conversations", e)
            emptyList()
        }
    }

    private fun PeopleTileModel.toViewModel(): PeopleTileViewModel {
    val priorityTiles = MutableStateFlow(priorityTiles())
    val recentTiles = MutableStateFlow(recentTiles())
    val appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
    val result = MutableStateFlow<PeopleViewModel.Result?>(null)

    fun onTileRefreshRequested() {
        priorityTiles.value = priorityTiles()
        recentTiles.value = recentTiles()
    }

    fun onWidgetIdChanged(widgetId: Int) {
        appWidgetId.value = widgetId
    }

    fun clearResult() {
        result.value = null
    }

    fun onTileClicked(tile: PeopleTileViewModel) {
        val widgetId = appWidgetId.value
        if (PeopleSpaceUtils.DEBUG) {
            Log.d(
                TAG,
                "Put ${tile.username}'s shortcut ID: ${tile.key.shortcutId} for widget ID $widgetId"
            )
        }
        widgetRepository.setWidgetTile(widgetId, tile.key)
        result.value =
            PeopleViewModel.Result.Success(
                Intent().apply { putExtra(EXTRA_APPWIDGET_ID, appWidgetId.value) }
            )
    }

    fun onUserJourneyCancelled() {
        result.value = PeopleViewModel.Result.Cancelled
    }

    return PeopleViewModel(
        priorityTiles = priorityTiles.asStateFlow(),
        recentTiles = recentTiles.asStateFlow(),
        appWidgetId = appWidgetId.asStateFlow(),
        result = result.asStateFlow(),
        onTileRefreshRequested = ::onTileRefreshRequested,
        onWidgetIdChanged = ::onWidgetIdChanged,
        clearResult = ::clearResult,
        onTileClicked = ::onTileClicked,
        onUserJourneyCancelled = ::onUserJourneyCancelled,
    )
}

fun PeopleTileModel.toViewModel(@Application context: Context): PeopleTileViewModel {
    val icon =
        PeopleTileViewHelper.getPersonIconBitmap(
            context,
@@ -136,27 +186,3 @@ class PeopleViewModel(
        )
    return PeopleTileViewModel(key, icon, username)
}

    /** The Factory that should be used to create a [PeopleViewModel]. */
    class Factory
    @Inject
    constructor(
        @Application private val context: Context,
        private val tileRepository: PeopleTileRepository,
        private val widgetRepository: PeopleWidgetRepository,
    ) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            check(modelClass == PeopleViewModel::class.java)
            return PeopleViewModel(context, tileRepository, widgetRepository) as T
        }
    }

    sealed class Result {
        class Success(val data: Intent) : Result()
        object Cancelled : Result()
    }

    companion object {
        private const val TAG = "PeopleViewModel"
    }
}