1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.chips.ui.viewmodel 18 19 import android.annotation.SuppressLint 20 import com.android.app.tracing.coroutines.launchTraced as launch 21 import com.android.systemui.dagger.qualifiers.Application 22 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel 23 import kotlinx.coroutines.CoroutineScope 24 import kotlinx.coroutines.delay 25 import kotlinx.coroutines.flow.Flow 26 import kotlinx.coroutines.flow.MutableSharedFlow 27 import kotlinx.coroutines.flow.SharingStarted 28 import kotlinx.coroutines.flow.StateFlow 29 import kotlinx.coroutines.flow.combine 30 import kotlinx.coroutines.flow.stateIn 31 import kotlinx.coroutines.flow.transformLatest 32 33 /** 34 * A class that can help [OngoingActivityChipViewModel] instances with various transition states. 35 * 36 * For now, this class's only functionality is immediately hiding the chip if the user has tapped an 37 * activity chip and then clicked "Stop" on the resulting dialog. There's a bit of a delay between 38 * when the user clicks "Stop" and when the system services notify SysUI that the activity has 39 * indeed stopped. We don't want the chip to briefly show for a few frames during that delay, so 40 * this class helps us immediately hide the chip as soon as the user clicks "Stop" in the dialog. 41 * See b/353249803#comment4. 42 */ 43 class ChipTransitionHelper(@Application private val scope: CoroutineScope) { 44 /** A flow that emits each time the user has clicked "Stop" on the dialog. */ 45 @SuppressLint("SharedFlowCreation") 46 private val activityStoppedFromDialogEvent = MutableSharedFlow<Unit>() 47 48 /** True if the user recently stopped the activity from the dialog. */ 49 private val wasActivityRecentlyStoppedFromDialog: Flow<Boolean> = 50 activityStoppedFromDialogEvent 51 .transformLatest { 52 // Give system services 500ms to stop the activity and notify SysUI. Once more than 53 // 500ms has elapsed, we should go back to using the current system service 54 // information as the source of truth. 55 emit(true) 56 delay(500) 57 emit(false) 58 } 59 // Use stateIn so that the flow created in [createChipFlow] is guaranteed to 60 // emit. (`combine`s require that all input flows have emitted.) 61 .stateIn(scope, SharingStarted.Lazily, false) 62 63 /** 64 * Notifies this class that the user just clicked "Stop" on the stop dialog that's shown when 65 * the chip is tapped. 66 * 67 * Call this method in order to immediately hide the chip. 68 */ 69 fun onActivityStoppedFromDialog() { 70 // Because this event causes UI changes, make sure it's launched on the main thread scope. 71 scope.launch { activityStoppedFromDialogEvent.emit(Unit) } 72 } 73 74 /** 75 * Creates a flow that will forcibly hide the chip if the user recently stopped the activity 76 * (see [onActivityStoppedFromDialog]). In general, this flow just uses value in [chip]. 77 */ 78 fun createChipFlow(chip: Flow<OngoingActivityChipModel>): StateFlow<OngoingActivityChipModel> { 79 return combine(chip, wasActivityRecentlyStoppedFromDialog) { 80 chipModel, 81 activityRecentlyStopped -> 82 if (activityRecentlyStopped) { 83 // There's a bit of a delay between when the user stops an activity via 84 // SysUI and when the system services notify SysUI that the activity has 85 // indeed stopped. Prevent the chip from showing during this delay by 86 // immediately hiding it without any animation. 87 OngoingActivityChipModel.Inactive(shouldAnimate = false) 88 } else { 89 chipModel 90 } 91 } 92 .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Inactive()) 93 } 94 } 95