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