• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.android.internal.telephony.flags.Flags
27 import com.google.common.util.concurrent.Futures.immediateFuture
28 import com.google.common.util.concurrent.ListenableFuture
29 import java.util.concurrent.Executor
30 import java.util.concurrent.Executors
31 import java.util.concurrent.TimeUnit
32 import kotlinx.coroutines.CoroutineDispatcher
33 import kotlinx.coroutines.Dispatchers
34 import kotlinx.coroutines.asExecutor
35 import kotlinx.coroutines.channels.awaitClose
36 import kotlinx.coroutines.flow.Flow
37 import kotlinx.coroutines.flow.callbackFlow
38 import kotlinx.coroutines.flow.flowOf
39 import kotlinx.coroutines.flow.flowOn
40 
41 /**
42  * A repository class for interacting with the SatelliteManager API.
43  */
44 open class SatelliteRepository(
45     private val context: Context,
46 ) {
47 
48     /**
49      * Checks if the satellite modem is enabled.
50      *
51      * @param executor The executor to run the asynchronous operation on
52      * @return A ListenableFuture that will resolve to `true` if the satellite modem enabled,
53      *         `false` otherwise.
54      */
55     fun requestIsEnabled(executor: Executor): ListenableFuture<Boolean> {
56         val satelliteManager: SatelliteManager? =
57             context.getSystemService(SatelliteManager::class.java)
58         if (satelliteManager == null) {
59             Log.w(TAG, "SatelliteManager is null")
60             return immediateFuture(false)
61         }
62 
63         return CallbackToFutureAdapter.getFuture { completer ->
64             try {
65                 satelliteManager.requestIsEnabled(executor,
66                     object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
67                         override fun onResult(result: Boolean) {
68                             Log.i(TAG, "Satellite modem enabled status: $result")
69                             completer.set(result)
70                         }
71 
72                         override fun onError(error: SatelliteManager.SatelliteException) {
73                             super.onError(error)
74                             Log.w(TAG, "Can't get satellite modem enabled status", error)
75                             completer.set(false)
76                         }
77                     })
78                 "requestIsEnabled"
79             } catch (e: IllegalStateException) {
80                 Log.w(TAG, "IllegalStateException $e")
81                 completer.set(false)
82             }
83 
84         }
85     }
86 
87     /**
88      * Checks if a satellite session has started.
89      *
90      * @param executor The executor to run the asynchronous operation on
91      * @return A ListenableFuture that will resolve to `true` if a satellite session has started,
92      *         `false` otherwise.
93      */
94     fun requestIsSessionStarted(executor: Executor): ListenableFuture<Boolean> {
95         isSessionStarted?.let { return immediateFuture(it) }
96 
97         val satelliteManager: SatelliteManager? =
98             context.getSystemService(SatelliteManager::class.java)
99         if (satelliteManager == null) {
100             Log.w(TAG, "SatelliteManager is null")
101             return immediateFuture(false)
102         }
103 
104         return CallbackToFutureAdapter.getFuture { completer ->
105             val callback = object : SatelliteModemStateCallback {
106                 override fun onSatelliteModemStateChanged(state: Int) {
107                     val isSessionStarted = isSatelliteSessionStarted(state)
108                     Log.i(
109                         TAG, "Satellite modem state changed: state=$state"
110                             + ", isSessionStarted=$isSessionStarted"
111                     )
112                     completer.set(isSessionStarted)
113                     satelliteManager.unregisterForModemStateChanged(this)
114                 }
115             }
116 
117             var registerResult = SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
118             try {
119                 registerResult = satelliteManager.registerForModemStateChanged(executor, callback)
120             } catch (e: IllegalStateException) {
121                 Log.w(TAG, "IllegalStateException $e")
122             }
123             if (registerResult != SatelliteManager.SATELLITE_RESULT_SUCCESS) {
124                 Log.w(TAG, "Failed to register for satellite modem state change: $registerResult")
125                 completer.set(false)
126             }
127             "requestIsSessionStarted"
128         }
129     }
130 
131     /**
132      * Provides a Flow that emits the session state of the satellite modem. Updates are triggered
133      * when the modem state changes.
134      *
135      * @param defaultDispatcher The CoroutineDispatcher to use (Defaults to `Dispatchers.Default`).
136      * @return A Flow emitting `true` when the session is started and `false` otherwise.
137      */
138     fun getIsSessionStartedFlow(
139         defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
140     ): Flow<Boolean> {
141         val satelliteManager: SatelliteManager? =
142             context.getSystemService(SatelliteManager::class.java)
143         if (satelliteManager == null) {
144             Log.w(TAG, "SatelliteManager is null")
145             return flowOf(false)
146         }
147 
148         return callbackFlow {
149             val callback = SatelliteModemStateCallback { state ->
150                 val isSessionStarted = isSatelliteSessionStarted(state)
151                 Log.i(
152                     TAG, "Satellite modem state changed: state=$state"
153                         + ", isSessionStarted=$isSessionStarted"
154                 )
155                 trySend(isSessionStarted)
156             }
157             var registerResult: Int = SatelliteManager.SATELLITE_RESULT_ERROR
158             try {
159                 registerResult = satelliteManager.registerForModemStateChanged(
160                     defaultDispatcher.asExecutor(),
161                     callback
162                 )
163             } catch (e: IllegalStateException) {
164                 Log.w(TAG, "IllegalStateException $e")
165             }
166 
167             if (registerResult != SatelliteManager.SATELLITE_RESULT_SUCCESS) {
168                 // If the registration failed (e.g., device doesn't support satellite),
169                 // SatelliteManager will not emit the current state by callback.
170                 // We send `false` value by ourself to make sure the flow has initial value.
171                 Log.w(TAG, "Failed to register for satellite modem state change: $registerResult")
172                 trySend(false)
173             }
174 
175             awaitClose {
176                 try {
177                     satelliteManager.unregisterForModemStateChanged(callback)
178                 } catch (e: IllegalStateException) {
179                     Log.w(TAG, "IllegalStateException $e")
180                 }
181             }
182         }.flowOn(Dispatchers.Default)
183     }
184 
185     /**
186      * Check if the modem is in a satellite session.
187      *
188      * @param state The SatelliteModemState provided by the SatelliteManager.
189      * @return `true` if the modem is in a satellite session, `false` otherwise.
190      */
191     fun isSatelliteSessionStarted(@SatelliteManager.SatelliteModemState state: Int): Boolean {
192         return when (state) {
193             SatelliteManager.SATELLITE_MODEM_STATE_OFF,
194             SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE,
195             SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN -> false
196             else -> true
197         }
198     }
199 
200     /**
201      *  @return A list with application package names which support Satellite service.
202      *  e.g. "com.android.settings"
203      */
204     open fun getSatelliteDataOptimizedApps(): List<String> {
205         val satelliteManager: SatelliteManager? =
206             context.getSystemService(SatelliteManager::class.java)
207         if (satelliteManager == null) {
208             Log.d(TAG, "SatelliteManager is null")
209             return emptyList()
210         }
211         try {
212             return satelliteManager.getSatelliteDataOptimizedApps();
213         } catch (e: IllegalStateException) {
214             Log.w(TAG, "IllegalStateException $e")
215         }
216         return emptyList()
217     }
218 
219     companion object {
220         private const val TAG: String = "SatelliteRepository"
221 
222         private var isSessionStarted: Boolean? = null
223 
224         @VisibleForTesting
225         fun setIsSessionStartedForTesting(isEnabled: Boolean) {
226             this.isSessionStarted = isEnabled
227         }
228 
229         fun isSatelliteOn(context: Context, timeoutMs: Long = 2000): Boolean =
230             try {
231                 SatelliteRepository(context)
232                     .requestIsSessionStarted(Executors.newSingleThreadExecutor())
233                     .get(timeoutMs, TimeUnit.MILLISECONDS)
234             } catch (e: Exception) {
235                 Log.e(TAG, "Error to get satellite status : $e")
236                 false
237             }
238     }
239 }
240