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