Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1d8bab61 authored by Etan Cohen's avatar Etan Cohen Committed by Android (Google) Code Review
Browse files

Merge "[SettingsLib] Wrapper for the "fix connectivity" Settings action"

parents 47e8e593 a8460318
Loading
Loading
Loading
Loading
+310 −0
Original line number Diff line number Diff line
/*
 * 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);
            }
        });
    }
}