blob: 33b459cb45ac1cae8747aad13c38a8a9e1c82507 [file] [log] [blame]
package com.android.clockwork.bluetooth;
import static com.android.clockwork.common.ThermalEmergencyTracker.ThermalEmergencyMode;
import static com.android.clockwork.common.WearBluetoothSettings.PROXY_SCORE_BLE;
import static com.android.clockwork.common.WearBluetoothSettings.PROXY_SCORE_CLASSIC;
import static com.android.clockwork.common.WearBluetoothSettings.PROXY_SCORE_ON_CHARGER;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.WorkerThread;
import android.app.AlarmManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.provider.Settings;
import android.util.EventLog;
import android.util.Log;
import com.android.clockwork.bluetooth.proxy.ProxyGattServer;
import com.android.clockwork.bluetooth.proxy.ProxyPinger;
import com.android.clockwork.bluetooth.proxy.ProxyServiceConfig;
import com.android.clockwork.bluetooth.proxy.ProxyServiceDetector;
import com.android.clockwork.common.DeviceEnableSetting;
import com.android.clockwork.common.EventHistory;
import com.android.clockwork.common.LogUtil;
import com.android.clockwork.common.ProxyConnectivityDebounce.ProxyStatus;
import com.android.clockwork.common.WearBluetoothSettings;
import com.android.clockwork.flags.BooleanFlag;
import com.android.clockwork.power.PowerTracker;
import com.android.clockwork.power.TimeOnlyMode;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class manages a collection of Shards, a set of objects that interact with the Bluetooth
* subsystem. To ensure correct use of the Bluetooth APIs, this class instantiates Shards only when
* it is safe to call Bluetooth APIs and destroys that when it is no longer safe to operate on
* Bluetooth.
*
* In particular, this class guarantees that Shards are only active when the following conditions
* are true:
*
* 1) A Bluetooth adapter exists on the device (not true in the Android emulator)
* 2) The Bluetooth adapter is enabled
* 3) The device is paired with a companion phone and the companion's BluetoothDevice object is
* available.
*
* Eventually, this class will also guarantee that the companion device is nearby and connectable
* before instantiating the Shards. This functionality is not currently available.
*/
public class WearBluetoothMediator implements
CompanionProxyShard.Listener,
CompanionTracker.Listener,
WearBluetoothMediatorSettings.Listener,
PowerTracker.Listener,
TimeOnlyMode.Listener,
ProxyGattServer.Listener,
DeviceEnableSetting.Listener,
ProxyServiceDetector.Listener {
static final String TAG = WearBluetoothSettings.LOG_TAG;
/** Listener for companion proxy connectivity status. */
public interface ProxyStatusListener {
/** called when proxy connectivity status changes. */
void onProxyStatusChange(ProxyStatus proxyStatus);
}
private static final int HEADSET_CLIENT_BIT = 1 << BluetoothProfile.HEADSET_CLIENT;
/** After attempting to connect proxy upon bootup, wait this long before giving up. */
static final Long CANCEL_ON_BOOT_CONNECT_DELAY_MS = TimeUnit.MINUTES.toMillis(5);
static final String ACTION_CANCEL_ON_BOOT_CONNECT =
"com.android.clockwork.bluetooth.action.CANCEL_ON_BOOT_CONNECT";
@VisibleForTesting
static final String ACTION_TOGGLE_HFP =
"com.android.clockwork.bluetooth.action.toggle_hfp";
@VisibleForTesting
static final String EXTRA_ADAPTER_ENABLE = "adapter_enable";
@VisibleForTesting
static final String EXTRA_HFP_ENABLE = "hfpEnabled";
@VisibleForTesting
static final String EXTRA_SET_BY_USER = "setByUser";
@VisibleForTesting
static final int MSG_DISABLE_BT = 0;
@VisibleForTesting
static final int MSG_ENABLE_BT = 1;
// A default timeoue of two minutes seems to be used by most devices at the moment.
@VisibleForTesting
static final int DEFAULT_DISCOVERABLE_TIMEOUT_SECS = 120;
@VisibleForTesting
static final int PORT_RFCOMM = 0;
private static final long WAIT_FOR_SET_RADIO_POWER_IN_MS = TimeUnit.SECONDS.toMillis(2);
private final Object mLock = new Object();
// TODO(cmanton) Do we need to keep a reference to this as it only used on boot
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
private final AtomicBoolean mProxyConnected = new AtomicBoolean(false);
private final AtomicBoolean mFirstAdapterEnableAfterBoot = new AtomicBoolean(true);
private final AtomicBoolean mUserUnlocked = new AtomicBoolean(false);
private final EventHistory<ProxyConnectionEvent> mProxyHistory =
new EventHistory<>("Proxy Connection History", 30, false);
private final EventHistory<BtDecision> mHistory =
new EventHistory<>("Bluetooth Radio Power History", 30, false);
private final AlarmManager mAlarmManager;
private final BluetoothAdapter mAdapter;
private final BluetoothLogger mBtLogger;
private final BluetoothShardRunner mShardRunner;
private final CompanionTracker mCompanionTracker;
private final Context mContext;
private final PowerTracker mPowerTracker;
private final WearBluetoothMediatorSettings mSettings;
private final DeviceInformationGattServer mDeviceInformationServer;
private final ProxyGattServer mProxyGattServer;
private final ProxyPinger mProxyPinger;
private final DeviceEnableSetting mDeviceEnableSetting;
private final BooleanFlag mUserAbsentRadiosOff;
private final BroadcastReceiver bondStateReceiver = new BroadcastReceiver() {
@MainThread
@Override
public void onReceive(Context mContext, Intent intent) {
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
final BluetoothDevice device = intent.getParcelableExtra(
BluetoothDevice.EXTRA_DEVICE);
final int previousBondState = intent.getIntExtra(
BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.BOND_NONE);
final int currentBondState = intent.getIntExtra(
BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
Log.i(TAG, "Device " + device + " changed bond state: " + currentBondState);
if (currentBondState == BluetoothDevice.BOND_BONDED) {
mCompanionTracker.receivedBondedAction(device);
}
if (previousBondState == BluetoothDevice.BOND_BONDED
&& currentBondState == BluetoothDevice.BOND_BONDING) {
mBtLogger.logUnexpectedPairingEvent(device);
}
}
}
};
@VisibleForTesting
HandlerThread mRadioPowerThread;
@VisibleForTesting
Handler mRadioPowerHandler;
private final BroadcastReceiver toggleHfpReceiver = new BroadcastReceiver() {
@MainThread
@Override
public void onReceive(Context mContext, Intent intent) {
if (!ACTION_TOGGLE_HFP.equals(intent.getAction())
|| !intent.hasExtra(EXTRA_HFP_ENABLE)) {
return;
}
boolean enabled = intent.getBooleanExtra(EXTRA_HFP_ENABLE, false);
boolean toggleResult = toggleHfpClientProfile(mContext,
enabled,
intent.getBooleanExtra(EXTRA_SET_BY_USER, false));
if (enabled && toggleResult) {
// Set a flag to reboot BT once it starts turning off.
mRebootAdapterDueToHfpEnable = true;
changeRadioPower(false, Reason.OFF_HFP_ENABLE);
}
}
};
private boolean mDeviceConnected;
private boolean mHfpConnected;
private final BroadcastReceiver adapterStateReceiver = new BroadcastReceiver() {
@MainThread
@Override
public void onReceive(Context mContext, Intent intent) {
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
final int adapterState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.STATE_OFF);
final int previousAdapterState = intent.getIntExtra(
BluetoothAdapter.EXTRA_PREVIOUS_STATE,
BluetoothAdapter.STATE_OFF);
if (adapterState == BluetoothAdapter.STATE_ON) {
onAdapterEnabled();
// Set this flag to false, now that BT has been turned on.
mRebootAdapterDueToHfpEnable = false;
} else if (previousAdapterState == BluetoothAdapter.STATE_ON) {
// All transition states are treated as if the adapter is off so that flows such
// as "ON -> TURNING_OFF -> TURNING_ON -> ON" are detected. This is useful to
// restart the GATT servers when Bluetooth process restarts.
onAdapterDisabled();
if (mRebootAdapterDueToHfpEnable) {
// This flags notes that we need enable BT because BT was disabled when HFP
// was being enabled.
changeRadioPower(true, Reason.ON_HFP_ENABLE);
}
}
}
}
};
private final BroadcastReceiver hfpConnectionStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED
.equals(intent.getAction())) {
throw new IllegalStateException(
"Expected ACTION_CONNECTION_STATE_CHANGED, received " + intent.getAction());
}
final int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
mHfpConnected = newState == BluetoothProfile.STATE_CONNECTED;
}
};
private final BroadcastReceiver aclStateReceiver = new BroadcastReceiver() {
@MainThread
@Override
public void onReceive(Context mContext, Intent intent) {
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (mCompanionTracker.getCompanion() == null
|| !device.getAddress().equals(mCompanionTracker.getCompanion().getAddress())) {
LogUtil.logD(TAG, "Ignoring ACL connection event for non-companion device.");
return;
}
switch (intent.getAction()) {
case BluetoothDevice.ACTION_ACL_CONNECTED:
LogUtil.logD(TAG, "ACL_CONNECTED for device: %s", device.getAddress());
if (device.isConnected()) {
onCompanionDeviceConnected();
}
break;
case BluetoothDevice.ACTION_ACL_DISCONNECTED:
LogUtil.logD(TAG, "ACL_DISCONNECTED for device: %s", device.getAddress());
if (!device.isConnected()) {
onCompanionDeviceDisconnected();
} else {
LogUtil.logD(TAG,
"Ignoring ACL Disconnected because device is still connected.");
}
break;
}
}
};
private boolean mActivityMode;
private boolean mCellOnlyMode;
private boolean mIsThermalEmergency;
private boolean mTimeOnlyMode;
private boolean mIsAirplaneModeOn;
private boolean mIsSettingsPreferenceBluetoothOn;
private boolean mRebootAdapterDueToHfpEnable;
private ProxyStatusListener mProxyStatusListener;
private final ProxyServiceDetector mProxyServiceDetector;
public WearBluetoothMediator(final Context context,
final AlarmManager alarmManager,
final WearBluetoothMediatorSettings btSettings,
final BluetoothAdapter btAdapter,
final BluetoothLogger btLogger,
final BluetoothShardRunner shardRunner,
final CompanionTracker companionTracker,
final PowerTracker powerTracker,
final DeviceEnableSetting deviceEnableSetting,
final BooleanFlag userAbsentRadiosOff,
final TimeOnlyMode timeOnlyMode,
final DeviceInformationGattServer deviceInformationServer,
final ProxyGattServer proxyGattServer,
final ProxyPinger proxyPinger) {
mContext = context;
mAlarmManager = alarmManager;
mSettings = btSettings;
mAdapter = btAdapter;
mBtLogger = btLogger;
mShardRunner = shardRunner;
mCompanionTracker = companionTracker;
mDeviceEnableSetting = deviceEnableSetting;
mUserAbsentRadiosOff = userAbsentRadiosOff;
mPowerTracker = powerTracker;
mProxyGattServer = proxyGattServer;
mProxyPinger = proxyPinger;
mCompanionTracker.addListener(this);
mPowerTracker.addListener(this);
mSettings.addListener(this);
mUserAbsentRadiosOff.addListener(this::onUserAbsentRadiosOffChanged);
timeOnlyMode.addListener(this);
proxyGattServer.setListener(this);
mDeviceEnableSetting.addListener(this);
mIsAirplaneModeOn = mSettings.getIsInAirplaneMode();
mIsSettingsPreferenceBluetoothOn = mSettings.getIsSettingsPreferenceBluetoothOn();
mDeviceInformationServer = deviceInformationServer;
mRadioPowerThread = new HandlerThread(TAG + ".RadioPowerHandler");
mRadioPowerThread.start();
mRadioPowerHandler = new RadioPowerHandler(mRadioPowerThread.getLooper());
mProxyServiceDetector = new ProxyServiceDetector(mContext, this, mCompanionTracker);
}
public void onBootCompleted() {
IntentFilter aclIntentFilter = new IntentFilter();
aclIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
aclIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
mContext.registerReceiver(aclStateReceiver, aclIntentFilter);
mContext.registerReceiver(adapterStateReceiver,
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
mContext.registerReceiver(bondStateReceiver,
new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
mContext.registerReceiver(toggleHfpReceiver,
new IntentFilter(ACTION_TOGGLE_HFP),
android.Manifest.permission.BLUETOOTH_ADMIN,
/* scheduler= */ null,
Context.RECEIVER_EXPORTED);
mContext.registerReceiver(hfpConnectionStateReceiver,
new IntentFilter(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED));
// onBootCompleted does NOT execute on the main thread, but all of this stuff needs to
// run on the main thread, so we redirect the work to the main mRadioPowerHandler here
mMainHandler.post(() -> {
if (mDeviceConnected || mProxyConnected.get()) {
return;
}
// The adapter should always be enabled on boot (unless airplane mode is on).
if (mAdapter.isEnabled()) {
onAdapterEnabled();
} else {
// Not enabled. Enable if airplane mode is NOT on.
if (!mSettings.getIsInAirplaneMode()) {
Log.w(TAG, "Enabling an unexpectedly disabled Bluetooth adapter.");
changeRadioPower(true, Reason.ON_BOOT_AUTO);
mSettings.setSettingsPreferenceBluetoothOn(true);
}
}
});
}
/** Called when exited direct boot, user unlocked device. */
public void onUserUnlocked() {
mUserUnlocked.set(true);
LogUtil.logD(TAG, "User unlocked; Companion connected " + mDeviceConnected);
if (mDeviceConnected && !mCompanionTracker.isCompanionBle()) {
doStartProxyShard("User unlocked");
}
}
private void doStartProxyShard(String reason) {
mProxyServiceDetector.update();
mShardRunner.startProxyShard(
getScoreForProxy(),
mSettings.getDnsServers(),
this,
reason,
mProxyServiceDetector.getCurrentConfig());
}
@Override
public void onBackgroundProxyConfigUpdated(String reason) {
if (mShardRunner.isProxyShardStarted()) {
// Only restart if already connected.
doStartProxyShard(reason);
}
}
@Override
public void onProxyConnectFailed(ProxyServiceConfig failedConfig) {
mProxyServiceDetector.handleProxyConnectError(failedConfig);
}
/**
* Note that transitions back to deviceEnabled are explicitly not handled here. Shard starts
* are triggered by an incoming BT connection, which occurs only if the user has re-enabled
* the device from the remote Companion app.
*
* <p>Effectively, re-enabling the device just allows for the next transition to disabled.
*/
@Override
public void onDeviceEnableChanged() {
if (!mDeviceEnableSetting.isDeviceEnabled()) {
mShardRunner.stopProxyShard();
stopHfcShard();
}
}
@Override
public void onTimeOnlyModeChanged(boolean timeOnlyMode) {
if (mTimeOnlyMode != timeOnlyMode) {
mTimeOnlyMode = timeOnlyMode;
updateRadioPower();
}
}
public void updateActivityMode(boolean activeMode) {
if (mActivityMode != activeMode) {
mActivityMode = activeMode;
updateRadioPower();
}
}
public void updateCellOnlyMode(boolean cellOnlyMode) {
if (mCellOnlyMode != cellOnlyMode) {
mCellOnlyMode = cellOnlyMode;
updateRadioPower();
}
}
/** Trigger mediator update due to change in connectivity thermal manager. */
public void updateThermalEmergencyMode(ThermalEmergencyMode mode) {
boolean enabled = mode.isEnabled() && mode.isBtEffected();
if (mIsThermalEmergency != enabled) {
mIsThermalEmergency = enabled;
updateRadioPower();
}
}
public void onUserAbsentRadiosOffChanged(boolean isEnabled) {
updateRadioPower();
}
public void setProxyStatusListener(ProxyStatusListener listener) {
mProxyStatusListener = listener;
}
private void updateRadioPower() {
if (mIsAirplaneModeOn) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Disabling mediator while airplane mode enabled");
}
return;
} else if (mCellOnlyMode) {
changeRadioPower(false, Reason.OFF_CELL_ONLY_MODE);
} else if (mActivityMode) {
changeRadioPower(false, Reason.OFF_ACTIVITY_MODE);
} else if (mIsThermalEmergency) {
changeRadioPower(false, Reason.OFF_THERMAL_EMERGENCY);
} else if (mPowerTracker.isDeviceIdle() && mUserAbsentRadiosOff.isEnabled()
&& !mPowerTracker.getDozeModeAllowListedFeatures().get(
PowerTracker.DOZE_MODE_BT_INDEX)) {
changeRadioPower(false, Reason.OFF_USER_ABSENT);
} else if (mTimeOnlyMode) {
changeRadioPower(false, Reason.OFF_TIME_ONLY_MODE);
} else if (!mIsSettingsPreferenceBluetoothOn) {
changeRadioPower(false, Reason.OFF_SETTINGS_PREFERENCE);
} else {
changeRadioPower(true, Reason.ON_AUTO);
}
}
private void changeRadioPower(boolean enable, Reason reason) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, reason.name() + " attempt to change radio power: " + enable);
}
Message msg = Message.obtain(mRadioPowerHandler,
enable ? MSG_ENABLE_BT : MSG_DISABLE_BT, reason);
mRadioPowerHandler.removeMessages(MSG_ENABLE_BT);
mRadioPowerHandler.removeMessages(MSG_DISABLE_BT);
mRadioPowerHandler.sendMessage(msg);
}
@Override
public void onPowerSaveModeChanged() {
// BluetoothMediator does not respond directly to PowerSaveMode changes.
}
@Override
public void onChargingStateChanged() {
mShardRunner.updateProxyShard(getScoreForProxy());
}
@Override
public void onDeviceIdleModeChanged() {
if (!mPowerTracker.getDozeModeAllowListedFeatures().get(PowerTracker.DOZE_MODE_BT_INDEX)) {
updateRadioPower();
} else {
Log.d(TAG, "Ignoring doze mode intent as BT is being kept enabled during doze.");
}
}
@Override // WearBluetoothMediatorSettings.Listener
public void onAirplaneModeSettingChanged(boolean isAirplaneModeOn) {
mIsAirplaneModeOn = isAirplaneModeOn;
}
@Override // WearBluetoothMediatorSettings.Listener
public void onSettingsPreferenceBluetoothSettingChanged(
boolean isSettingsPreferenceBluetoothOn) {
mIsSettingsPreferenceBluetoothOn = isSettingsPreferenceBluetoothOn;
}
/**
* Similar to meteredness, it is expected that DnsServers will change while proxy is running,
* such as whenever the phone transitions between various modes of connectivity. This ensures
* that the proxy updates alongside the phone.
*/
@Override // WearBluetoothMediatorSettings.Listener
public void onDnsServersChanged() {
mShardRunner.updateProxyShard(mSettings.getDnsServers());
}
@Override // ProxyGattServer.Listener
public void onProxyConfigUpdate(int psm, int channelChangeId, int minPingIntervalSeconds) {
mMainHandler.post(() -> {
boolean hasChanged = mProxyServiceDetector.setIosV1Params(psm, channelChangeId);
if (mProxyConnected.get() && !hasChanged) {
Log.i(TAG, "Proxy already connected. Ignoring config update");
return;
}
mProxyPinger.setMinPingIntervalMs(minPingIntervalSeconds * 1000);
doStartProxyShard("PSM Update Received");
});
}
@Override // CompanionProxyShard.Listener
public void onProxyBleData() {
mProxyPinger.pingIfNeeded();
}
private void updateProxyStatusListener(boolean isConnected, boolean hasInternet) {
if (mProxyStatusListener == null) {
Log.w(TAG, "No ProxyStatusListener set");
return;
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, String.format("updateProxyStatusListener: %s %s", isConnected, hasInternet));
}
if (mProxyStatusListener != null) {
mProxyStatusListener.onProxyStatusChange(
new ProxyStatus(isConnected, mAdapter.isEnabled(), hasInternet));
}
}
@Override // CompanionProxyShard.Listener
public void onProxyConnectionChange(boolean isConnected, int proxyScore, boolean withInternet) {
if (isConnected) {
LogUtil.logD(TAG, "sysproxy connection changed - connected"
+ (withInternet ? " with internet score (" + proxyScore + ")"
: " but with no internet"));
} else {
LogUtil.logD(TAG, "sysproxy connection changed - disconnected");
}
mProxyConnected.set(isConnected);
mBtLogger.logProxyConnectionChange(isConnected);
mProxyHistory.recordEvent(new ProxyConnectionEvent(
isConnected,
withInternet,
proxyScore));
updateProxyStatusListener(isConnected, withInternet);
if (!isConnected && mCompanionTracker.isCompanionBle()) {
// If the iOS companion app is terminated, e.g. because it crashed, the L2CAP socket is
// closed and sysproxy disconnects. However, iOS Bluetooth state restoration feature
// keeps the companion connected over GATT. The watch pings the companion app over GATT
// here to launch it again and reconnect over L2CAP.
mProxyPinger.ping();
}
}
public boolean isProxyConnected() {
return mProxyConnected.get();
}
@Override
public void onCompanionChanged() {
mBtLogger.logCompanionPairingEvent(mCompanionTracker.isCompanionBle());
mProxyServiceDetector.onCompanionChanged();
mProxyGattServer.setCompanionDevice(mCompanionTracker.getCompanion());
BluetoothDevice companion = mCompanionTracker.getCompanion();
if (companion == null || !companion.isConnected()) {
Log.w(TAG, "onCompanionChanged: wait ACL connected for new companion");
return;
}
setDeviceConnected(true);
if (!mCompanionTracker.isCompanionBle()) {
LogUtil.logD(TAG, "Starting Proxy Shard because new companion device paired.");
doStartProxyShard("Companion Found");
}
LogUtil.logD(TAG, "New companion device paired. Starting HfcShard.");
startHfcShard();
}
private void startHfcShard() {
LogUtil.logD(TAG, "Connect Bluetooth Classic Profiles.");
if (mCompanionTracker.getBluetoothClassicCompanion() != null && !mHfpConnected) {
LogUtil.logD(TAG, "Starting HFC shard.");
mShardRunner.startHfcShard(mCompanionTracker.getBluetoothClassicCompanion());
}
}
private void stopHfcShard() {
LogUtil.logD(TAG, "Disconnecting Bluetooth Classic Profiles.");
LogUtil.logD(TAG, "Stopping HFC Shard.");
mHfpConnected = false;
mShardRunner.stopHfcShard();
}
private int getScoreForProxy() {
return mCompanionTracker.isCompanionBle() ? PROXY_SCORE_BLE :
(mPowerTracker.isCharging() ? PROXY_SCORE_ON_CHARGER : PROXY_SCORE_CLASSIC);
}
private void onAdapterEnabled() {
boolean firstEnableAfterBoot = mFirstAdapterEnableAfterBoot.getAndSet(false);
if (firstEnableAfterBoot) {
mCompanionTracker.onBluetoothAdapterReady();
}
mDeviceInformationServer.stop();
mDeviceInformationServer.start();
mProxyServiceDetector.startDiscovery();
BluetoothDevice companionDevice = mCompanionTracker.getCompanion();
if (companionDevice == null) {
mProxyGattServer.stop();
LogUtil.logD(TAG, "Starting proxy GATT server as watch is not paired.");
// Proxy GATT server is only used for iOS but it starts in the unpaired state as well
// in case the watch is paired with iOS later. Starting it right after pairing would
// unnecessarily invalidate GATT handles and cause a temporary disconnection on the
// wearable transport.
mProxyGattServer.start();
return; // if no companion paired, we're done.
}
// Ensure that discoverable timeout isn't infinite when in paired
// state. This code is for handling a corner case and should not be
// relied upon to ensure that the adapter is in the expected state.
if (firstEnableAfterBoot) {
Duration discoverableTimeout = mAdapter.getDiscoverableTimeout();
if(discoverableTimeout != null && discoverableTimeout.toSeconds() == 0) {
Log.w(TAG, "Detected infinite discoverable timeout while paired. "
+ "Setting to default value of " + DEFAULT_DISCOVERABLE_TIMEOUT_SECS);
mAdapter.setDiscoverableTimeout(
Duration.ofSeconds(DEFAULT_DISCOVERABLE_TIMEOUT_SECS));
}
}
if (mCompanionTracker.isCompanionBle()) {
LogUtil.logD(TAG, "Starting proxy GATT server.");
mProxyGattServer.stop();
mProxyGattServer.setCompanionDevice(companionDevice);
mProxyGattServer.start();
}
}
private void onAdapterDisabled() {
LogUtil.logD(TAG, "Bluetooth Adapter disabled. Stopping all shards.");
mShardRunner.stopProxyShard();
mProxyServiceDetector.stopDiscovery();
stopHfcShard();
if (mDeviceConnected) {
setDeviceConnected(false);
}
// GATT servers are not stopped here to avoid sending an unnecessary GATT service changed
// indication to the companion due to removal of the GATT services. This is needed because
// the watch actually remains connected for a few seconds after the adapter starts turning
// off. Keeping the servers active is fine as the BT stack discards all GATT services when
// the adapter turns off anyway and service changed indication will be send later when the
// adapter turns on and the GATT servers start again.
}
private void onCompanionDeviceConnected() {
setDeviceConnected(true);
// If proxy is connected via some other means, then we don't need to start it again.
LogUtil.logD(TAG, "onCompanionDeviceConnected userUnlocked: " + mUserUnlocked.get());
// Set companion type to the Bluetooth module
final String strDeviceOsType =
mCompanionTracker.isCompanionBle() ? "COMPANION_SECONDARY" : "COMPANION_PRIMARY";
mCompanionTracker
.getCompanion()
.setMetadata(BluetoothDevice.METADATA_SOFTWARE_VERSION, strDeviceOsType.getBytes());
if (!mProxyConnected.get() && !mCompanionTracker.isCompanionBle()) {
if (mUserUnlocked.get()) {
LogUtil.logD(TAG, "Companion device connected. Starting proxy shard.");
doStartProxyShard("Companion Connected");
} else {
LogUtil.logD(TAG, "Companion device connected but waiting user unlock.");
}
}
startHfcShard();
}
private void onCompanionDeviceDisconnected() {
LogUtil.logD(TAG, "Companion device disconnected. Stopping proxy shard.");
setDeviceConnected(false);
mShardRunner.stopProxyShard();
stopHfcShard();
}
private void setDeviceConnected(boolean deviceConnected) {
mDeviceConnected = deviceConnected;
mBtLogger.logAclConnectionChange(deviceConnected);
}
public void dump(@NonNull final IndentingPrintWriter ipw) {
ipw.println("======== WearBluetoothMediator ========");
ipw.printPair("Companion address",
getCompanionAddress()
+ ((mCompanionTracker.getCompanion() == null) ? "(not bonded)" : ""));
ipw.printPair("Companion type", mCompanionTracker.isCompanionBle() ? "BLE" : "CLASSIC");
ipw.println();
ipw.printPair("Device", mDeviceConnected ? "connected" : "disconnected");
ipw.printPair("Proxy", mProxyConnected.get() ? "connected" : "disconnected");
ipw.printPair("btAdapter", mAdapter.isEnabled() ? "enabled" : "disabled");
ipw.println();
ipw.printPair("mIsAirplaneModeOn", mIsAirplaneModeOn);
ipw.printPair("mIsSettingsPreferenceBluetoothOn", mIsSettingsPreferenceBluetoothOn);
ipw.println();
ipw.printPair("mActivityMode", mActivityMode);
ipw.printPair("mIsThermalEmergency", mIsThermalEmergency);
ipw.printPair("mTimeOnlyMode", mTimeOnlyMode);
ipw.println();
ipw.printPair("Allowed during doze mode", mPowerTracker.getDozeModeAllowListedFeatures()
.get(PowerTracker.DOZE_MODE_BT_INDEX));
ipw.println();
mHistory.dump(ipw);
ipw.println();
mShardRunner.dumpShards(ipw);
ipw.println();
mProxyHistory.dump(ipw);
ipw.println();
mDeviceInformationServer.dump(ipw);
ipw.println();
mProxyServiceDetector.dump(ipw);
ipw.println();
mProxyGattServer.dump(ipw);
ipw.println();
}
/**
* Attempts toggling the HFP enabled-state, and returns {@code true} only if there has been an
* actual toggle from a different to the new provided state.
*/
private boolean toggleHfpClientProfile(Context context, boolean enable, boolean fromUser) {
ContentResolver resolver = context.getContentResolver();
final long disabledProfilesSetting =
Settings.Global.getLong(resolver, Settings.Global.BLUETOOTH_DISABLED_PROFILES, 0);
long modifiedSetting = disabledProfilesSetting;
final boolean currentEnabled = (disabledProfilesSetting & HEADSET_CLIENT_BIT) == 0;
if (enable && !currentEnabled) {
modifiedSetting ^= HEADSET_CLIENT_BIT;
} else if (!enable && currentEnabled) {
modifiedSetting |= HEADSET_CLIENT_BIT;
}
if (modifiedSetting == disabledProfilesSetting) {
return false;
}
Settings.Global.putLong(
resolver, Settings.Global.BLUETOOTH_DISABLED_PROFILES, modifiedSetting);
if (fromUser) {
Settings.Global.putInt(
resolver,
Settings.Global.Wearable.USER_HFP_CLIENT_SETTING,
enable
? Settings.Global.Wearable.HFP_CLIENT_ENABLED
: Settings.Global.Wearable.HFP_CLIENT_DISABLED);
}
return true;
}
private String getCompanionAddress() {
if (Build.IS_DEBUGGABLE) {
return Binder.withCleanCallingIdentity(mCompanionTracker::getCompanionAddress);
}
return "<redacted>";
}
/** The reason that Bluetooth radio power changed. */
public enum Reason {
OFF_ACTIVITY_MODE,
OFF_CELL_ONLY_MODE,
OFF_TIME_ONLY_MODE,
OFF_USER_ABSENT,
OFF_SETTINGS_PREFERENCE,
OFF_THERMAL_EMERGENCY,
ON_AUTO,
ON_BOOT_AUTO,
OFF_HFP_ENABLE,
ON_HFP_ENABLE
}
/** Encapsulate the decision process for modifying the bluetooth radio power state */
public class BtDecision extends EventHistory.Event {
public final Reason mReason;
public BtDecision(Reason reason) {
mReason = reason;
}
@Override
public String getName() {
return mReason.name();
}
}
/**
*
* @param connected Indicates watch has active rfcomm connection to phone.
* @param withInternet Indicates phone has validated default network.
* @param timestamp The timestamp in ms when the event triggered.
* @param score The current advertised network score for the network.
*/
@VisibleForTesting
final class ProxyConnectionEvent extends EventHistory.Event {
public final boolean connected;
public final boolean withInternet;
public final int score;
public ProxyConnectionEvent(boolean connected, boolean withInternet, int score) {
this.connected = connected;
this.withInternet = withInternet;
this.score = score;
}
@Override
public String getName() {
if (connected) {
if (withInternet) {
return "CONNECTED [SCORE:" + score + "]";
} else {
return "CONNECTED [NO INTERNET]";
}
} else {
return "DISCONNECTED";
}
}
@Override
public boolean isDuplicateOf(EventHistory.Event event) {
if (!(event instanceof ProxyConnectionEvent)) {
return false;
}
ProxyConnectionEvent that = (ProxyConnectionEvent) event;
// Ignore different network score if there is no internet
if (that.withInternet || withInternet) {
return that.connected == connected && that.withInternet == withInternet
&& that.score == score;
} else {
return that.connected == connected;
}
}
}
private class RadioPowerHandler extends Handler {
public RadioPowerHandler(Looper looper) {
super(looper);
}
@WorkerThread
@Override
public void handleMessage(Message msg) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "handleMessage: " + msg);
}
boolean enable = (msg.what == MSG_ENABLE_BT);
Reason reason = (Reason) msg.obj;
if (enable) {
mAdapter.enable();
} else {
mAdapter.disable();
}
// Log the radio change event.
final BtDecision decision = new BtDecision(reason);
EventLog.writeEvent(
EventLogTags.BT_RADIO_POWER_CHANGE_EVENT,
enable ? 1 : 0,
decision.getName(),
decision.getTimestampMs());
Log.i(TAG, decision.getName() + " changed radio power: " + enable);
mHistory.recordEvent(decision);
try {
synchronized (mLock) {
// Block the thread to ensure the service state is changed.
// 2 seconds timeout is enough for the radio power toggle.
mLock.wait(WAIT_FOR_SET_RADIO_POWER_IN_MS);
}
} catch (InterruptedException e) {
Log.e(TAG, "wait() interrupted!", e);
}
}
}
}