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.statusbar.pipeline.satellite.domain.interactor
18
19 import com.android.systemui.dagger.SysUISingleton
20 import com.android.systemui.dagger.qualifiers.Background
21 import com.android.systemui.log.LogBuffer
22 import com.android.systemui.log.core.LogLevel
23 import com.android.systemui.log.table.TableLogBuffer
24 import com.android.systemui.log.table.logDiffsForTable
25 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
26 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteTableLog
27 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
28 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
29 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
30 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
31 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
32 import javax.inject.Inject
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.flow.Flow
35 import kotlinx.coroutines.flow.SharingStarted
36 import kotlinx.coroutines.flow.combine
37 import kotlinx.coroutines.flow.distinctUntilChanged
38 import kotlinx.coroutines.flow.flatMapLatest
39 import kotlinx.coroutines.flow.flowOf
40 import kotlinx.coroutines.flow.map
41 import kotlinx.coroutines.flow.stateIn
42
43 @SysUISingleton
44 class DeviceBasedSatelliteInteractor
45 @Inject
46 constructor(
47 val repo: DeviceBasedSatelliteRepository,
48 iconsInteractor: MobileIconsInteractor,
49 wifiInteractor: WifiInteractor,
50 @Background scope: CoroutineScope,
51 @DeviceBasedSatelliteInputLog private val logBuffer: LogBuffer,
52 @DeviceBasedSatelliteTableLog private val tableLog: TableLogBuffer,
53 ) {
54 /** Whether or not we should show the satellite icon when all connections are OOS */
55 val isOpportunisticSatelliteIconEnabled = repo.isOpportunisticSatelliteIconEnabled
56
57 /** Must be observed by any UI showing Satellite iconography */
58 val isSatelliteAllowed =
59 repo.isSatelliteAllowedForCurrentLocation
60 .logDiffsForTable(tableLog, columnName = COL_ALLOWED, initialValue = false)
61 .stateIn(scope, SharingStarted.WhileSubscribed(), false)
62
63 /** See [SatelliteConnectionState] for relevant states */
64 val connectionState =
65 repo.connectionState
66 .logDiffsForTable(tableLog, initialValue = SatelliteConnectionState.Off)
67 .stateIn(scope, SharingStarted.WhileSubscribed(), SatelliteConnectionState.Off)
68
69 /** 0-4 description of the connection strength */
70 val signalStrength =
71 repo.signalStrength
72 .logDiffsForTable(tableLog, columnName = COL_LEVEL, initialValue = 0)
73 .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
74
75 val isSatelliteProvisioned = repo.isSatelliteProvisioned
76
77 val isWifiActive: Flow<Boolean> =
78 wifiInteractor.wifiNetwork.map { it is WifiNetworkModel.Active }
79
80 private val allConnectionsOos =
81 iconsInteractor.icons
82 .aggregateOver(
83 selector = { intr ->
84 combine(intr.isInService, intr.isEmergencyOnly, intr.isNonTerrestrial) {
85 isInService,
86 isEmergencyOnly,
87 isNtn ->
88 !isInService && !isEmergencyOnly && !isNtn
89 }
90 },
91 defaultValue = true, // no connections == everything is OOS
92 ) { isOosAndNotEmergencyAndNotSatellite ->
93 isOosAndNotEmergencyAndNotSatellite.all { it }
94 }
95 .distinctUntilChanged()
96 .logDiffsForTable(tableLog, columnName = COL_ALL_OOS, initialValue = true)
97
98 /** When all connections are considered OOS, satellite connectivity is potentially valid */
99 val areAllConnectionsOutOfService =
100 combine(allConnectionsOos, iconsInteractor.isDeviceInEmergencyCallsOnlyMode) {
101 connectionsOos,
102 deviceEmergencyOnly ->
103 logBuffer.log(
104 TAG,
105 LogLevel.INFO,
106 {
107 bool1 = connectionsOos
108 bool2 = deviceEmergencyOnly
109 },
110 {
111 "Updating OOS status. allConnectionsOOs=$bool1 " +
112 "deviceEmergencyOnly=$bool2"
113 },
114 )
115 // If no connections exist, or all are OOS, then we look to the device-based
116 // service state to detect if any calls are possible
117 connectionsOos && !deviceEmergencyOnly
118 }
119 .distinctUntilChanged()
120 .logDiffsForTable(tableLog, columnName = COL_FULL_OOS, initialValue = true)
121 .stateIn(scope, SharingStarted.WhileSubscribed(), true)
122
123 /** True if any known mobile network is currently using a non terrestrial network */
124 val isAnyConnectionNtn =
125 iconsInteractor.icons.aggregateOver(selector = { it.isNonTerrestrial }, false) {
126 nonTerrestrialNetworks ->
127 nonTerrestrialNetworks.any { it == true }
128 }
129
130 companion object {
131 const val TAG = "DeviceBasedSatelliteInteractor"
132
133 const val COL_LEVEL = "level"
134 const val COL_ALL_OOS = "allConnsOOS"
135 const val COL_ALLOWED = "allowed"
136 // Going to try to optimize for not using too much width on the table here. This information
137 // can be ascertained by checking for the device emergency only in the mobile logs as well
138 const val COL_FULL_OOS = "allOosAndNoEmer"
139 }
140 }
141
142 /**
143 * aggregateOver allows us to combine over the leaf-nodes of successive lists emitted from the
144 * top-level flow. Re-emits if the list changes, or any of the intermediate values change.
145 *
146 * Provides a way to connect the reactivity of the top-level flow with the reactivity of an
147 * arbitrarily-defined relationship ([selector]) from R to the flow that R exposes.
148 *
149 * [defaultValue] allows for a default value to be used if there are no leaf nodes after applying
150 * [selector]. E.g., if there are no mobile connections, assume that there is no service.
151 */
aggregateOvernull152 private inline fun <R, reified S, T> Flow<List<R>>.aggregateOver(
153 crossinline selector: (R) -> Flow<S>,
154 defaultValue: T,
155 crossinline transform: (Array<S>) -> T,
156 ): Flow<T> {
157 return map { list -> list.map { selector(it) } }
158 .flatMapLatest { newFlows ->
159 if (newFlows.isEmpty()) {
160 flowOf(defaultValue)
161 } else {
162 combine(newFlows) { newVals -> transform(newVals) }
163 }
164 }
165 }
166