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