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