1 /*
<lambda>null2  * Copyright (C) 2023 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.shade.ui.viewmodel
18 
19 import android.content.Context
20 import android.content.Intent
21 import android.content.IntentFilter
22 import android.icu.text.DateFormat
23 import android.icu.text.DisplayContext
24 import android.os.UserHandle
25 import android.provider.Settings
26 import com.android.systemui.broadcast.BroadcastDispatcher
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Application
29 import com.android.systemui.plugins.ActivityStarter
30 import com.android.systemui.privacy.OngoingPrivacyChip
31 import com.android.systemui.privacy.PrivacyItem
32 import com.android.systemui.res.R
33 import com.android.systemui.scene.domain.interactor.SceneInteractor
34 import com.android.systemui.scene.shared.model.SceneFamilies
35 import com.android.systemui.scene.shared.model.TransitionKeys
36 import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
37 import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
38 import com.android.systemui.shade.domain.interactor.ShadeInteractor
39 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
40 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
41 import java.util.Date
42 import java.util.Locale
43 import javax.inject.Inject
44 import kotlinx.coroutines.CoroutineScope
45 import kotlinx.coroutines.flow.MutableStateFlow
46 import kotlinx.coroutines.flow.SharingStarted
47 import kotlinx.coroutines.flow.StateFlow
48 import kotlinx.coroutines.flow.asStateFlow
49 import kotlinx.coroutines.flow.launchIn
50 import kotlinx.coroutines.flow.map
51 import kotlinx.coroutines.flow.onEach
52 import kotlinx.coroutines.flow.stateIn
53 import kotlinx.coroutines.launch
54 
55 /** Models UI state for the shade header. */
56 @SysUISingleton
57 class ShadeHeaderViewModel
58 @Inject
59 constructor(
60     @Application private val applicationScope: CoroutineScope,
61     context: Context,
62     private val activityStarter: ActivityStarter,
63     private val sceneInteractor: SceneInteractor,
64     shadeInteractor: ShadeInteractor,
65     mobileIconsInteractor: MobileIconsInteractor,
66     val mobileIconsViewModel: MobileIconsViewModel,
67     private val privacyChipInteractor: PrivacyChipInteractor,
68     private val clockInteractor: ShadeHeaderClockInteractor,
69     broadcastDispatcher: BroadcastDispatcher,
70 ) {
71     /** True if there is exactly one mobile connection. */
72     val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
73 
74     /** The list of subscription Ids for current mobile connections. */
75     val mobileSubIds =
76         mobileIconsInteractor.filteredSubscriptions
77             .map { list -> list.map { it.subscriptionId } }
78             .stateIn(applicationScope, SharingStarted.WhileSubscribed(), emptyList())
79 
80     /** The list of PrivacyItems to be displayed by the privacy chip. */
81     val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems
82 
83     /** Whether or not mic & camera indicators are enabled in the device privacy config. */
84     val isMicCameraIndicationEnabled: StateFlow<Boolean> =
85         privacyChipInteractor.isMicCameraIndicationEnabled
86 
87     /** Whether or not location indicators are enabled in the device privacy config. */
88     val isLocationIndicationEnabled: StateFlow<Boolean> =
89         privacyChipInteractor.isLocationIndicationEnabled
90 
91     /** Whether or not the privacy chip should be visible. */
92     val isPrivacyChipVisible: StateFlow<Boolean> = privacyChipInteractor.isChipVisible
93 
94     /** Whether or not the privacy chip is enabled in the device privacy config. */
95     val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled
96 
97     /** Whether or not the Shade Header should be disabled based on disableFlags. */
98     val isDisabled: StateFlow<Boolean> =
99         shadeInteractor.isQsEnabled
100             .map { !it }
101             .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
102 
103     private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm)
104     private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year)
105     private val longerDateFormat = MutableStateFlow(getFormatFromPattern(longerPattern))
106     private val shorterDateFormat = MutableStateFlow(getFormatFromPattern(shorterPattern))
107 
108     private val _shorterDateText: MutableStateFlow<String> = MutableStateFlow("")
109     val shorterDateText: StateFlow<String> = _shorterDateText.asStateFlow()
110 
111     private val _longerDateText: MutableStateFlow<String> = MutableStateFlow("")
112     val longerDateText: StateFlow<String> = _longerDateText.asStateFlow()
113 
114     init {
115         broadcastDispatcher
116             .broadcastFlow(
117                 filter =
118                     IntentFilter().apply {
119                         addAction(Intent.ACTION_TIME_TICK)
120                         addAction(Intent.ACTION_TIME_CHANGED)
121                         addAction(Intent.ACTION_TIMEZONE_CHANGED)
122                         addAction(Intent.ACTION_LOCALE_CHANGED)
123                     },
124                 user = UserHandle.SYSTEM,
125                 map = { intent, _ ->
126                     intent.action == Intent.ACTION_TIMEZONE_CHANGED ||
127                         intent.action == Intent.ACTION_LOCALE_CHANGED
128                 }
129             )
130             .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) }
131             .launchIn(applicationScope)
132 
133         applicationScope.launch { updateDateTexts(false) }
134     }
135 
136     /** Notifies that the privacy chip was clicked. */
137     fun onPrivacyChipClicked(privacyChip: OngoingPrivacyChip) {
138         privacyChipInteractor.onPrivacyChipClicked(privacyChip)
139     }
140 
141     /** Notifies that the clock was clicked. */
142     fun onClockClicked() {
143         clockInteractor.launchClockActivity()
144     }
145 
146     /** Notifies that the system icons container was clicked. */
147     fun onSystemIconContainerClicked() {
148         sceneInteractor.changeScene(
149             SceneFamilies.Home,
150             "ShadeHeaderViewModel.onSystemIconContainerClicked",
151             TransitionKeys.SlightlyFasterShadeCollapse,
152         )
153     }
154 
155     /** Notifies that the shadeCarrierGroup was clicked. */
156     fun onShadeCarrierGroupClicked() {
157         activityStarter.postStartActivityDismissingKeyguard(
158             Intent(Settings.ACTION_WIRELESS_SETTINGS),
159             0
160         )
161     }
162 
163     private fun updateDateTexts(invalidateFormats: Boolean) {
164         if (invalidateFormats) {
165             longerDateFormat.value = getFormatFromPattern(longerPattern)
166             shorterDateFormat.value = getFormatFromPattern(shorterPattern)
167         }
168 
169         val currentTime = Date()
170 
171         _longerDateText.value = longerDateFormat.value.format(currentTime)
172         _shorterDateText.value = shorterDateFormat.value.format(currentTime)
173     }
174 
175     private fun getFormatFromPattern(pattern: String?): DateFormat {
176         val l = Locale.getDefault()
177         val format = DateFormat.getInstanceForSkeleton(pattern, l)
178         // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of
179         // CAPITALIZATION_FOR_STANDALONE is to address
180         // https://unicode-org.atlassian.net/browse/ICU-21631
181         // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE
182         format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE)
183         return format
184     }
185 }
186