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 }