1 /* 2 * Copyright (C) 2020 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.connectivity; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.content.pm.PackageManager; 24 import android.net.wifi.WifiManager; 25 import android.net.wifi.WifiManager.SubsystemRestartTrackingCallback; 26 import android.os.Handler; 27 import android.os.HandlerExecutor; 28 import android.provider.Settings; 29 import android.telephony.TelephonyCallback; 30 import android.telephony.TelephonyManager; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.VisibleForTesting; 35 36 /** 37 * An interface class to manage connectivity subsystem recovery/restart operations. 38 */ 39 public class ConnectivitySubsystemsRecoveryManager { 40 private static final String TAG = "ConnectivitySubsystemsRecoveryManager"; 41 42 private final Context mContext; 43 private final Handler mHandler; 44 private RecoveryAvailableListener mRecoveryAvailableListener = null; 45 46 private static final long RESTART_TIMEOUT_MS = 15_000; // 15 seconds 47 48 private WifiManager mWifiManager = null; 49 private TelephonyManager mTelephonyManager = null; 50 private final BroadcastReceiver mApmMonitor = new BroadcastReceiver() { 51 @Override 52 public void onReceive(Context context, Intent intent) { 53 RecoveryAvailableListener listener = mRecoveryAvailableListener; 54 if (listener != null) { 55 listener.onRecoveryAvailableChangeListener(isRecoveryAvailable()); 56 } 57 } 58 }; 59 private boolean mApmMonitorRegistered = false; 60 private boolean mWifiRestartInProgress = false; 61 private boolean mTelephonyRestartInProgress = false; 62 private RecoveryStatusCallback mCurrentRecoveryCallback = null; 63 private final SubsystemRestartTrackingCallback mWifiSubsystemRestartTrackingCallback = 64 new SubsystemRestartTrackingCallback() { 65 @Override 66 public void onSubsystemRestarting() { 67 // going to do nothing on this - already assuming that subsystem is restarting 68 } 69 70 @Override 71 public void onSubsystemRestarted() { 72 mWifiRestartInProgress = false; 73 stopTrackingWifiRestart(); 74 checkIfAllSubsystemsRestartsAreDone(); 75 } 76 }; 77 private final MobileTelephonyCallback mTelephonyCallback = new MobileTelephonyCallback(); 78 79 private class MobileTelephonyCallback extends TelephonyCallback implements 80 TelephonyCallback.RadioPowerStateListener { 81 @Override onRadioPowerStateChanged(int state)82 public void onRadioPowerStateChanged(int state) { 83 if (!mTelephonyRestartInProgress || mCurrentRecoveryCallback == null) { 84 stopTrackingTelephonyRestart(); 85 } 86 87 if (state == TelephonyManager.RADIO_POWER_ON) { 88 mTelephonyRestartInProgress = false; 89 stopTrackingTelephonyRestart(); 90 checkIfAllSubsystemsRestartsAreDone(); 91 } 92 } 93 } 94 ConnectivitySubsystemsRecoveryManager(@onNull Context context, @NonNull Handler handler)95 public ConnectivitySubsystemsRecoveryManager(@NonNull Context context, 96 @NonNull Handler handler) { 97 mContext = context; 98 mHandler = new Handler(handler.getLooper()); 99 100 if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) { 101 mWifiManager = mContext.getSystemService(WifiManager.class); 102 if (mWifiManager == null) { 103 Log.e(TAG, "WifiManager not available!?"); 104 } 105 } 106 107 if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { 108 mTelephonyManager = mContext.getSystemService(TelephonyManager.class); 109 if (mTelephonyManager == null) { 110 Log.e(TAG, "TelephonyManager not available!?"); 111 } 112 } 113 } 114 115 /** 116 * A listener which indicates to the caller whether a recovery operation is available across 117 * the specified technologies. 118 * 119 * Set using {@link #setRecoveryAvailableListener(RecoveryAvailableListener)}, cleared 120 * using {@link #clearRecoveryAvailableListener()}. 121 */ 122 public interface RecoveryAvailableListener { 123 /** 124 * Called whenever the recovery availability status changes. 125 * 126 * @param isAvailable True if recovery is available across ANY of the requested 127 * technologies, false if recovery is not available across ALL of the 128 * requested technologies. 129 */ onRecoveryAvailableChangeListener(boolean isAvailable)130 void onRecoveryAvailableChangeListener(boolean isAvailable); 131 } 132 133 /** 134 * Set a {@link RecoveryAvailableListener} to listen to changes in the recovery availability 135 * operation for the specified technology(ies). 136 * 137 * @param listener Listener to be triggered 138 */ setRecoveryAvailableListener(@onNull RecoveryAvailableListener listener)139 public void setRecoveryAvailableListener(@NonNull RecoveryAvailableListener listener) { 140 mHandler.post(() -> { 141 mRecoveryAvailableListener = listener; 142 startTrackingRecoveryAvailability(); 143 }); 144 } 145 146 /** 147 * Clear a listener set with 148 * {@link #setRecoveryAvailableListener(RecoveryAvailableListener)}. 149 */ clearRecoveryAvailableListener()150 public void clearRecoveryAvailableListener() { 151 mHandler.post(() -> { 152 mRecoveryAvailableListener = null; 153 stopTrackingRecoveryAvailability(); 154 }); 155 } 156 isApmEnabled()157 private boolean isApmEnabled() { 158 return Settings.Global.getInt(mContext.getContentResolver(), 159 Settings.Global.AIRPLANE_MODE_ON, 0) == 1; 160 } 161 isWifiEnabled()162 private boolean isWifiEnabled() { 163 // TODO: this doesn't consider the scan-only mode. I.e. WiFi is "disabled" while location 164 // mode is enabled. Probably need to reset WiFi in that state as well. Though this may 165 // appear strange to the user in that they've actually disabled WiFi. 166 return mWifiManager != null && (mWifiManager.isWifiEnabled() 167 || mWifiManager.isWifiApEnabled()); 168 } 169 170 /** 171 * Provide an indication as to whether subsystem recovery is "available" - i.e. will be 172 * executed if triggered via {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)}. 173 * 174 * @return true if a subsystem recovery is available, false otherwise. 175 */ isRecoveryAvailable()176 public boolean isRecoveryAvailable() { 177 if (!isApmEnabled()) return true; 178 179 // even if APM is enabled we may still have recovery potential if WiFi is enabled 180 return isWifiEnabled(); 181 } 182 startTrackingRecoveryAvailability()183 private void startTrackingRecoveryAvailability() { 184 if (mApmMonitorRegistered) return; 185 186 mContext.registerReceiver(mApmMonitor, 187 new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED), null, mHandler); 188 mApmMonitorRegistered = true; 189 } 190 stopTrackingRecoveryAvailability()191 private void stopTrackingRecoveryAvailability() { 192 if (!mApmMonitorRegistered) return; 193 194 mContext.unregisterReceiver(mApmMonitor); 195 mApmMonitorRegistered = false; 196 } 197 198 @VisibleForTesting startTrackingWifiRestart()199 void startTrackingWifiRestart() { 200 if (mWifiManager == null) return; 201 mWifiManager.registerSubsystemRestartTrackingCallback(new HandlerExecutor(mHandler), 202 mWifiSubsystemRestartTrackingCallback); 203 } 204 205 @VisibleForTesting stopTrackingWifiRestart()206 void stopTrackingWifiRestart() { 207 if (mWifiManager == null) return; 208 mWifiManager.unregisterSubsystemRestartTrackingCallback( 209 mWifiSubsystemRestartTrackingCallback); 210 } 211 212 @VisibleForTesting startTrackingTelephonyRestart()213 void startTrackingTelephonyRestart() { 214 if (mTelephonyManager == null) return; 215 mTelephonyManager.registerTelephonyCallback(new HandlerExecutor(mHandler), 216 mTelephonyCallback); 217 } 218 219 @VisibleForTesting stopTrackingTelephonyRestart()220 void stopTrackingTelephonyRestart() { 221 if (mTelephonyManager == null) return; 222 mTelephonyManager.unregisterTelephonyCallback(mTelephonyCallback); 223 } 224 checkIfAllSubsystemsRestartsAreDone()225 private void checkIfAllSubsystemsRestartsAreDone() { 226 if (!mWifiRestartInProgress && !mTelephonyRestartInProgress 227 && mCurrentRecoveryCallback != null) { 228 mCurrentRecoveryCallback.onSubsystemRestartOperationEnd(); 229 mCurrentRecoveryCallback = null; 230 } 231 } 232 233 /** 234 * Callbacks used with 235 * {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} to get 236 * information about when recovery starts and is completed. 237 */ 238 public interface RecoveryStatusCallback { 239 /** 240 * Callback for a subsystem restart triggered via 241 * {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} - indicates 242 * that operation has started. 243 */ onSubsystemRestartOperationBegin()244 void onSubsystemRestartOperationBegin(); 245 246 /** 247 * Callback for a subsystem restart triggered via 248 * {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} - indicates 249 * that operation has ended. Note that subsystems may still take some time to come up to 250 * full functionality. 251 */ onSubsystemRestartOperationEnd()252 void onSubsystemRestartOperationEnd(); 253 } 254 255 /** 256 * Trigger connectivity recovery for all requested technologies. 257 * 258 * @param reason An optional reason code to pass through to the technology-specific 259 * API. May be used to trigger a bug report. 260 * @param callback Callbacks triggered when recovery status changes. 261 */ triggerSubsystemRestart(String reason, @NonNull RecoveryStatusCallback callback)262 public void triggerSubsystemRestart(String reason, @NonNull RecoveryStatusCallback callback) { 263 // TODO: b/183530649 : clean-up or make use of the `reason` argument 264 mHandler.post(() -> { 265 boolean someSubsystemRestarted = false; 266 267 if (mWifiRestartInProgress) { 268 Log.e(TAG, "Wifi restart still in progress"); 269 return; 270 } 271 272 if (mTelephonyRestartInProgress) { 273 Log.e(TAG, "Telephony restart still in progress"); 274 return; 275 } 276 277 if (isWifiEnabled()) { 278 mWifiManager.restartWifiSubsystem(); 279 mWifiRestartInProgress = true; 280 someSubsystemRestarted = true; 281 startTrackingWifiRestart(); 282 } 283 284 if (mTelephonyManager != null && !isApmEnabled()) { 285 if (mTelephonyManager.rebootRadio()) { 286 mTelephonyRestartInProgress = true; 287 someSubsystemRestarted = true; 288 startTrackingTelephonyRestart(); 289 } 290 } 291 292 if (someSubsystemRestarted) { 293 mCurrentRecoveryCallback = callback; 294 callback.onSubsystemRestartOperationBegin(); 295 296 mHandler.postDelayed(() -> { 297 stopTrackingWifiRestart(); 298 stopTrackingTelephonyRestart(); 299 mWifiRestartInProgress = false; 300 mTelephonyRestartInProgress = false; 301 if (mCurrentRecoveryCallback != null) { 302 mCurrentRecoveryCallback.onSubsystemRestartOperationEnd(); 303 mCurrentRecoveryCallback = null; 304 } 305 }, RESTART_TIMEOUT_MS); 306 } 307 }); 308 } 309 } 310 311