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.settingslib.satellite
18 
19 import android.content.ComponentName
20 import android.content.Context
21 import android.content.Intent
22 import android.os.OutcomeReceiver
23 import android.telephony.satellite.SatelliteManager
24 import android.telephony.satellite.SatelliteModemStateCallback
25 import android.util.Log
26 import android.view.WindowManager
27 import androidx.lifecycle.LifecycleOwner
28 import androidx.lifecycle.lifecycleScope
29 import com.android.settingslib.wifi.WifiUtils
30 import kotlinx.coroutines.CoroutineScope
31 import kotlinx.coroutines.Dispatchers
32 import kotlinx.coroutines.Dispatchers.Default
33 import kotlinx.coroutines.Job
34 import kotlinx.coroutines.asExecutor
35 import kotlinx.coroutines.flow.Flow
36 import kotlinx.coroutines.flow.callbackFlow
37 import kotlinx.coroutines.flow.flowOf
38 import kotlinx.coroutines.launch
39 import kotlinx.coroutines.suspendCancellableCoroutine
40 import kotlinx.coroutines.withContext
41 import java.util.concurrent.ExecutionException
42 import java.util.concurrent.TimeoutException
43 import kotlin.coroutines.resume
44 import kotlinx.coroutines.channels.awaitClose
45 import kotlinx.coroutines.flow.conflate
46 import kotlinx.coroutines.flow.first
47 import kotlinx.coroutines.flow.flowOn
48 
49 /** A util for Satellite dialog */
50 object SatelliteDialogUtils {
51 
52     /**
53      * Uses to start Satellite dialog to prevent users from using the BT, Airplane Mode, and
54      * Wifi during the satellite mode is on.
55      */
56     @JvmStatic
57     fun mayStartSatelliteWarningDialog(
58             context: Context,
59             lifecycleOwner: LifecycleOwner,
60             type: Int,
61             allowClick: (isAllowed: Boolean) -> Unit
62     ): Job {
63         return mayStartSatelliteWarningDialog(
64                 context, lifecycleOwner.lifecycleScope, type, allowClick)
65     }
66 
67     /**
68      * Uses to start Satellite dialog to prevent users from using the BT, Airplane Mode, and
69      * Wifi during the satellite mode is on.
70      */
71     @JvmStatic
72     fun mayStartSatelliteWarningDialog(
73             context: Context,
74             coroutineScope: CoroutineScope,
75             type: Int,
76             allowClick: (isAllowed: Boolean) -> Unit
77     ): Job =
78             coroutineScope.launch {
79                 var isSatelliteModeOn = false
80                 try {
81                     isSatelliteModeOn = requestIsSessionStarted(context)
82                 } catch (e: InterruptedException) {
83                     Log.w(TAG, "Error to get satellite status : $e")
84                 } catch (e: ExecutionException) {
85                     Log.w(TAG, "Error to get satellite status : $e")
86                 } catch (e: TimeoutException) {
87                     Log.w(TAG, "Error to get satellite status : $e")
88                 }
89 
90                 if (isSatelliteModeOn) {
91                     startSatelliteWarningDialog(context, type)
92                 }
93                 withContext(Dispatchers.Main) {
94                     allowClick(!isSatelliteModeOn)
95                 }
96             }
97 
98     private fun startSatelliteWarningDialog(context: Context, type: Int) {
99         context.startActivity(Intent(Intent.ACTION_MAIN).apply {
100             component = ComponentName(
101                     "com.android.settings",
102                     "com.android.settings.network.SatelliteWarningDialogActivity"
103             )
104             putExtra(WifiUtils.DIALOG_WINDOW_TYPE,
105                     WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG)
106             putExtra(EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG, type)
107             addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
108         })
109     }
110 
111     /**
112      * Checks if the satellite modem is enabled.
113      *
114      * @param executor The executor to run the asynchronous operation on
115      * @return A ListenableFuture that will resolve to `true` if the satellite modem enabled,
116      *         `false` otherwise.
117      */
118     private suspend fun requestIsEnabled(
119             context: Context,
120     ): Boolean = withContext(Default) {
121         val satelliteManager: SatelliteManager? =
122                 context.getSystemService(SatelliteManager::class.java)
123         if (satelliteManager == null) {
124             Log.w(TAG, "SatelliteManager is null")
125             return@withContext false
126         }
127 
128         suspendCancellableCoroutine {continuation ->
129             try {
130                 satelliteManager?.requestIsEnabled(Default.asExecutor(),
131                         object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
132                             override fun onResult(result: Boolean) {
133                                 Log.i(TAG, "Satellite modem enabled status: $result")
134                                 continuation.resume(result)
135                             }
136 
137                             override fun onError(error: SatelliteManager.SatelliteException) {
138                                 super.onError(error)
139                                 Log.w(TAG, "Can't get satellite modem enabled status", error)
140                                 continuation.resume(false)
141                             }
142                         })
143             } catch (e: IllegalStateException) {
144                 Log.w(TAG, "IllegalStateException: $e")
145                 continuation.resume(false)
146             }
147         }
148     }
149 
150     private suspend fun requestIsSessionStarted(
151             context: Context
152     ): Boolean = withContext(Default) {
153         val satelliteManager: SatelliteManager? =
154                 context.getSystemService(SatelliteManager::class.java)
155         if (satelliteManager == null) {
156             Log.w(TAG, "SatelliteManager is null")
157             return@withContext false
158         }
159 
160         getIsSessionStartedFlow(context).conflate().first()
161     }
162 
163     /**
164      * Provides a Flow that emits the session state of the satellite modem. Updates are triggered
165      * when the modem state changes.
166      *
167      * @param defaultDispatcher The CoroutineDispatcher to use (Defaults to `Dispatchers.Default`).
168      * @return A Flow emitting `true` when the session is started and `false` otherwise.
169      */
170     private fun getIsSessionStartedFlow(
171             context: Context
172     ): Flow<Boolean> {
173         val satelliteManager: SatelliteManager? =
174                 context.getSystemService(SatelliteManager::class.java)
175         if (satelliteManager == null) {
176             Log.w(TAG, "SatelliteManager is null")
177             return flowOf(false)
178         }
179 
180         return callbackFlow {
181             val callback = SatelliteModemStateCallback { state ->
182                 val isSessionStarted = isSatelliteSessionStarted(state)
183                 Log.i(TAG, "Satellite modem state changed: state=$state"
184                         + ", isSessionStarted=$isSessionStarted")
185                 trySend(isSessionStarted)
186             }
187 
188             var registerResult = SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
189             try {
190                 registerResult = satelliteManager.registerForModemStateChanged(
191                         Default.asExecutor(),
192                         callback
193                 )
194             } catch (e: IllegalStateException) {
195                 Log.w(TAG, "IllegalStateException: $e")
196             }
197 
198 
199             if (registerResult != SatelliteManager.SATELLITE_RESULT_SUCCESS) {
200                 // If the registration failed (e.g., device doesn't support satellite),
201                 // SatelliteManager will not emit the current state by callback.
202                 // We send `false` value by ourself to make sure the flow has initial value.
203                 Log.w(TAG, "Failed to register for satellite modem state change: $registerResult")
204                 trySend(false)
205             }
206 
207             awaitClose {
208                 try {
209                     satelliteManager.unregisterForModemStateChanged(callback)
210                 } catch (e: IllegalStateException) {
211                     Log.w(TAG, "IllegalStateException: $e")
212                 }
213             }
214         }.flowOn(Default)
215     }
216 
217 
218     /**
219      * Check if the modem is in a satellite session.
220      *
221      * @param state The SatelliteModemState provided by the SatelliteManager.
222      * @return `true` if the modem is in a satellite session, `false` otherwise.
223      */
224     fun isSatelliteSessionStarted(@SatelliteManager.SatelliteModemState state: Int): Boolean {
225         return when (state) {
226             SatelliteManager.SATELLITE_MODEM_STATE_OFF,
227             SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE,
228             SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN -> false
229             else -> true
230         }
231     }
232 
233     const val TAG = "SatelliteDialogUtils"
234 
235     const val EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG: String =
236             "extra_type_of_satellite_warning_dialog"
237     const val TYPE_IS_UNKNOWN = -1
238     const val TYPE_IS_WIFI = 0
239     const val TYPE_IS_BLUETOOTH = 1
240     const val TYPE_IS_AIRPLANE_MODE = 2
241 }