1 /*
<lambda>null2  * Copyright (C) 2024 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.settings.network
18 
19 import android.content.Context
20 import android.os.OutcomeReceiver
21 import android.telephony.satellite.SatelliteManager
22 import android.telephony.satellite.SatelliteModemStateCallback
23 import android.util.Log
24 import androidx.annotation.VisibleForTesting
25 import androidx.concurrent.futures.CallbackToFutureAdapter
26 import com.google.common.util.concurrent.Futures.immediateFuture
27 import com.google.common.util.concurrent.ListenableFuture
28 import kotlinx.coroutines.CoroutineDispatcher
29 import kotlinx.coroutines.Dispatchers
30 import kotlinx.coroutines.asExecutor
31 import kotlinx.coroutines.channels.awaitClose
32 import kotlinx.coroutines.flow.Flow
33 import kotlinx.coroutines.flow.callbackFlow
34 import kotlinx.coroutines.flow.flowOf
35 import java.util.concurrent.Executor
36 import kotlinx.coroutines.flow.flowOn
37 
38 /**
39  * A repository class for interacting with the SatelliteManager API.
40  */
41 class SatelliteRepository(
42     private val context: Context,
43 ) {
44 
45     /**
46      * Checks if the satellite modem is enabled.
47      *
48      * @param executor The executor to run the asynchronous operation on
49      * @return A ListenableFuture that will resolve to `true` if the satellite modem enabled,
50      *         `false` otherwise.
51      */
52     fun requestIsEnabled(executor: Executor): ListenableFuture<Boolean> {
53         val satelliteManager: SatelliteManager? =
54             context.getSystemService(SatelliteManager::class.java)
55         if (satelliteManager == null) {
56             Log.w(TAG, "SatelliteManager is null")
57             return immediateFuture(false)
58         }
59 
60         return CallbackToFutureAdapter.getFuture { completer ->
61             satelliteManager.requestIsEnabled(executor,
62                 object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
63                     override fun onResult(result: Boolean) {
64                         Log.i(TAG, "Satellite modem enabled status: $result")
65                         completer.set(result)
66                     }
67 
68                     override fun onError(error: SatelliteManager.SatelliteException) {
69                         super.onError(error)
70                         Log.w(TAG, "Can't get satellite modem enabled status", error)
71                         completer.set(false)
72                     }
73                 })
74             "requestIsEnabled"
75         }
76     }
77 
78     /**
79      * Checks if a satellite session has started.
80      *
81      * @param executor The executor to run the asynchronous operation on
82      * @return A ListenableFuture that will resolve to `true` if a satellite session has started,
83      *         `false` otherwise.
84      */
85     fun requestIsSessionStarted(executor: Executor): ListenableFuture<Boolean> {
86         isSessionStarted?.let { return immediateFuture(it) }
87 
88         val satelliteManager: SatelliteManager? =
89             context.getSystemService(SatelliteManager::class.java)
90         if (satelliteManager == null) {
91             Log.w(TAG, "SatelliteManager is null")
92             return immediateFuture(false)
93         }
94 
95         return CallbackToFutureAdapter.getFuture { completer ->
96             val callback = object : SatelliteModemStateCallback {
97                 override fun onSatelliteModemStateChanged(state: Int) {
98                     val isSessionStarted = isSatelliteSessionStarted(state)
99                     Log.i(TAG, "Satellite modem state changed: state=$state"
100                             + ", isSessionStarted=$isSessionStarted")
101                     completer.set(isSessionStarted)
102                     satelliteManager.unregisterForModemStateChanged(this)
103                 }
104             }
105 
106             val registerResult = satelliteManager.registerForModemStateChanged(executor, callback)
107             if (registerResult != SatelliteManager.SATELLITE_RESULT_SUCCESS) {
108                 Log.w(TAG, "Failed to register for satellite modem state change: $registerResult")
109                 completer.set(false)
110             }
111             "requestIsSessionStarted"
112         }
113     }
114 
115     /**
116      * Provides a Flow that emits the session state of the satellite modem. Updates are triggered
117      * when the modem state changes.
118      *
119      * @param defaultDispatcher The CoroutineDispatcher to use (Defaults to `Dispatchers.Default`).
120      * @return A Flow emitting `true` when the session is started and `false` otherwise.
121      */
122     fun getIsSessionStartedFlow(
123         defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
124     ): Flow<Boolean> {
125         val satelliteManager: SatelliteManager? =
126             context.getSystemService(SatelliteManager::class.java)
127         if (satelliteManager == null) {
128             Log.w(TAG, "SatelliteManager is null")
129             return flowOf(false)
130         }
131 
132         return callbackFlow {
133             val callback = SatelliteModemStateCallback { state ->
134                 val isSessionStarted = isSatelliteSessionStarted(state)
135                 Log.i(TAG, "Satellite modem state changed: state=$state"
136                     + ", isSessionStarted=$isSessionStarted")
137                 trySend(isSessionStarted)
138             }
139 
140             val registerResult = satelliteManager.registerForModemStateChanged(
141                 defaultDispatcher.asExecutor(),
142                 callback
143             )
144 
145             if (registerResult != SatelliteManager.SATELLITE_RESULT_SUCCESS) {
146                 // If the registration failed (e.g., device doesn't support satellite),
147                 // SatelliteManager will not emit the current state by callback.
148                 // We send `false` value by ourself to make sure the flow has initial value.
149                 Log.w(TAG, "Failed to register for satellite modem state change: $registerResult")
150                 trySend(false)
151             }
152 
153             awaitClose { satelliteManager.unregisterForModemStateChanged(callback) }
154         }.flowOn(Dispatchers.Default)
155     }
156 
157     /**
158      * Check if the modem is in a satellite session.
159      *
160      * @param state The SatelliteModemState provided by the SatelliteManager.
161      * @return `true` if the modem is in a satellite session, `false` otherwise.
162      */
163     fun isSatelliteSessionStarted(@SatelliteManager.SatelliteModemState state: Int): Boolean {
164         return when (state) {
165             SatelliteManager.SATELLITE_MODEM_STATE_OFF,
166             SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE,
167             SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN -> false
168             else -> true
169         }
170     }
171 
172     companion object {
173         private const val TAG: String = "SatelliteRepository"
174 
175         private var isSessionStarted: Boolean? = null
176 
177         @VisibleForTesting
178         fun setIsSessionStartedForTesting(isEnabled: Boolean) {
179             this.isSessionStarted = isEnabled
180         }
181     }
182 }
183