diff options
| author | 2022-12-19 23:19:22 +0000 | |
|---|---|---|
| committer | 2022-12-19 23:19:22 +0000 | |
| commit | 595b05c582cb0cdd381dd09e8c1fe37149df30f0 (patch) | |
| tree | ee3cfbbba5740f6ff549d99ddf1aa6aa9b5e182b | |
| parent | 976aca3b08336d770afe8c22f7e1f985d875c1cb (diff) | |
| parent | b2433a64ccfad67f338e47918e82e9212ed15a28 (diff) | |
Merge "Add FSI view model" into tm-qpr-dev am: b2433a64cc
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20694074
Change-Id: Ia6e0104a2f625371198668750206840f0b74435d
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
3 files changed, 206 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 30117d988091..4db77abacd69 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -42,6 +42,7 @@ import com.android.systemui.settings.dagger.MultiUserUtilsModule import com.android.systemui.shortcut.ShortcutKeyDispatcher import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo 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.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator @@ -86,6 +87,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(FsiChromeRepo::class) abstract fun bindFSIChromeRepo(sysui: FsiChromeRepo): CoreStartable + /** Inject into FsiChromeWindowViewModel. */ + @Binds + @IntoMap + @ClassKey(FsiChromeViewModelFactory::class) + abstract fun bindFSIChromeWindowViewModel(sysui: FsiChromeViewModelFactory): CoreStartable + /** Inject into GarbageMonitor.Service. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactory.kt new file mode 100644 index 000000000000..1ca698b6bd58 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactory.kt @@ -0,0 +1,87 @@ +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() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactoryTest.kt new file mode 100644 index 000000000000..5cee9e377dfb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewModelFactoryTest.kt @@ -0,0 +1,112 @@ +/* + * 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) + } + } +} |