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