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.data.prod 18 19 import android.os.OutcomeReceiver 20 import android.telephony.TelephonyCallback 21 import android.telephony.TelephonyManager 22 import android.telephony.satellite.NtnSignalStrengthCallback 23 import android.telephony.satellite.SatelliteManager 24 import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS 25 import android.telephony.satellite.SatelliteModemStateCallback 26 import android.telephony.satellite.SatelliteProvisionStateCallback 27 import android.telephony.satellite.SatelliteSupportedStateCallback 28 import androidx.annotation.VisibleForTesting 29 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Application 32 import com.android.systemui.dagger.qualifiers.Background 33 import com.android.systemui.log.LogBuffer 34 import com.android.systemui.log.core.LogLevel 35 import com.android.systemui.log.core.MessageInitializer 36 import com.android.systemui.log.core.MessagePrinter 37 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog 38 import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog 39 import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository 40 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS 41 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported 42 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported 43 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported 44 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown 45 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState 46 import com.android.systemui.util.kotlin.getOrNull 47 import com.android.systemui.util.kotlin.pairwise 48 import com.android.systemui.util.time.SystemClock 49 import java.util.Optional 50 import javax.inject.Inject 51 import kotlin.coroutines.resume 52 import kotlinx.coroutines.CoroutineDispatcher 53 import kotlinx.coroutines.CoroutineScope 54 import kotlinx.coroutines.ExperimentalCoroutinesApi 55 import kotlinx.coroutines.asExecutor 56 import kotlinx.coroutines.channels.awaitClose 57 import kotlinx.coroutines.delay 58 import kotlinx.coroutines.flow.Flow 59 import kotlinx.coroutines.flow.MutableStateFlow 60 import kotlinx.coroutines.flow.SharingStarted 61 import kotlinx.coroutines.flow.StateFlow 62 import kotlinx.coroutines.flow.collectLatest 63 import kotlinx.coroutines.flow.distinctUntilChanged 64 import kotlinx.coroutines.flow.flatMapLatest 65 import kotlinx.coroutines.flow.flowOf 66 import kotlinx.coroutines.flow.flowOn 67 import kotlinx.coroutines.flow.map 68 import kotlinx.coroutines.flow.mapNotNull 69 import kotlinx.coroutines.flow.onStart 70 import kotlinx.coroutines.flow.stateIn 71 import kotlinx.coroutines.launch 72 import kotlinx.coroutines.suspendCancellableCoroutine 73 import kotlinx.coroutines.withContext 74 75 /** 76 * A SatelliteManager that has responded that it has satellite support. Use [SatelliteSupport] to 77 * get one 78 */ 79 private typealias SupportedSatelliteManager = SatelliteManager 80 81 /** 82 * "Supported" here means supported by the device. The value of this should be stable during the 83 * process lifetime. 84 * 85 * @VisibleForTesting 86 */ 87 sealed interface SatelliteSupport { 88 /** Not yet fetched */ 89 data object Unknown : SatelliteSupport 90 91 /** 92 * SatelliteManager says that this mode is supported. Note that satellite manager can never be 93 * null now 94 */ 95 data class Supported(val satelliteManager: SupportedSatelliteManager) : SatelliteSupport 96 97 /** 98 * Either we were told that there is no support for this feature, or the manager is null, or 99 * some other exception occurred while querying for support. 100 */ 101 data object NotSupported : SatelliteSupport 102 103 @OptIn(ExperimentalCoroutinesApi::class) 104 companion object { 105 /** 106 * Convenience function to switch to the supported flow. [retrySignal] is a flow that emits 107 * [Unit] whenever the [supported] flow needs to be restarted 108 */ 109 fun <T> Flow<SatelliteSupport>.whenSupported( 110 supported: (SatelliteManager) -> Flow<T>, 111 orElse: Flow<T>, 112 retrySignal: Flow<Unit>, 113 ): Flow<T> = flatMapLatest { satelliteSupport -> 114 when (satelliteSupport) { 115 is Supported -> { 116 retrySignal.flatMapLatest { supported(satelliteSupport.satelliteManager) } 117 } 118 else -> orElse 119 } 120 } 121 } 122 } 123 124 /** 125 * Basically your everyday run-of-the-mill system service listener, with three notable exceptions. 126 * 127 * First, there is an availability bit that we are tracking via [SatelliteManager]. See 128 * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about 129 * this bit is that there is no callback that exists. Therefore we implement a simple polling 130 * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see 131 * [POLLING_INTERVAL_MS]) and see what the current state is. 132 * 133 * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See 134 * [SatelliteSupport] for details on how we track the state. What's worth noting here is that 135 * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental 136 * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method 137 * [SatelliteSupport.whenSupported]. By defining flows as simple functions based on a 138 * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told 139 * us that satellite is supported. Therefore, we don't expect exceptions to be thrown. 140 * 141 * Lastly, this class is designed to wait a full minute of process uptime before making any requests 142 * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that 143 * is still booting up or anything like that. We can tune or remove this behavior in the future if 144 * necessary. 145 */ 146 @SysUISingleton 147 class DeviceBasedSatelliteRepositoryImpl 148 @Inject 149 constructor( 150 satelliteManagerOpt: Optional<SatelliteManager>, 151 telephonyManager: TelephonyManager, 152 @Background private val bgDispatcher: CoroutineDispatcher, 153 @Application private val scope: CoroutineScope, 154 @DeviceBasedSatelliteInputLog private val logBuffer: LogBuffer, 155 @VerboseDeviceBasedSatelliteInputLog private val verboseLogBuffer: LogBuffer, 156 private val systemClock: SystemClock, 157 ) : RealDeviceBasedSatelliteRepository { 158 159 private val satelliteManager: SatelliteManager? 160 161 override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean> 162 163 // Some calls into satellite manager will throw exceptions if it is not supported. 164 // This is never expected to change after boot, but may need to be retried in some cases 165 @get:VisibleForTesting 166 val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown) 167 168 /** 169 * Note that we are given an "unbound" [TelephonyManager] (meaning it was not created with a 170 * specific `subscriptionId`). Therefore this is the radio power state of the 171 * DEFAULT_SUBSCRIPTION_ID subscription. This subscription, I am led to believe, is the one that 172 * would be used for the SatelliteManager subscription. 173 * 174 * By watching power state changes, we can detect if the telephony process crashes. 175 * 176 * See b/337258696 for details 177 */ 178 private val radioPowerState: StateFlow<Int> = <lambda>null179 conflatedCallbackFlow { 180 val cb = 181 object : TelephonyCallback(), TelephonyCallback.RadioPowerStateListener { 182 override fun onRadioPowerStateChanged(powerState: Int) { 183 trySend(powerState) 184 } 185 } 186 187 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), cb) 188 189 awaitClose { telephonyManager.unregisterTelephonyCallback(cb) } 190 } 191 .flowOn(bgDispatcher) 192 .stateIn( 193 scope, 194 SharingStarted.WhileSubscribed(), 195 TelephonyManager.RADIO_POWER_UNAVAILABLE 196 ) 197 198 /** 199 * In the event that a telephony phone process has crashed, we expect to see a radio power state 200 * change from ON to something else. This trigger can be used to re-start a flow via 201 * [whenSupported] 202 * 203 * This flow emits [Unit] when started so that newly-started collectors always run, and only 204 * restart when the state goes from ON -> !ON 205 */ 206 private val telephonyProcessCrashedEvent: Flow<Unit> = 207 radioPowerState 208 .pairwise() newnull209 .mapNotNull { (prev: Int, new: Int) -> 210 if ( 211 prev == TelephonyManager.RADIO_POWER_ON && 212 new != TelephonyManager.RADIO_POWER_ON 213 ) { 214 Unit 215 } else { 216 null 217 } 218 } <lambda>null219 .onStart { emit(Unit) } 220 221 init { 222 satelliteManager = satelliteManagerOpt.getOrNull() 223 224 isSatelliteAllowedForCurrentLocation = MutableStateFlow(false) 225 226 if (satelliteManager != null) { 227 // Outer scope launch allows us to delay until MIN_UPTIME <lambda>null228 scope.launch { 229 // First, check that satellite is supported on this device 230 satelliteSupport.value = checkSatelliteSupportAfterMinUptime(satelliteManager) 231 logBuffer.i( 232 { str1 = satelliteSupport.value.toString() }, 233 { "Checked for system support. support=$str1" }, 234 ) 235 236 // Second, launch a job to poll for service availability based on location 237 scope.launch { pollForAvailabilityBasedOnLocation() } 238 239 // Third, register a listener to let us know if there are changes to support 240 scope.launch { listenForChangesToSatelliteSupport(satelliteManager) } 241 } 242 } else { <lambda>null243 logBuffer.i { "Satellite manager is null" } 244 satelliteSupport.value = NotSupported 245 } 246 } 247 checkSatelliteSupportAfterMinUptimenull248 private suspend fun checkSatelliteSupportAfterMinUptime( 249 sm: SatelliteManager 250 ): SatelliteSupport { 251 val waitTime = ensureMinUptime(systemClock, MIN_UPTIME) 252 if (waitTime > 0) { 253 logBuffer.i({ long1 = waitTime }) { 254 "Waiting $long1 ms before checking for satellite support" 255 } 256 delay(waitTime) 257 } 258 259 return sm.checkSatelliteSupported() 260 } 261 262 /* 263 * As there is no listener available for checking satellite allowed, we must poll the service. 264 * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart 265 * the job, so a flaky connection might cause more frequent checks. 266 */ pollForAvailabilityBasedOnLocationnull267 private suspend fun pollForAvailabilityBasedOnLocation() { 268 satelliteSupport 269 .whenSupported( 270 supported = ::isSatelliteAllowedHasListener, 271 orElse = flowOf(false), 272 retrySignal = telephonyProcessCrashedEvent, 273 ) 274 .collectLatest { hasSubscribers -> 275 if (hasSubscribers) { 276 while (true) { 277 logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" } 278 checkIsSatelliteAllowed() 279 delay(POLLING_INTERVAL_MS) 280 } 281 } 282 } 283 } 284 285 /** 286 * Register a callback with [SatelliteManager] to let us know if there is a change in satellite 287 * support. This job restarts if there is a crash event detected. 288 * 289 * Note that the structure of this method looks similar to [whenSupported], but since we want 290 * this callback registered even when it is [NotSupported], we just mimic the structure here. 291 */ listenForChangesToSatelliteSupportnull292 private suspend fun listenForChangesToSatelliteSupport(sm: SatelliteManager) { 293 telephonyProcessCrashedEvent.collectLatest { 294 satelliteIsSupportedCallback.collect { supported -> 295 if (supported) { 296 satelliteSupport.value = Supported(sm) 297 } else { 298 satelliteSupport.value = NotSupported 299 } 300 } 301 } 302 } 303 304 /** 305 * Callback version of [checkSatelliteSupported]. This flow should be retried on the same 306 * [telephonyProcessCrashedEvent] signal, but does not require a [SupportedSatelliteManager], 307 * since it specifically watches for satellite support. 308 */ 309 private val satelliteIsSupportedCallback: Flow<Boolean> = 310 if (satelliteManager == null) { 311 flowOf(false) 312 } else { <lambda>null313 conflatedCallbackFlow { 314 val callback = SatelliteSupportedStateCallback { supported -> 315 logBuffer.i { 316 "onSatelliteSupportedStateChanged: " + 317 "${if (supported) "supported" else "not supported"}" 318 } 319 trySend(supported) 320 } 321 322 var registered = false 323 try { 324 satelliteManager.registerForSupportedStateChanged( 325 bgDispatcher.asExecutor(), 326 callback 327 ) 328 registered = true 329 } catch (e: Exception) { 330 logBuffer.e("error registering for supported state change", e) 331 } 332 333 awaitClose { 334 if (registered) { 335 satelliteManager.unregisterForSupportedStateChanged(callback) 336 } 337 } 338 } 339 } 340 341 override val isSatelliteProvisioned: StateFlow<Boolean> = 342 satelliteSupport 343 .whenSupported( 344 supported = ::satelliteProvisioned, 345 orElse = flowOf(false), 346 retrySignal = telephonyProcessCrashedEvent, 347 ) 348 .stateIn(scope, SharingStarted.Eagerly, false) 349 satelliteProvisionednull350 private fun satelliteProvisioned(sm: SupportedSatelliteManager): Flow<Boolean> = 351 conflatedCallbackFlow { 352 // TODO(b/347992038): SatelliteManager should be sending the current provisioned 353 // status when we register a callback. Until then, we have to manually query here. 354 355 // First, check to see what the current status is, and send the result to the output 356 trySend(queryIsSatelliteProvisioned(sm)) 357 358 val callback = SatelliteProvisionStateCallback { provisioned -> 359 logBuffer.i { 360 "onSatelliteProvisionStateChanged: " + 361 if (provisioned) "provisioned" else "not provisioned" 362 } 363 trySend(provisioned) 364 } 365 366 var registered = false 367 try { 368 logBuffer.i { "registerForProvisionStateChanged" } 369 sm.registerForProvisionStateChanged( 370 bgDispatcher.asExecutor(), 371 callback, 372 ) 373 registered = true 374 } catch (e: Exception) { 375 logBuffer.e("error registering for provisioning state callback", e) 376 } 377 378 awaitClose { 379 if (registered) { 380 sm.unregisterForProvisionStateChanged(callback) 381 } 382 } 383 } 384 .flowOn(bgDispatcher) 385 386 /** Check the current satellite provisioning status. */ queryIsSatelliteProvisionednull387 private suspend fun queryIsSatelliteProvisioned(sm: SupportedSatelliteManager): Boolean = 388 withContext(bgDispatcher) { 389 suspendCancellableCoroutine { continuation -> 390 val receiver = 391 object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { 392 override fun onResult(result: Boolean) { 393 logBuffer.i { "requestIsProvisioned.onResult: $result" } 394 continuation.resume(result) 395 } 396 397 override fun onError(exception: SatelliteManager.SatelliteException) { 398 logBuffer.e("requestIsProvisioned.onError:", exception) 399 continuation.resume(false) 400 } 401 } 402 403 logBuffer.i { "Query for current satellite provisioned state." } 404 try { 405 sm.requestIsProvisioned(bgDispatcher.asExecutor(), receiver) 406 } catch (e: Exception) { 407 logBuffer.e("Exception while calling SatelliteManager.requestIsProvisioned:", e) 408 continuation.resume(false) 409 } 410 } 411 } 412 413 /** 414 * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there 415 * are active listeners to [isSatelliteAllowedForCurrentLocation] 416 */ 417 @SuppressWarnings("unused") isSatelliteAllowedHasListenernull418 private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> = 419 isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged() 420 421 override val connectionState = 422 satelliteSupport 423 .whenSupported( 424 supported = ::connectionStateFlow, 425 orElse = flowOf(SatelliteConnectionState.Off), 426 retrySignal = telephonyProcessCrashedEvent, 427 ) 428 .stateIn(scope, SharingStarted.Eagerly, SatelliteConnectionState.Off) 429 430 // By using the SupportedSatelliteManager here, we expect registration never to fail connectionStateFlownull431 private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> = 432 conflatedCallbackFlow { 433 val cb = SatelliteModemStateCallback { state -> 434 logBuffer.i({ int1 = state }) { "onSatelliteModemStateChanged: state=$int1" } 435 trySend(SatelliteConnectionState.fromModemState(state)) 436 } 437 438 var registered = false 439 440 try { 441 val res = sm.registerForModemStateChanged(bgDispatcher.asExecutor(), cb) 442 registered = res == SATELLITE_RESULT_SUCCESS 443 } catch (e: Exception) { 444 logBuffer.e("error registering for modem state", e) 445 } 446 447 awaitClose { if (registered) sm.unregisterForModemStateChanged(cb) } 448 } 449 .flowOn(bgDispatcher) 450 451 override val signalStrength = 452 satelliteSupport 453 .whenSupported( 454 supported = ::signalStrengthFlow, 455 orElse = flowOf(0), 456 retrySignal = telephonyProcessCrashedEvent, 457 ) 458 .stateIn(scope, SharingStarted.Eagerly, 0) 459 460 // By using the SupportedSatelliteManager here, we expect registration never to fail signalStrengthFlownull461 private fun signalStrengthFlow(sm: SupportedSatelliteManager) = 462 conflatedCallbackFlow { 463 val cb = NtnSignalStrengthCallback { signalStrength -> 464 verboseLogBuffer.i({ int1 = signalStrength.level }) { 465 "onNtnSignalStrengthChanged: level=$int1" 466 } 467 trySend(signalStrength.level) 468 } 469 470 var registered = false 471 try { 472 sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) 473 registered = true 474 logBuffer.i { "Registered for signal strength successfully" } 475 } catch (e: Exception) { 476 logBuffer.e("error registering for signal strength", e) 477 } 478 479 awaitClose { 480 if (registered) { 481 sm.unregisterForNtnSignalStrengthChanged(cb) 482 logBuffer.i { "Unregistered for signal strength successfully" } 483 } 484 } 485 } 486 .flowOn(bgDispatcher) 487 488 /** Fire off a request to check for satellite availability. Always runs on the bg context */ checkIsSatelliteAllowednull489 private suspend fun checkIsSatelliteAllowed() = 490 withContext(bgDispatcher) { 491 satelliteManager?.requestIsCommunicationAllowedForCurrentLocation( 492 bgDispatcher.asExecutor(), 493 object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { 494 override fun onError(e: SatelliteManager.SatelliteException) { 495 logBuffer.e( 496 "Found exception when checking availability", 497 e, 498 ) 499 isSatelliteAllowedForCurrentLocation.value = false 500 } 501 502 override fun onResult(allowed: Boolean) { 503 logBuffer.i { "isSatelliteAllowedForCurrentLocation: $allowed" } 504 isSatelliteAllowedForCurrentLocation.value = allowed 505 } 506 } 507 ) 508 } 509 checkSatelliteSupportednull510 private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport = 511 suspendCancellableCoroutine { continuation -> 512 val cb = 513 object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { 514 override fun onResult(supported: Boolean) { 515 continuation.resume( 516 if (supported) { 517 Supported(satelliteManager = this@checkSatelliteSupported) 518 } else { 519 NotSupported 520 } 521 ) 522 } 523 524 override fun onError(error: SatelliteManager.SatelliteException) { 525 logBuffer.e( 526 "Exception when checking for satellite support. " + 527 "Assuming it is not supported for this device.", 528 error, 529 ) 530 531 // Assume that an error means it's not supported 532 continuation.resume(NotSupported) 533 } 534 } 535 536 try { 537 requestIsSupported(bgDispatcher.asExecutor(), cb) 538 } catch (error: Exception) { 539 logBuffer.e( 540 "Exception when checking for satellite support. " + 541 "Assuming it is not supported for this device.", 542 error, 543 ) 544 continuation.resume(NotSupported) 545 } 546 } 547 548 companion object { 549 // TTL for satellite polling is twenty minutes 550 const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 20 551 552 // Let the system boot up and stabilize before we check for system support 553 const val MIN_UPTIME: Long = 1000 * 60 554 555 private const val TAG = "DeviceBasedSatelliteRepo" 556 557 /** Calculates how long we have to wait to reach MIN_UPTIME */ ensureMinUptimenull558 private fun ensureMinUptime(clock: SystemClock, uptime: Long): Long = 559 uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis()) 560 561 /** A couple of convenience logging methods rather than a whole class */ 562 private fun LogBuffer.i( 563 initializer: MessageInitializer = {}, 564 printer: MessagePrinter, 565 ) = this.log(TAG, LogLevel.INFO, initializer, printer) 566 LogBuffernull567 private fun LogBuffer.e(message: String, exception: Throwable? = null) = 568 this.log( 569 tag = TAG, 570 level = LogLevel.ERROR, 571 message = message, 572 exception = exception, 573 ) 574 } 575 } 576