blob: acbc44e68dd8a19698b72bb906db4e8462178a1f [file] [log] [blame]
/*
* Copyright (C) 2017 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.clockwork.wifi;
import static com.android.clockwork.common.ThermalEmergencyTracker.ThermalEmergencyMode;
import static com.android.clockwork.wifi.WearWifiMediatorSettings.WIFI_SETTING_ON;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.SystemClock;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import com.android.clockwork.bluetooth.CompanionTracker;
import com.android.clockwork.common.DeviceEnableSetting;
import com.android.clockwork.common.EventHistory;
import com.android.clockwork.common.PartialWakeLock;
import com.android.clockwork.common.ProxyConnectivityDebounce;
import com.android.clockwork.common.RadioToggler;
import com.android.clockwork.connectivity.WearConnectivityPackageManager;
import com.android.clockwork.flags.BooleanFlag;
import com.android.clockwork.power.PowerTracker;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.util.concurrent.TimeUnit;
/**
* Wi-Fi Mediator responds to these signals:
* - Whether device is charging
* - Whether wifi has been requested by another app
* - Whether device is connected to bluetooth
* - Whether the device has any configured networks to connect to
*
* Details: go/cw-wifi-state-management-f go/cw-wifi-settings-management-f
*/
public class WearWifiMediator implements
DeviceEnableSetting.Listener,
PowerTracker.Listener,
WearWifiMediatorSettings.Listener,
WifiBackoff.Listener, ProxyConnectivityDebounce.Listener {
static final String ACTION_EXIT_WIFI_LINGER =
"com.android.clockwork.connectivity.action.ACTION_EXIT_WIFI_LINGER";
private static final String TAG = "WearWifiMediator";
/**
* Enforces a delay every time we decide to turn WiFi off.
* Prevents WiFi from thrashing when we very quickly transition between desired wifi states.
*
* We use AlarmManager to enforce the turning off of WiFi after the linger period.
* The constants below define the default linger window to be 30-60 seconds.
*/
private static final long DEFAULT_WIFI_LINGER_DURATION_MS = TimeUnit.SECONDS.toMillis(30);
private static final long MAX_ACCEPTABLE_LINGER_DELAY_MS = TimeUnit.SECONDS.toMillis(30);
/**
* How long should we wait between calls to enabling/disabling the WifiAdapter.
*/
private static final long WAIT_FOR_RADIO_TOGGLE_DELAY_MS = TimeUnit.SECONDS.toMillis(2);
@VisibleForTesting
static long sWifiNetworkWorkaroundDelayMs = TimeUnit.SECONDS.toMillis(30);
@VisibleForTesting
final PendingIntent exitWifiLingerIntent;
// dependencies
private final Context mContext;
private final AlarmManager mAlarmManager;
private final WearWifiMediatorSettings mSettings;
private final CompanionTracker mCompanionTracker;
private final PowerTracker mPowerTracker;
private final DeviceEnableSetting mDeviceEnableSetting;
private final WearConnectivityPackageManager mWearConnectivityPackageManager;
private final BooleanFlag mUserAbsentRadiosOff;
private final WifiBackoff mWifiBackoff;
private final WifiManager mWifiManager;
private final EventHistory<WifiDecision> mDecisionHistory =
new EventHistory<>("Wifi Decision History", 30, false);
private final WifiLogger mWifiLogger;
private RadioToggler mRadioToggler;
// params
private long mWifiLingerDurationMs;
// state
private boolean mInitialized = false;
private String mWifiSetting;
private boolean mInWifiSettings = false;
private boolean mEnableWifiWhenCharging;
private boolean mWifiDesired;
private boolean mWifiConnected = false;
private boolean mWifiLingering = false;
@VisibleForTesting
BroadcastReceiver exitWifiLingerReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_EXIT_WIFI_LINGER.equals(intent.getAction())) {
if (mWifiLingering) {
mWifiLingering = false;
toggleRadioState(false);
}
}
}
};
private boolean mShouldDelayNextWifiOn = false;
private AlarmManager.OnAlarmListener mExitWifiDelayAlarm;
private boolean mCancelBackOff = false;
private boolean mInHardwareLowPowerMode;
private boolean mIsProxyConnected;
private int mNumUnmeteredRequests = 0;
private int mNumHighBandwidthRequests = 0;
private int mNumWifiRequests = 0;
private boolean mActivityMode = false;
private boolean mCellOnlyMode = false;
private boolean mIsThermalEmergency;
private int mNumConfiguredNetworks = 0;
private boolean mDisableWifiMediator = false;
private boolean mWifiOnWhenProxyDisconnected = true;
private boolean mIsUserUnlocked = false;
private TelephonyManager mTelephonyManager;
private boolean mIsInEmergencyCall;
private long mWifiWaitForBtOnBootElapsedTime;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION:
boolean multipleNetworksChanged = intent.getBooleanExtra(
WifiManager.EXTRA_MULTIPLE_NETWORKS_CHANGED, false);
if (multipleNetworksChanged) {
refreshNumConfiguredNetworks();
} else {
int changeReason = intent.getIntExtra(WifiManager.EXTRA_CHANGE_REASON, -1);
if (changeReason == WifiManager.CHANGE_REASON_ADDED) {
mNumConfiguredNetworks++;
} else if (changeReason == WifiManager.CHANGE_REASON_REMOVED) {
mNumConfiguredNetworks--;
}
// This should basically never happen, but in case something goes wrong...
if (mNumConfiguredNetworks < 0) {
Log.w(TAG, "Configured networks is less than 0. Doing a full refresh");
refreshNumConfiguredNetworks();
}
}
updateWifiState("mNumConfiguredNetworks changed: " + mNumConfiguredNetworks);
break;
case WifiManager.NETWORK_STATE_CHANGED_ACTION:
NetworkInfo networkInfo = intent.getParcelableExtra(
WifiManager.EXTRA_NETWORK_INFO);
boolean prevConnectedState = mWifiConnected;
Log.d(TAG, "NETWORK_STATE_CHANGED : "
+ (mWifiConnected ? "Connected" : "Disconnected"));
mWifiConnected = networkInfo.isConnected();
if (prevConnectedState != mWifiConnected) {
refreshBackoffStatus();
}
break;
case WifiManager.WIFI_STATE_CHANGED_ACTION:
// WifiMediator needs to monitor WiFi state in case another app directly
// manipulates the WiFi adapter by calling WifiManager.setWifiEnabled.
// The trick here is to distinguish changes to the adapter state that are
// a result of WifiMediator's own decisions, vs. changes to the adapter state
// from an outside app calling into WifiManager.
// see b/34360714 for more background/details
int wifiState = intent.getIntExtra(
WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN);
if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
mRadioToggler.refreshRadioState();
if (WIFI_SETTING_ON.equals(mWifiSetting)) {
// If WiFi is already set to ON, then WiFi may have been turned
// on by WifiMediator itself. We should only force an update if
// mWifiDesired is false in this case.
if (!mWifiDesired) {
Log.d(TAG,
"Wifi is ON but mWifiDesired is FALSE. Updating state...");
long savedWifiLingerDuration = mWifiLingerDurationMs;
mWifiLingerDurationMs = -1;
updateWifiState("WiFi unexpectedly ON");
mWifiLingerDurationMs = savedWifiLingerDuration;
}
} else {
// This is definitely the case where another app has directly toggled
// WiFi via WifiManager.setWifiEnabled(true). We want that
// to effectively set the WiFi Setting to ON.
Log.d(TAG, "WiFi re-enabled from outside the Settings Menu."
+ " Updating settings...");
// update our cache of states and settings to reflect reality
// before calling updateWifiState()
mWifiSetting = WIFI_SETTING_ON;
mSettings.putWifiSetting(mWifiSetting);
updateWifiState("WiFi turned on by setWifiEnabled(true)");
}
} else if (wifiState == WifiManager.WIFI_STATE_DISABLED) {
mRadioToggler.refreshRadioState();
if (WIFI_SETTING_ON.equals(mWifiSetting) && mWifiDesired) {
// Some app must have called setWifiEnabled(false) while WifiMediator
// is actively trying to keep WiFi on.
Log.d(TAG, "Wifi is OFF but mWifiDesired is TRUE. Updating state...");
updateWifiState("WiFi turned off by setWifiEnabled(false)");
}
// ignore all other potential cases where setWifiEnabled(false) can
// be called. they're effectively a no-op, either because the WiFi Setting
// is already Off, or because wifi is not desired by WifiMediator
// we purposefully do not allow setWifiEnabled(false) to toggle the
// actual WiFi Setting to Off.
}
break;
case Intent.ACTION_NEW_OUTGOING_CALL:
// Emergency calls can only be detected when the telephony feature is available.
if (mTelephonyManager != null) {
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(phoneNumber);
if (mIsInEmergencyCall) {
Log.d(TAG, "Emergency call starting.");
updateWifiState("Emergency call starting");
}
}
break;
default:
// pass
}
}
};
// TODO(b/206298206): Only initialize if the Telephony feature is enabled.
@VisibleForTesting
final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
if (mIsInEmergencyCall && state == TelephonyManager.CALL_STATE_IDLE) {
Log.d(TAG, "Emergency call ended.");
mIsInEmergencyCall = false;
updateWifiState("Emergency call ended");
}
}
};
public WearWifiMediator(Context context,
AlarmManager alarmManager,
WearWifiMediatorSettings wifiMediatorSettings,
CompanionTracker companionTracker,
PowerTracker powerTracker,
DeviceEnableSetting deviceEnableSetting,
WearConnectivityPackageManager wearConnectivityPackageManager,
BooleanFlag userAbsentRadiosOff,
WifiBackoff wifiBackoff,
WifiManager wifiManager,
WifiLogger wifiLogger) {
mContext = context;
mAlarmManager = alarmManager;
mSettings = wifiMediatorSettings;
mSettings.addListener(this);
mCompanionTracker = companionTracker;
mPowerTracker = powerTracker;
mPowerTracker.addListener(this);
mUserAbsentRadiosOff = userAbsentRadiosOff;
mUserAbsentRadiosOff.addListener(this::onUserAbsentRadiosOffChanged);
mDeviceEnableSetting = deviceEnableSetting;
mDeviceEnableSetting.addListener(this);
mWearConnectivityPackageManager = wearConnectivityPackageManager;
mWifiBackoff = wifiBackoff;
mWifiBackoff.setListener(this);
mWifiManager = wifiManager;
mWifiLogger = wifiLogger;
mWifiLingerDurationMs = DEFAULT_WIFI_LINGER_DURATION_MS;
IntentFilter intentFilter = getBroadcastReceiverIntentFilter();
PackageManager packageManager = context.getPackageManager();
// If telephony/cellular is available then listen to call states necessary to know when in
// emergency calls. Wifi scans can be used to determine location during emergency calls.
if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL);
}
context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
exitWifiLingerIntent =
PendingIntent.getBroadcast(
context,
0,
new Intent(ACTION_EXIT_WIFI_LINGER),
PendingIntent.FLAG_IMMUTABLE);
RadioToggler.Radio wifiRadio = new RadioToggler.Radio() {
@Override
public String logTag() {
return TAG;
}
@Override
public boolean getEnabled() {
return mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED;
}
@Override
public void setEnabled(boolean enabled) {
mWifiManager.setWifiEnabled(enabled);
}
};
mRadioToggler = new RadioToggler(wifiRadio,
new PartialWakeLock(mContext, "WifiMediator"), WAIT_FOR_RADIO_TOGGLE_DELAY_MS);
}
@VisibleForTesting
void overrideRadioTogglerForTest(RadioToggler radioToggler) {
mRadioToggler = radioToggler;
}
public void onBootCompleted(boolean proxyConnected) {
mContext.registerReceiver(exitWifiLingerReceiver,
new IntentFilter(ACTION_EXIT_WIFI_LINGER),
Context.RECEIVER_EXPORTED_UNAUDITED);
mIsProxyConnected = proxyConnected;
mWifiSetting = mSettings.getWifiSetting();
mEnableWifiWhenCharging = mSettings.getEnableWifiWhileCharging();
mDisableWifiMediator = mSettings.getDisableWifiMediator();
mInHardwareLowPowerMode = mSettings.getHardwareLowPowerMode();
mRadioToggler.refreshRadioState();
mWifiOnWhenProxyDisconnected =
mSettings.getWifiOnWhenProxyDisconnected();
getConfiguredNetworksAsync();
mInitialized = true;
updateWifiState("onBootCompleted");
}
/** Called when exit direct boot, user unlocked device. */
public void onUserUnlocked() {
mIsUserUnlocked = true;
// Allow some time for Bluetooth to connect before turning on wifi. This avoids the
// unnecessary transition to wifi on during boot, then establish the bluetooth connection
// and immediately turn off wifi.
mShouldDelayNextWifiOn = true;
updateWifiState("onUserUnlocked");
}
private void getConfiguredNetworksAsync() {
final Handler handler = new Handler();
handler.post(() -> {
Log.d(TAG, "Checking configured networks.");
refreshNumConfiguredNetworks();
if (mNumConfiguredNetworks != 0) {
updateWifiState("numConfiguredNetworksUpdated");
return;
}
handler.postDelayed(() -> {
Log.d(TAG, "Re-checking configured networks .");
refreshNumConfiguredNetworks();
updateWifiState("numConfiguredNetworksUpdated");
}, sWifiNetworkWorkaroundDelayMs);
});
}
@VisibleForTesting
void setWifiLingerDuration(long durationMs) {
mWifiLingerDurationMs = durationMs;
}
/**
* Do NOT use this method outside of testing!
*/
@VisibleForTesting
void setNumConfiguredNetworks(int numConfiguredNetworks) {
mNumConfiguredNetworks = numConfiguredNetworks;
}
@Override
public void onProxyConnectedChange(boolean isProxyConnected) {
mIsProxyConnected = isProxyConnected;
updateWifiState("proxyConnected changed: " + mIsProxyConnected);
}
public void updateNumUnmeteredRequests(int numUnmeteredRequests) {
mNumUnmeteredRequests = numUnmeteredRequests;
updateWifiState("numUnmeteredRequests changed: " + mNumUnmeteredRequests);
}
public void updateNumHighBandwidthRequests(int numHighBandwidthRequests) {
mNumHighBandwidthRequests = numHighBandwidthRequests;
updateWifiState("numHighBandwidthRequests changed: " + mNumHighBandwidthRequests);
}
public void updateNumWifiRequests(int numWifiRequests) {
mNumWifiRequests = numWifiRequests;
updateWifiState("numWifiRequests changed: " + mNumWifiRequests);
}
public void updateActivityMode(boolean activityMode) {
if (mActivityMode != activityMode) {
mActivityMode = activityMode;
updateWifiState("activity mode changed: " + mActivityMode);
}
}
public void updateCellOnlyMode(boolean cellOnlyMode) {
if (mCellOnlyMode != cellOnlyMode) {
mCellOnlyMode = cellOnlyMode;
updateWifiState("cell only mode changed: " + mCellOnlyMode);
}
}
/** Trigger mediator update due to change in connectivity thermal manager. */
public void updateThermalEmergencyMode(ThermalEmergencyMode mode) {
boolean enabled = mode.isEnabled() && mode.isWifiEffected();
if (mIsThermalEmergency != enabled) {
mIsThermalEmergency = enabled;
updateWifiState("thermal emergency changed: " + enabled);
}
}
public void onUserAbsentRadiosOffChanged(boolean isEnabled) {
updateWifiState("UserAbsentRadiosOff flag changed: " + isEnabled);
}
@Override
public void onDeviceEnableChanged() {
if (mDeviceEnableSetting.affectsWifi()) {
updateWifiState(
"DeviceEnabled flag changed: " + mDeviceEnableSetting.isDeviceEnabled());
}
}
private void refreshNumConfiguredNetworks() {
mNumConfiguredNetworks = mWifiManager.getConfiguredNetworks().size();
Log.d(TAG, "mNumConfiguredNetworks refreshed to: " + mNumConfiguredNetworks);
}
/**
* PowerTracker.Listener callbacks
*/
@Override
public void onPowerSaveModeChanged() {
updateWifiState("PowerSaveMode changed: " + mPowerTracker.isInPowerSave());
}
@Override
public void onChargingStateChanged() {
updateWifiState("ChargingState changed: " + mPowerTracker.isCharging());
}
@Override
public void onDeviceIdleModeChanged() {
if (!mPowerTracker.getDozeModeAllowListedFeatures()
.get(PowerTracker.DOZE_MODE_WIFI_INDEX)) {
updateWifiState("DeviceIdleMode changed: " + mPowerTracker.isDeviceIdle());
} else {
Log.d(TAG, "Ignoring doze mode intent as WiFi is being kept enabled during doze.");
}
}
/**
* WearWifiMediatorSettings.Listener callbacks
*/
@Override
public void onWifiSettingChanged(String newWifiSetting) {
mWifiSetting = newWifiSetting;
updateWifiState("wifiSetting changed: " + mWifiSetting);
}
@Override
public void onInWifiSettingsMenuChanged(boolean inWifiSettingsMenu) {
mInWifiSettings = inWifiSettingsMenu;
updateWifiState("inWifiSettingsMenu changed: " + mInWifiSettings);
}
@Override
public void onEnableWifiWhileChargingChanged(boolean enableWifiWhileCharging) {
mEnableWifiWhenCharging = enableWifiWhileCharging;
updateWifiState("enableWifiWhileCharging changed: " + mEnableWifiWhenCharging);
}
@Override
public void onDisableWifiMediatorChanged(boolean disableWifiMediator) {
mDisableWifiMediator = disableWifiMediator;
updateWifiState("disableWifiMediator changed: " + mDisableWifiMediator);
}
@Override
public void onHardwareLowPowerModeChanged(boolean inHardwareLowPowerMode) {
mInHardwareLowPowerMode = inHardwareLowPowerMode;
updateWifiState("inHardwareLowPowerMode changed: " + mInHardwareLowPowerMode);
}
@Override
public void onWifiOnWhenProxyDisconnectedChanged(boolean wifiOnWhenProxyDisconnected) {
mWifiOnWhenProxyDisconnected = wifiOnWhenProxyDisconnected;
updateWifiState("wifiOnWhenProxyDisconnected changed: " + mWifiOnWhenProxyDisconnected);
}
/**
* WifiBackoff.Listener callbacks
*/
@Override
public void onWifiBackoffChanged() {
updateWifiState("onWifiBackoffChanged: " + mWifiBackoff.isInBackoff());
}
/**
* Sets the flag for backoff cancellation
*/
private void setCancelBackOff(boolean mCancelBackOff) {
this.mCancelBackOff = mCancelBackOff;
}
/**
* Turn on or off wifi based on state.
*
* Be very careful when adding a new rule! The order in which these rules are laid out
* defines their priority and conditionality. Each rule is subject to the conditions of
* all the rules above it, but not vice versa.
*/
private void updateWifiState(String reason) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "updateWifiState [" + reason + "]");
}
if (!mInitialized) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "updateWifiState [" + reason + "] ignored because"
+ " WifiMediator is not yet initialized");
}
return;
}
if (mInHardwareLowPowerMode) {
setWifiDecision(false, WifiDecisionReason.OFF_HARDWARE_LOW_POWER);
setCancelBackOff(true);
} else if (mIsThermalEmergency) {
setWifiDecision(false, WifiDecisionReason.OFF_THERMAL_EMERGENCY);
setCancelBackOff(true);
} else if (mDisableWifiMediator) {
return;
} else if (!WIFI_SETTING_ON.equals(mWifiSetting)) {
// WIFI_SETTING_OFF/OFF_AIRPLANE unconditionally keeps Wi-Fi off.
setWifiDecision(false, WifiDecisionReason.OFF_WIFI_SETTING_OFF);
setCancelBackOff(true);
} else if (mIsInEmergencyCall) {
// Wifi is used for location during emergency calls.
setWifiDecision(true, WifiDecisionReason.ON_EMERGENCY);
// Don't allow wifi backoff to turn off wifi if no connection is available.
setCancelBackOff(true);
} else if (!mIsUserUnlocked) {
setWifiDecision(false, WifiDecisionReason.OFF_DIRECTBOOT);
} else if (mDeviceEnableSetting.affectsWifi() && !mDeviceEnableSetting.isDeviceEnabled()) {
setWifiDecision(false, WifiDecisionReason.OFF_DEVICE_DISABLED);
setCancelBackOff(true);
} else if (mCellOnlyMode) {
setWifiDecision(false, WifiDecisionReason.OFF_CELL_ONLY_MODE);
setCancelBackOff(true);
} else if (mInWifiSettings) {
// If WIFI_SETTING_ON and in Wifi Settings, we should always turn on wifi.
setWifiDecision(true, WifiDecisionReason.ON_IN_WIFI_SETTINGS);
setCancelBackOff(false);
} else if (mPowerTracker.isCharging() && mEnableWifiWhenCharging) {
setWifiDecision(true, WifiDecisionReason.ON_CHARGING);
setCancelBackOff(false);
} else if (mPowerTracker.isInPowerSave()) {
setWifiDecision(false, WifiDecisionReason.OFF_POWER_SAVE);
setCancelBackOff(true);
} else if (mActivityMode) {
setWifiDecision(false, WifiDecisionReason.OFF_ACTIVITY_MODE);
setCancelBackOff(true);
} else if (mPowerTracker.isDeviceIdle() && mUserAbsentRadiosOff.isEnabled()
&& !mPowerTracker.getDozeModeAllowListedFeatures().get(
PowerTracker.DOZE_MODE_WIFI_INDEX)) {
setWifiDecision(false, WifiDecisionReason.OFF_USER_ABSENT);
setCancelBackOff(true);
} else if (mWifiBackoff.isInBackoff()) {
// All rules past this one are subject to Wifi Backoff.
setWifiDecision(false, WifiDecisionReason.OFF_WIFI_BACKOFF);
} else if (mNumHighBandwidthRequests > 0) {
setWifiDecision(true, WifiDecisionReason.ON_NETWORK_REQUEST);
setCancelBackOff(false);
} else if (mNumUnmeteredRequests > 0 && mCompanionTracker.isCompanionBle()) {
setWifiDecision(true, WifiDecisionReason.ON_NETWORK_REQUEST);
setCancelBackOff(false);
} else if (mNumWifiRequests > 0) {
setWifiDecision(true, WifiDecisionReason.ON_NETWORK_REQUEST);
setCancelBackOff(false);
} else if (mNumConfiguredNetworks == 0) {
setWifiDecision(false, WifiDecisionReason.OFF_NO_CONFIGURED_NETWORKS);
} else if (mWifiOnWhenProxyDisconnected && !mIsProxyConnected) {
if (mShouldDelayNextWifiOn) {
delayWifiEnable(WifiDecisionReason.ON_PROXY_DISCONNECTED);
} else {
setWifiDecision(true, WifiDecisionReason.ON_PROXY_DISCONNECTED);
setCancelBackOff(false);
}
} else {
setWifiDecision(false, WifiDecisionReason.OFF_NO_REQUESTS);
}
refreshBackoffStatus();
}
private void delayWifiEnable(WifiDecisionReason reason) {
if (mExitWifiDelayAlarm != null) {
// already in wifi delay
return;
}
// TODO(216499169): rename settings to reflect common delay reason
long delayMs = mSettings.getWifiOnBootDelayMs();
if (delayMs <= 0) {
Log.d(TAG, "Delay disabled: " + delayMs);
setWifiDecision(true, reason);
return;
}
Log.d(TAG, "Delay wifi " + delayMs + "ms for reason: " + reason);
mExitWifiDelayAlarm = () -> {
if (mExitWifiDelayAlarm == null) {
// delay just cancelled but callback already queued
return;
}
Log.d(TAG, "Leaving wifi delay state");
mAlarmManager.cancel(mExitWifiDelayAlarm);
mShouldDelayNextWifiOn = false;
mExitWifiDelayAlarm = null;
updateWifiState("exit delay wifi");
};
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + delayMs, "WifiMediatorDelay",
mExitWifiDelayAlarm, null);
}
private void setWifiDecision(boolean wifiDesired, WifiDecisionReason reason) {
Log.d(TAG, String.format("SetWifiDecision: %s; reason:%s", wifiDesired, reason));
if (mExitWifiDelayAlarm != null) {
mAlarmManager.cancel(mExitWifiDelayAlarm);
mExitWifiDelayAlarm = null;
}
mWifiDesired = wifiDesired;
mDecisionHistory.recordEvent(new WifiDecision(reason));
if (mWifiDesired) {
mAlarmManager.cancel(exitWifiLingerIntent);
mWifiLingering = false;
toggleRadioState(mWifiDesired);
} else if (shouldSkipWifiLingering(reason)) {
// if wifi lingering is not configured or if the user is actively turning off wifi,
// do it right away
toggleRadioState(false);
} else {
Log.d(TAG, "linger before turning wifi Off with mWifiLingering = "
+ mWifiLingering + " , with mWifiLingerDurationMs = " + mWifiLingerDurationMs);
// for all other reasons for disabling wifi, linger before turning it off
// unless a previous call to updateWifiState already set the linger state
if (!mWifiLingering) {
mAlarmManager.cancel(exitWifiLingerIntent);
mWifiLingering = true;
mAlarmManager.setWindow(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + mWifiLingerDurationMs,
MAX_ACCEPTABLE_LINGER_DELAY_MS,
exitWifiLingerIntent);
}
}
mShouldDelayNextWifiOn =
reason == WifiDecisionReason.OFF_POWER_SAVE
|| reason == WifiDecisionReason.OFF_USER_ABSENT
|| reason == WifiDecisionReason.OFF_THERMAL_EMERGENCY
|| (reason == WifiDecisionReason.OFF_WIFI_SETTING_OFF
&& mSettings.getIsInAirplaneMode());
Log.d(TAG, "SetWifiDecision: next " +
(mShouldDelayNextWifiOn ? "should delay" : "no delay"));
}
/**
* This method codifies the cases where we should bypass WiFi lingering and directly
* shut off WiFi when the decision to do so is made.
*/
private boolean shouldSkipWifiLingering(WifiDecisionReason reason) {
return mWifiLingerDurationMs <= 0
|| WifiDecisionReason.OFF_WIFI_SETTING_OFF.equals(reason)
|| WifiDecisionReason.OFF_POWER_SAVE.equals(reason)
|| WifiDecisionReason.OFF_USER_ABSENT.equals(reason)
|| WifiDecisionReason.OFF_HARDWARE_LOW_POWER.equals(reason);
}
/**
* We need to keep track of the signals that ConnectivityService uses to determine whether
* to connect or disconnect wifi, and correlate that information with any changes in wifi
* state. This allows us to distinguish the cases when wifi is disconnected because we are
* not within range of an AP, or if wifi is disconnected simply because ConnectivityService
* deems it no longer necessary to connect to wifi.
*
* Having this distinction is necessary for scheduling backoff during appropriate times.
*/
private boolean connectivityServiceWantsWifi() {
return !mIsProxyConnected
|| (mNumUnmeteredRequests > 0 && mCompanionTracker.isCompanionBle())
|| (mNumHighBandwidthRequests > 0)
|| (mNumWifiRequests > 0);
}
/**
* This is a pretty conservative approach to deciding when to allow the device to enter or
* stay in Wifi Backoff. Basically, we'll only really go into backoff if something attempts
* to hold wifi up for a very long period of time and is unsuccessful in connecting.
*
* We will exit backoff the moment wifi is connected or the need to connect to wifi disappears,
* so this addresses the primary backoff use case (proxy disconnected outside of wifi range)
* while minimally affecting wifi behavior in all other scenarios.
*/
private void refreshBackoffStatus() {
if (!mWifiConnected && connectivityServiceWantsWifi() && !mPowerTracker.isCharging()
&& !mCancelBackOff) {
Log.d(TAG, "refreshBackoffStatus scheduleBackoff with mWifiConnected = "
+ mWifiConnected + " , connectivityServiceWantsWifi = "
+ connectivityServiceWantsWifi() + " , isCharging = "
+ mPowerTracker.isCharging() + " , mCancelBackOff = "
+ mCancelBackOff);
mWifiBackoff.scheduleBackoff();
} else {
Log.d(TAG, "refreshBackoffStatus cancelling backoff");
mWifiBackoff.cancelBackoff();
}
// any time we need to check for wifi backoff is a good time to update our current wifi
// status
mWifiLogger.recordWifiState(mRadioToggler.getRadioEnabled(), mWifiConnected,
connectivityServiceWantsWifi());
}
private void toggleRadioState(boolean enable) {
// Ensure the wifi specific packages match the expected wifi radio state.
Log.d(TAG, "toggleRadioState: " + enable);
mWearConnectivityPackageManager.onWifiRadioState(enable);
mRadioToggler.toggleRadio(enable);
}
@VisibleForTesting
IntentFilter getBroadcastReceiverIntentFilter() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
return intentFilter;
}
public void dump(IndentingPrintWriter ipw) {
ipw.println("======== WearWifiMediator ========");
if (mDisableWifiMediator) {
ipw.println("WifiMediator is disabled by developer setting. To re-enable, run:");
ipw.println(" adb shell settings put global cw_disable_wifimediator 0");
}
ipw.printPair("mWifiSetting", mWifiSetting);
ipw.printPair("mWifiEnabled", mRadioToggler.getRadioEnabled());
ipw.printPair("mDeviceEnabled", mDeviceEnableSetting.isDeviceEnabled());
ipw.printPair("deviceEnableAffectsWifi", mDeviceEnableSetting.affectsWifi());
ipw.println();
ipw.printPair("mWifiConnected", mWifiConnected);
ipw.printPair("mWifiLingering", mWifiLingering);
ipw.println();
ipw.printPair("mEnableWifiWhenCharging", mEnableWifiWhenCharging);
ipw.printPair("mInHardwareLowPowerMode", mInHardwareLowPowerMode);
ipw.println();
ipw.printPair("mWifiDesired", mWifiDesired);
ipw.printPair("mInWifiSettings", mInWifiSettings);
ipw.printPair("mNumConfiguredNetworks", mNumConfiguredNetworks);
ipw.println();
ipw.printPair("mWifiOnWhenProxyDisconnected", mWifiOnWhenProxyDisconnected);
ipw.printPair("mShouldDelayNextWifiOn", mShouldDelayNextWifiOn);
ipw.println();
ipw.printPair("mActivityMode", mActivityMode);
ipw.printPair("mIsThermalEmergency", mIsThermalEmergency);
ipw.println();
ipw.printPair("Allowed during doze mode",
mPowerTracker.getDozeModeAllowListedFeatures()
.get(PowerTracker.DOZE_MODE_WIFI_INDEX));
ipw.println();
ipw.println();
mWifiBackoff.dump(ipw);
ipw.println();
ipw.println();
mDecisionHistory.dump(ipw);
ipw.println();
ipw.println();
ipw.println();
}
private enum WifiDecisionReason {
OFF_WAIT_FOR_BT_ON_BOOT,
OFF_ACTIVITY_MODE,
OFF_CELL_ONLY_MODE,
OFF_HARDWARE_LOW_POWER,
OFF_NO_CONFIGURED_NETWORKS,
OFF_NO_REQUESTS,
OFF_POWER_SAVE,
OFF_USER_ABSENT,
OFF_WIFI_BACKOFF,
OFF_WIFI_SETTING_OFF,
OFF_DEVICE_DISABLED,
OFF_THERMAL_EMERGENCY,
OFF_DIRECTBOOT,
ON_CHARGING,
ON_IN_WIFI_SETTINGS,
ON_NETWORK_REQUEST,
ON_PROXY_DISCONNECTED,
ON_EMERGENCY,
}
public class WifiDecision extends EventHistory.Event {
public final WifiDecisionReason reason;
public WifiDecision(WifiDecisionReason reason) {
this.reason = reason;
}
@Override
public String getName() {
return reason.name();
}
}
}