From a84603182bbbe99197736976bce1eadf75a6b437 Mon Sep 17 00:00:00 2001 From: Etan Cohen Date: Mon, 7 Dec 2020 21:57:20 -0800 Subject: [PATCH] [SettingsLib] Wrapper for the "fix connectivity" Settings action Provide a shim API for WiFi and Telephony subsystem restart operations. Bug: 175084231 Test: TBD - pending UI test integration Change-Id: Ie016937e0c6768716958b3a647522e7e7a3f6df5 --- ...ConnectivitySubsystemsRecoveryManager.java | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java diff --git a/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java b/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java new file mode 100644 index 0000000000000..a92105320a62c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/connectivity/ConnectivitySubsystemsRecoveryManager.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.connectivity; + +import android.annotation.NonNull; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.provider.Settings; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +/** + * An interface class to manage connectivity subsystem recovery/restart operations. + */ +public class ConnectivitySubsystemsRecoveryManager { + private static final String TAG = "ConnectivitySubsystemsRecoveryManager"; + + private final Context mContext; + private final Handler mHandler; + private RecoveryAvailableListener mRecoveryAvailableListener = null; + + private static final long RESTART_TIMEOUT_MS = 15_000; // 15 seconds + + private WifiManager mWifiManager = null; + private TelephonyManager mTelephonyManager = null; + private final BroadcastReceiver mApmMonitor = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + RecoveryAvailableListener listener = mRecoveryAvailableListener; + if (listener != null) { + listener.onRecoveryAvailableChangeListener(isRecoveryAvailable()); + } + } + }; + private boolean mApmMonitorRegistered = false; + private boolean mWifiRestartInProgress = false; + private boolean mTelephonyRestartInProgress = false; + private RecoveryStatusCallback mCurrentRecoveryCallback = null; + private final BroadcastReceiver mWifiMonitor = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!mWifiRestartInProgress || mCurrentRecoveryCallback == null) { + stopTrackingWifiRestart(); + } + + // TODO: harden this code to avoid race condition. What if WiFi toggled just before + // recovery triggered. Either use new broadcasts from framework or detect more state + // changes. + boolean recoveryDone = false; + if (TextUtils.equals(intent.getAction(), WifiManager.WIFI_STATE_CHANGED_ACTION)) { + if (intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, + WifiManager.WIFI_STATE_UNKNOWN) == WifiManager.WIFI_STATE_ENABLED) { + recoveryDone = true; + } + } else if (TextUtils.equals(intent.getAction(), + WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) { + if (intent.getIntExtra(WifiManager.EXTRA_WIFI_AP_STATE, + WifiManager.WIFI_AP_STATE_FAILED) == WifiManager.WIFI_AP_STATE_ENABLED) { + recoveryDone = true; + } + } + + if (recoveryDone) { + mWifiRestartInProgress = false; + stopTrackingWifiRestart(); + checkIfAllSubsystemsRestartsAreDone(); + } + } + }; + private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { + @Override + public void onRadioPowerStateChanged(int state) { + if (!mTelephonyRestartInProgress || mCurrentRecoveryCallback == null) { + stopTrackingTelephonyRestart(); + } + + if (state == TelephonyManager.RADIO_POWER_ON) { + mTelephonyRestartInProgress = false; + stopTrackingTelephonyRestart(); + checkIfAllSubsystemsRestartsAreDone(); + } + } + }; + + public ConnectivitySubsystemsRecoveryManager(@NonNull Context context, + @NonNull Handler handler) { + mContext = context; + mHandler = new Handler(handler.getLooper()); + + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) { + mWifiManager = mContext.getSystemService(WifiManager.class); + if (mWifiManager == null) { + Log.e(TAG, "WifiManager not available!?"); + } + } + + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + if (mTelephonyManager == null) { + Log.e(TAG, "TelephonyManager not available!?"); + } + } + } + + /** + * A listener which indicates to the caller whether a recovery operation is available across + * the specified technologies. + * + * Set using {@link #setRecoveryAvailableListener(RecoveryAvailableListener)}, cleared + * using {@link #clearRecoveryAvailableListener()}. + */ + public interface RecoveryAvailableListener { + /** + * Called whenever the recovery availability status changes. + * + * @param isAvailable True if recovery is available across ANY of the requested + * technologies, false if recovery is not available across ALL of the + * requested technologies. + */ + void onRecoveryAvailableChangeListener(boolean isAvailable); + } + + /** + * Set a {@link RecoveryAvailableListener} to listen to changes in the recovery availability + * operation for the specified technology(ies). + * + * @param listener Listener to be triggered + */ + public void setRecoveryAvailableListener(@NonNull RecoveryAvailableListener listener) { + mHandler.post(() -> { + mRecoveryAvailableListener = listener; + startTrackingRecoveryAvailability(); + }); + } + + /** + * Clear a listener set with + * {@link #setRecoveryAvailableListener(RecoveryAvailableListener)}. + */ + public void clearRecoveryAvailableListener() { + mHandler.post(() -> { + mRecoveryAvailableListener = null; + stopTrackingRecoveryAvailability(); + }); + } + + private boolean isApmEnabled() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 0) == 1; + } + + private boolean isWifiEnabled() { + // TODO: this doesn't consider the scan-only mode. I.e. WiFi is "disabled" while location + // mode is enabled. Probably need to reset WiFi in that state as well. Though this may + // appear strange to the user in that they've actually disabled WiFi. + return mWifiManager != null && (mWifiManager.isWifiEnabled() + || mWifiManager.isWifiApEnabled()); + } + + /** + * Provide an indication as to whether subsystem recovery is "available" - i.e. will be + * executed if triggered via {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)}. + * + * @return true if a subsystem recovery is available, false otherwise. + */ + public boolean isRecoveryAvailable() { + if (!isApmEnabled()) return true; + + // even if APM is enabled we may still have recovery potential if WiFi is enabled + return isWifiEnabled(); + } + + private void startTrackingRecoveryAvailability() { + if (mApmMonitorRegistered) return; + + mContext.registerReceiver(mApmMonitor, + new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED), null, mHandler); + mApmMonitorRegistered = true; + } + + private void stopTrackingRecoveryAvailability() { + if (!mApmMonitorRegistered) return; + + mContext.unregisterReceiver(mApmMonitor); + mApmMonitorRegistered = false; + } + + private void startTrackingWifiRestart() { + IntentFilter filter = new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION); + filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); + mContext.registerReceiver(mWifiMonitor, filter, null, mHandler); + } + + private void stopTrackingWifiRestart() { + mContext.unregisterReceiver(mWifiMonitor); + } + + private void startTrackingTelephonyRestart() { + mTelephonyManager.registerPhoneStateListener(new HandlerExecutor(mHandler), + mPhoneStateListener); + } + + private void stopTrackingTelephonyRestart() { + mTelephonyManager.unregisterPhoneStateListener(mPhoneStateListener); + } + + private void checkIfAllSubsystemsRestartsAreDone() { + if (!mWifiRestartInProgress && !mTelephonyRestartInProgress) { + mCurrentRecoveryCallback.onSubsystemRestartOperationEnd(); + mCurrentRecoveryCallback = null; + } + } + + /** + * Callbacks used with + * {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} to get + * information about when recovery starts and is completed. + */ + public interface RecoveryStatusCallback { + /** + * Callback for a subsystem restart triggered via + * {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} - indicates + * that operation has started. + */ + void onSubsystemRestartOperationBegin(); + + /** + * Callback for a subsystem restart triggered via + * {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} - indicates + * that operation has ended. Note that subsystems may still take some time to come up to + * full functionality. + */ + void onSubsystemRestartOperationEnd(); + } + + /** + * Trigger connectivity recovery for all requested technologies. + * + * @param reason An optional reason code to pass through to the technology-specific + * API. May be used to trigger a bug report. + * @param callback Callbacks triggered when recovery status changes. + */ + public void triggerSubsystemRestart(String reason, @NonNull RecoveryStatusCallback callback) { + mHandler.post(() -> { + boolean someSubsystemRestarted = false; + + if (mWifiRestartInProgress) { + Log.e(TAG, "Wifi restart still in progress"); + return; + } + + if (mTelephonyRestartInProgress) { + Log.e(TAG, "Telephony restart still in progress"); + return; + } + + if (isWifiEnabled()) { + mWifiManager.restartWifiSubsystem(reason); + mWifiRestartInProgress = true; + someSubsystemRestarted = true; + startTrackingWifiRestart(); + } + + if (mTelephonyManager != null && !isApmEnabled()) { + if (mTelephonyManager.rebootRadio()) { + mTelephonyRestartInProgress = true; + someSubsystemRestarted = true; + startTrackingTelephonyRestart(); + } + } + + if (someSubsystemRestarted) { + mCurrentRecoveryCallback = callback; + callback.onSubsystemRestartOperationBegin(); + + mHandler.postDelayed(() -> { + stopTrackingWifiRestart(); + stopTrackingTelephonyRestart(); + mWifiRestartInProgress = false; + mTelephonyRestartInProgress = false; + mCurrentRecoveryCallback.onSubsystemRestartOperationEnd(); + mCurrentRecoveryCallback = null; + }, RESTART_TIMEOUT_MS); + } + }); + } +} +