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

Commit 595b05c5 authored by Lyn Han's avatar Lyn Han Committed by Automerger Merge Worker
Browse files

Merge "Add FSI view model" into tm-qpr-dev am: b2433a64

parents 976aca3b b2433a64
Loading
Loading
Loading
Loading
+7 −0
Original line number Original line Diff line number Diff line
@@ -42,6 +42,7 @@ import com.android.systemui.settings.dagger.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
import com.android.systemui.shortcut.ShortcutKeyDispatcher
import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo
import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.notification.fsi.FsiChromeViewModelFactory
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.stylus.StylusUsiPowerStartable
import com.android.systemui.stylus.StylusUsiPowerStartable
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -86,6 +87,12 @@ abstract class SystemUICoreStartableModule {
    @ClassKey(FsiChromeRepo::class)
    @ClassKey(FsiChromeRepo::class)
    abstract fun bindFSIChromeRepo(sysui: FsiChromeRepo): CoreStartable
    abstract fun bindFSIChromeRepo(sysui: FsiChromeRepo): CoreStartable


    /** Inject into FsiChromeWindowViewModel.  */
    @Binds
    @IntoMap
    @ClassKey(FsiChromeViewModelFactory::class)
    abstract fun bindFSIChromeWindowViewModel(sysui: FsiChromeViewModelFactory): CoreStartable

    /** Inject into GarbageMonitor.Service.  */
    /** Inject into GarbageMonitor.Service.  */
    @Binds
    @Binds
    @IntoMap
    @IntoMap
+87 −0
Original line number Original line Diff line number Diff line
package com.android.systemui.statusbar.notification.fsi

import android.annotation.UiContext
import android.app.PendingIntent
import android.content.Context
import android.graphics.drawable.Drawable
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.notification.fsi.FsiDebug.Companion.log
import com.android.wm.shell.TaskView
import com.android.wm.shell.TaskViewFactory
import java.util.Optional
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.suspendCancellableCoroutine

/**
 * Handle view-related data for fullscreen intent container on lockscreen. Wraps FsiChromeRepo,
 * transforms events/state into view-relevant representation for FsiChromeView. Alive for lifetime
 * of SystemUI.
 */
@SysUISingleton
class FsiChromeViewModelFactory
@Inject
constructor(
    val repo: FsiChromeRepo,
    val taskViewFactory: Optional<TaskViewFactory>,
    @UiContext val context: Context,
    @Main val mainExecutor: Executor,
) : CoreStartable {

    companion object {
        private const val classTag = "FsiChromeViewModelFactory"
    }

    val viewModelFlow: Flow<FsiChromeViewModel?> =
        repo.infoFlow.mapLatest { fsiInfo ->
            fsiInfo?.let {
                log("$classTag viewModelFlow got new fsiInfo")

                // mapLatest emits null when FSIInfo is null
                FsiChromeViewModel(
                    fsiInfo.appName,
                    fsiInfo.appIcon,
                    createTaskView(),
                    fsiInfo.fullscreenIntent,
                    repo
                )
            }
        }

    override fun start() {
        log("$classTag start")
    }

    private suspend fun createTaskView(): TaskView = suspendCancellableCoroutine { k ->
        log("$classTag createTaskView")

        taskViewFactory.get().create(context, mainExecutor) { taskView -> k.resume(taskView) }
    }
}

// Alive for lifetime of FSI.
data class FsiChromeViewModel(
    val appName: String,
    val appIcon: Drawable,
    val taskView: TaskView,
    val fsi: PendingIntent,
    val repo: FsiChromeRepo
) {
    companion object {
        private const val classTag = "FsiChromeViewModel"
    }

    fun onDismiss() {
        log("$classTag onDismiss")
        repo.dismiss()
    }
    fun onFullscreen() {
        log("$classTag onFullscreen")
        repo.onFullscreen()
    }
}
+112 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2022 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.systemui.statusbar.notification.fsi

import android.app.PendingIntent
import android.graphics.drawable.Drawable
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock
import com.android.wm.shell.TaskView
import com.android.wm.shell.TaskViewFactory
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import java.util.function.Consumer
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
class FsiChromeViewModelFactoryTest : SysuiTestCase() {
    @Mock private lateinit var taskViewFactoryOptional: Optional<TaskViewFactory>
    @Mock private lateinit var taskViewFactory: TaskViewFactory
    @Mock lateinit var taskView: TaskView

    @Main var mainExecutor = FakeExecutor(FakeSystemClock())
    lateinit var viewModelFactory: FsiChromeViewModelFactory

    private val fakeInfoFlow = MutableStateFlow<FsiChromeRepo.FSIInfo?>(null)
    private var fsiChromeRepo: FsiChromeRepo =
        mock<FsiChromeRepo>().apply { whenever(infoFlow).thenReturn(fakeInfoFlow) }

    private val appName = "appName"
    private val appIcon: Drawable = context.getDrawable(com.android.systemui.R.drawable.ic_android)
    private val fsi: PendingIntent = Mockito.mock(PendingIntent::class.java)
    private val fsiInfo = FsiChromeRepo.FSIInfo(appName, appIcon, fsi)

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        whenever(taskViewFactoryOptional.get()).thenReturn(taskViewFactory)

        viewModelFactory =
            FsiChromeViewModelFactory(fsiChromeRepo, taskViewFactoryOptional, context, mainExecutor)
    }

    @Test
    fun testViewModelFlow_update_createsTaskView() {
        runTest {
            val latestViewModel =
                viewModelFactory.viewModelFlow
                    .onStart { FsiDebug.log("viewModelFactory.viewModelFlow.onStart") }
                    .stateIn(
                        backgroundScope, // stateIn runs forever, don't count it as test coroutine
                        SharingStarted.Eagerly,
                        null
                    )
            runCurrent() // Drain queued backgroundScope operations

            // Test: emit the fake FSIInfo
            fakeInfoFlow.emit(fsiInfo)
            runCurrent()

            val taskViewFactoryCallback: Consumer<TaskView> = withArgCaptor {
                verify(taskViewFactory).create(any(), any(), capture())
            }
            taskViewFactoryCallback.accept(taskView) // this will call k.resume
            runCurrent()

            // Verify that the factory has produced a new ViewModel
            // containing the relevant data from FsiInfo
            val expectedViewModel =
                FsiChromeViewModel(appName, appIcon, taskView, fsi, fsiChromeRepo)

            assertThat(latestViewModel.value).isEqualTo(expectedViewModel)
        }
    }
}