Loading android/app/res/values/config.xml +28 −0 Original line number Original line Diff line number Diff line Loading @@ -33,6 +33,34 @@ <integer name="gatt_low_power_min_interval">80</integer> <integer name="gatt_low_power_min_interval">80</integer> <integer name="gatt_low_power_max_interval">100</integer> <integer name="gatt_low_power_max_interval">100</integer> <!-- min/max connection intervals/latencies for companion devices --> <!-- Primary companion --> <integer name="gatt_high_priority_min_interval_primary">6</integer> <integer name="gatt_high_priority_max_interval_primary">8</integer> <integer name="gatt_high_priority_latency_primary">45</integer> <integer name="gatt_balanced_priority_min_interval_primary">6</integer> <integer name="gatt_balanced_priority_max_interval_primary">10</integer> <integer name="gatt_balanced_priority_latency_primary">120</integer> <integer name="gatt_low_power_min_interval_primary">8</integer> <integer name="gatt_low_power_max_interval_primary">10</integer> <integer name="gatt_low_power_latency_primary">150</integer> <!-- Secondary companion --> <integer name="gatt_high_priority_min_interval_secondary">6</integer> <integer name="gatt_high_priority_max_interval_secondary">6</integer> <integer name="gatt_high_priority_latency_secondary">0</integer> <integer name="gatt_balanced_priority_min_interval_secondary">12</integer> <integer name="gatt_balanced_priority_max_interval_secondary">12</integer> <integer name="gatt_balanced_priority_latency_secondary">30</integer> <integer name="gatt_low_power_min_interval_secondary">80</integer> <integer name="gatt_low_power_max_interval_secondary">100</integer> <integer name="gatt_low_power_latency_secondary">15</integer> <!-- ============================================================ --> <!-- Specifies latency parameters for high priority, balanced and low power <!-- Specifies latency parameters for high priority, balanced and low power GATT configurations. These values represents the number of packets a GATT configurations. These values represents the number of packets a peripheral device is allowed to skip. --> peripheral device is allowed to skip. --> Loading android/app/src/com/android/bluetooth/btservice/AdapterService.java +50 −0 Original line number Original line Diff line number Diff line Loading @@ -300,6 +300,7 @@ public class AdapterService extends Service { private ActiveDeviceManager mActiveDeviceManager; private ActiveDeviceManager mActiveDeviceManager; private DatabaseManager mDatabaseManager; private DatabaseManager mDatabaseManager; private SilenceDeviceManager mSilenceDeviceManager; private SilenceDeviceManager mSilenceDeviceManager; private CompanionManager mBtCompanionManager; private AppOpsManager mAppOps; private AppOpsManager mAppOps; private BluetoothSocketManagerBinder mBluetoothSocketManagerBinder; private BluetoothSocketManagerBinder mBluetoothSocketManagerBinder; Loading Loading @@ -441,6 +442,7 @@ public class AdapterService extends Service { getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE); getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE); getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_DYNAMIC_AUDIO_BUFFER); getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_DYNAMIC_AUDIO_BUFFER); mAdapterStateMachine.sendMessage(AdapterState.BREDR_STARTED); mAdapterStateMachine.sendMessage(AdapterState.BREDR_STARTED); mBtCompanionManager.loadCompanionInfo(); } } break; break; case BluetoothAdapter.STATE_OFF: case BluetoothAdapter.STATE_OFF: Loading Loading @@ -542,6 +544,8 @@ public class AdapterService extends Service { Looper.getMainLooper()); Looper.getMainLooper()); mSilenceDeviceManager.start(); mSilenceDeviceManager.start(); mBtCompanionManager = new CompanionManager(this, new ServiceFactory()); mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this); mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this); mActivityAttributionService = new ActivityAttributionService(); mActivityAttributionService = new ActivityAttributionService(); Loading Loading @@ -1556,6 +1560,32 @@ public class AdapterService extends Service { return !mCleaningUp; return !mCleaningUp; } } /** * Get an metadata of given device and key * * @param device Bluetooth device * @param key Metadata key * @param value Metadata value * @return if metadata is set successfully */ public boolean setMetadata(BluetoothDevice device, int key, byte[] value) { if (value == null || value.length > BluetoothDevice.METADATA_MAX_LENGTH) { return false; } return mDatabaseManager.setCustomMeta(device, key, value); } /** * Get an metadata of given device and key * * @param device Bluetooth device * @param key Metadata key * @return value of given device and key combination */ public byte[] getMetadata(BluetoothDevice device, int key) { return mDatabaseManager.getCustomMeta(device, key); } /** /** * Handlers for incoming service calls * Handlers for incoming service calls */ */ Loading Loading @@ -3171,6 +3201,10 @@ public class AdapterService extends Service { service.mBluetoothKeystoreService.factoryReset(); service.mBluetoothKeystoreService.factoryReset(); } } if (service.mBtCompanionManager != null) { service.mBtCompanionManager.factoryReset(); } return service.factoryResetNative(); return service.factoryResetNative(); } } Loading Loading @@ -5458,6 +5492,22 @@ public class AdapterService extends Service { return getMetricIdNative(Utils.getByteAddress(device)); return getMetricIdNative(Utils.getByteAddress(device)); } } public CompanionManager getCompanionManager() { return mBtCompanionManager; } /** * Call for the AdapterService receives bond state change * * @param device Bluetooth device * @param state bond state */ public void onBondStateChanged(BluetoothDevice device, int state) { if (mBtCompanionManager != null) { mBtCompanionManager.onBondStateChanged(device, state); } } /** /** * Allow audio low latency * Allow audio low latency * * Loading android/app/src/com/android/bluetooth/btservice/BondStateMachine.java +1 −0 Original line number Original line Diff line number Diff line Loading @@ -470,6 +470,7 @@ final class BondStateMachine extends StateMachine { if (newState == BluetoothDevice.BOND_NONE) { if (newState == BluetoothDevice.BOND_NONE) { intent.putExtra(BluetoothDevice.EXTRA_UNBOND_REASON, reason); intent.putExtra(BluetoothDevice.EXTRA_UNBOND_REASON, reason); } } mAdapterService.onBondStateChanged(device, newState); mAdapterService.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT, mAdapterService.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions()); Utils.getTempAllowlistBroadcastOptions()); infoLog("Bond State Change Intent:" + device + " " + state2str(oldState) + " => " infoLog("Bond State Change Intent:" + device + " " + state2str(oldState) + " => " Loading android/app/src/com/android/bluetooth/btservice/CompanionManager.java 0 → 100644 +370 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2022 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.bluetooth.btservice; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.VisibleForTesting; import com.android.bluetooth.R; import java.util.HashSet; import java.util.Set; /** A CompanionManager to specify parameters between companion devices and regular devices. 1. A paired device is recognized as a companion device if its METADATA_SOFTWARE_VERSION is set to BluetoothDevice.COMPANION_TYPE_PRIMARY or BluetoothDevice.COMPANION_TYPE_SECONDARY. 2. Only can have one companion device at a time. 3. Remove bond does not remove the companion device record. 4. Factory reset Bluetooth removes the companion device. 5. Companion device has individual GATT connection parameters. */ public class CompanionManager { private static final String TAG = "BluetoothCompanionManager"; private BluetoothDevice mCompanionDevice; private int mCompanionType; private final int[] mGattConnHighPrimary; private final int[] mGattConnBalancePrimary; private final int[] mGattConnLowPrimary; private final int[] mGattConnHighSecondary; private final int[] mGattConnBalanceSecondary; private final int[] mGattConnLowSecondary; private final int[] mGattConnHighDefault; private final int[] mGattConnBalanceDefault; private final int[] mGattConnLowDefault; @VisibleForTesting static final int COMPANION_TYPE_NONE = 0; @VisibleForTesting static final int COMPANION_TYPE_PRIMARY = 1; @VisibleForTesting static final int COMPANION_TYPE_SECONDARY = 2; public static final int GATT_CONN_INTERVAL_MIN = 0; public static final int GATT_CONN_INTERVAL_MAX = 1; public static final int GATT_CONN_LATENCY = 2; @VisibleForTesting static final String COMPANION_INFO = "bluetooth_companion_info"; @VisibleForTesting static final String COMPANION_DEVICE_KEY = "companion_device"; @VisibleForTesting static final String COMPANION_TYPE_KEY = "companion_type"; private final AdapterService mAdapterService; private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); private final Set<BluetoothDevice> mMetadataListeningDevices = new HashSet<>(); public CompanionManager(AdapterService service, ServiceFactory factory) { mAdapterService = service; mGattConnHighDefault = new int[] { service.getResources().getInteger(R.integer.gatt_high_priority_min_interval), service.getResources().getInteger(R.integer.gatt_high_priority_max_interval), service.getResources().getInteger(R.integer.gatt_high_priority_latency)}; mGattConnBalanceDefault = new int[] { service.getResources().getInteger(R.integer.gatt_balanced_priority_min_interval), service.getResources().getInteger(R.integer.gatt_balanced_priority_max_interval), service.getResources().getInteger(R.integer.gatt_balanced_priority_latency)}; mGattConnLowDefault = new int[] { service.getResources().getInteger(R.integer.gatt_low_power_min_interval), service.getResources().getInteger(R.integer.gatt_low_power_max_interval), service.getResources().getInteger(R.integer.gatt_low_power_latency)}; mGattConnHighPrimary = new int[] { service.getResources().getInteger( R.integer.gatt_high_priority_min_interval_primary), service.getResources().getInteger( R.integer.gatt_high_priority_max_interval_primary), service.getResources().getInteger( R.integer.gatt_high_priority_latency_primary)}; mGattConnBalancePrimary = new int[] { service.getResources().getInteger( R.integer.gatt_balanced_priority_min_interval_primary), service.getResources().getInteger( R.integer.gatt_balanced_priority_max_interval_primary), service.getResources().getInteger( R.integer.gatt_balanced_priority_latency_primary)}; mGattConnLowPrimary = new int[] { service.getResources().getInteger(R.integer.gatt_low_power_min_interval_primary), service.getResources().getInteger(R.integer.gatt_low_power_max_interval_primary), service.getResources().getInteger(R.integer.gatt_low_power_latency_primary)}; mGattConnHighSecondary = new int[] { service.getResources().getInteger( R.integer.gatt_high_priority_min_interval_secondary), service.getResources().getInteger( R.integer.gatt_high_priority_max_interval_secondary), service.getResources().getInteger(R.integer.gatt_high_priority_latency_secondary)}; mGattConnBalanceSecondary = new int[] { service.getResources().getInteger( R.integer.gatt_balanced_priority_min_interval_secondary), service.getResources().getInteger( R.integer.gatt_balanced_priority_max_interval_secondary), service.getResources().getInteger( R.integer.gatt_balanced_priority_latency_secondary)}; mGattConnLowSecondary = new int[] { service.getResources().getInteger(R.integer.gatt_low_power_min_interval_secondary), service.getResources().getInteger(R.integer.gatt_low_power_max_interval_secondary), service.getResources().getInteger(R.integer.gatt_low_power_latency_secondary)}; } void loadCompanionInfo() { synchronized (mMetadataListeningDevices) { String address = getCompanionPreferences().getString(COMPANION_DEVICE_KEY, ""); try { mCompanionDevice = mAdapter.getRemoteDevice(address); mCompanionType = getCompanionPreferences().getInt( COMPANION_TYPE_KEY, COMPANION_TYPE_NONE); } catch (IllegalArgumentException e) { mCompanionDevice = null; mCompanionType = COMPANION_TYPE_NONE; } } if (mCompanionDevice == null) { // We don't have any companion phone registered, try look from the bonded devices for (BluetoothDevice device : mAdapter.getBondedDevices()) { byte[] metadata = mAdapterService.getMetadata(device, BluetoothDevice.METADATA_SOFTWARE_VERSION); if (metadata == null) { continue; } String valueStr = new String(metadata); if ((valueStr.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY) || valueStr.equals(BluetoothDevice.COMPANION_TYPE_SECONDARY))) { // found the companion device, store and unregister all listeners Log.i(TAG, "Found companion device from the database!"); setCompanionDevice(device, valueStr); break; } registerMetadataListener(device); } } Log.i(TAG, "Companion device is " + mCompanionDevice + ", type=" + mCompanionType); } final BluetoothAdapter.OnMetadataChangedListener mMetadataListener = new BluetoothAdapter.OnMetadataChangedListener() { @Override public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) { String valueStr = new String(value); Log.d(TAG, String.format("Metadata updated in Device %s: %d = %s.", device, key, value == null ? null : valueStr)); if (key == BluetoothDevice.METADATA_SOFTWARE_VERSION && (valueStr.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY) || valueStr.equals(BluetoothDevice.COMPANION_TYPE_SECONDARY))) { setCompanionDevice(device, valueStr); } } }; private void setCompanionDevice(BluetoothDevice companionDevice, String type) { synchronized (mMetadataListeningDevices) { Log.i(TAG, "setCompanionDevice: " + companionDevice + ", type=" + type); mCompanionDevice = companionDevice; mCompanionType = type.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY) ? COMPANION_TYPE_PRIMARY : COMPANION_TYPE_SECONDARY; // unregister all metadata listeners for (BluetoothDevice device : mMetadataListeningDevices) { try { mAdapter.removeOnMetadataChangedListener(device, mMetadataListener); } catch (IllegalArgumentException e) { Log.e(TAG, "failed to unregister metadata listener for " + device + " " + e); } } mMetadataListeningDevices.clear(); SharedPreferences.Editor pref = getCompanionPreferences().edit(); pref.putString(COMPANION_DEVICE_KEY, mCompanionDevice.getAddress()); pref.putInt(COMPANION_TYPE_KEY, mCompanionType); pref.apply(); } } private SharedPreferences getCompanionPreferences() { return mAdapterService.getSharedPreferences(COMPANION_INFO, Context.MODE_PRIVATE); } /** * Bond state change event from the AdapterService * * @param device the Bluetooth device * @param state the new Bluetooth bond state of the device */ public void onBondStateChanged(BluetoothDevice device, int state) { synchronized (mMetadataListeningDevices) { if (mCompanionDevice != null) { // We already have the companion device, do not care bond state change any more. return; } switch (state) { case BluetoothDevice.BOND_BONDING: registerMetadataListener(device); break; case BluetoothDevice.BOND_NONE: removeMetadataListener(device); break; default: break; } } } private void registerMetadataListener(BluetoothDevice device) { synchronized (mMetadataListeningDevices) { Log.d(TAG, "register metadata listener: " + device); try { mAdapter.addOnMetadataChangedListener( device, mAdapterService.getMainExecutor(), mMetadataListener); } catch (IllegalArgumentException e) { Log.e(TAG, "failed to register metadata listener for " + device + " " + e); } mMetadataListeningDevices.add(device); } } private void removeMetadataListener(BluetoothDevice device) { synchronized (mMetadataListeningDevices) { if (!mMetadataListeningDevices.contains(device)) return; Log.d(TAG, "remove metadata listener: " + device); try { mAdapter.removeOnMetadataChangedListener(device, mMetadataListener); } catch (IllegalArgumentException e) { Log.e(TAG, "failed to unregister metadata listener for " + device + " " + e); } mMetadataListeningDevices.remove(device); } } /** * Method to get the stored companion device * * @return the companion Bluetooth device */ public BluetoothDevice getCompanionDevice() { return mCompanionDevice; } /** * Method to check whether it is a companion device * * @param address the address of the device * @return true if the address is a companion device, otherwise false */ public boolean isCompanionDevice(String address) { try { return isCompanionDevice(mAdapter.getRemoteDevice(address)); } catch (IllegalArgumentException e) { return false; } } /** * Method to check whether it is a companion device * * @param device the Bluetooth device * @return true if the device is a companion device, otherwise false */ public boolean isCompanionDevice(BluetoothDevice device) { if (device == null) return false; return device.equals(mCompanionDevice); } /** * Method to reset the stored companion info */ public void factoryReset() { synchronized (mMetadataListeningDevices) { mCompanionDevice = null; mCompanionType = COMPANION_TYPE_NONE; SharedPreferences.Editor pref = getCompanionPreferences().edit(); pref.remove(COMPANION_DEVICE_KEY); pref.remove(COMPANION_TYPE_KEY); pref.apply(); } } /** * Gets the GATT connection parameters of the device * * @param address the address of the Bluetooth device * @param type type of the parameter, can be GATT_CONN_INTERVAL_MIN, GATT_CONN_INTERVAL_MAX * or GATT_CONN_LATENCY * @param priority the priority of the connection, can be * BluetoothGatt.CONNECTION_PRIORITY_HIGH, BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER or * BluetoothGatt.CONNECTION_PRIORITY_BALANCED * @return the connection parameter in integer */ public int getGattConnParameters(String address, int type, int priority) { int companionType = isCompanionDevice(address) ? mCompanionType : COMPANION_TYPE_NONE; int parameter; switch (companionType) { case COMPANION_TYPE_PRIMARY: parameter = getGattConnParameterPrimary(type, priority); break; case COMPANION_TYPE_SECONDARY: parameter = getGattConnParameterSecondary(type, priority); break; default: parameter = getGattConnParameterDefault(type, priority); break; } return parameter; } private int getGattConnParameterPrimary(int type, int priority) { switch (priority) { case BluetoothGatt.CONNECTION_PRIORITY_HIGH: return mGattConnHighPrimary[type]; case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER: return mGattConnLowPrimary[type]; } return mGattConnBalancePrimary[type]; } private int getGattConnParameterSecondary(int type, int priority) { switch (priority) { case BluetoothGatt.CONNECTION_PRIORITY_HIGH: return mGattConnHighSecondary[type]; case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER: return mGattConnLowSecondary[type]; } return mGattConnBalanceSecondary[type]; } private int getGattConnParameterDefault(int type, int mode) { switch (mode) { case BluetoothGatt.CONNECTION_PRIORITY_HIGH: return mGattConnHighDefault[type]; case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER: return mGattConnLowDefault[type]; } return mGattConnBalanceDefault[type]; } } android/app/src/com/android/bluetooth/gatt/GattService.java +19 −33 Original line number Original line Diff line number Diff line Loading @@ -79,6 +79,7 @@ import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AbstractionLayer; import com.android.bluetooth.btservice.AbstractionLayer; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.BluetoothAdapterProxy; import com.android.bluetooth.btservice.BluetoothAdapterProxy; import com.android.bluetooth.btservice.CompanionManager; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.util.NumberUtils; import com.android.bluetooth.util.NumberUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting; Loading Loading @@ -3865,33 +3866,21 @@ public class GattService extends ProfileService { // Link supervision timeout is measured in N * 10ms // Link supervision timeout is measured in N * 10ms int timeout = 500; // 5s int timeout = 500; // 5s switch (connectionPriority) { case BluetoothGatt.CONNECTION_PRIORITY_HIGH: minInterval = getResources().getInteger(R.integer.gatt_high_priority_min_interval); maxInterval = getResources().getInteger(R.integer.gatt_high_priority_max_interval); latency = getResources().getInteger(R.integer.gatt_high_priority_latency); break; case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER: CompanionManager manager = minInterval = getResources().getInteger(R.integer.gatt_low_power_min_interval); AdapterService.getAdapterService().getCompanionManager(); maxInterval = getResources().getInteger(R.integer.gatt_low_power_max_interval); latency = getResources().getInteger(R.integer.gatt_low_power_latency); break; default: minInterval = manager.getGattConnParameters( // Using the values for CONNECTION_PRIORITY_BALANCED. address, CompanionManager.GATT_CONN_INTERVAL_MIN, connectionPriority); minInterval = maxInterval = manager.getGattConnParameters( getResources().getInteger(R.integer.gatt_balanced_priority_min_interval); address, CompanionManager.GATT_CONN_INTERVAL_MAX, connectionPriority); maxInterval = latency = manager.getGattConnParameters( getResources().getInteger(R.integer.gatt_balanced_priority_max_interval); address, CompanionManager.GATT_CONN_LATENCY, connectionPriority); latency = getResources().getInteger(R.integer.gatt_balanced_priority_latency); break; } if (DBG) { Log.d(TAG, "connectionParameterUpdate() - address=" + address + " params=" Log.d(TAG, "connectionParameterUpdate() - address=" + address + " params=" + connectionPriority + " interval=" + minInterval + "/" + maxInterval); + connectionPriority + " interval=" + minInterval + "/" + maxInterval } + " timeout=" + timeout); gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, latency, gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, latency, timeout, 0, 0); timeout, 0, 0); } } Loading @@ -3906,14 +3895,11 @@ public class GattService extends ProfileService { return; return; } } if (DBG) { Log.d(TAG, "leConnectionUpdate() - address=" + address + ", intervals=" Log.d(TAG, "leConnectionUpdate() - address=" + address + ", intervals=" + minInterval + "/" + maxInterval + ", latency=" + peripheralLatency + minInterval + "/" + maxInterval + ", latency=" + peripheralLatency + ", timeout=" + supervisionTimeout + "msec" + ", min_ce=" + ", timeout=" + supervisionTimeout + "msec" + ", min_ce=" + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen); + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen); } gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, peripheralLatency, supervisionTimeout, peripheralLatency, supervisionTimeout, minConnectionEventLen, maxConnectionEventLen); minConnectionEventLen, maxConnectionEventLen); Loading Loading
android/app/res/values/config.xml +28 −0 Original line number Original line Diff line number Diff line Loading @@ -33,6 +33,34 @@ <integer name="gatt_low_power_min_interval">80</integer> <integer name="gatt_low_power_min_interval">80</integer> <integer name="gatt_low_power_max_interval">100</integer> <integer name="gatt_low_power_max_interval">100</integer> <!-- min/max connection intervals/latencies for companion devices --> <!-- Primary companion --> <integer name="gatt_high_priority_min_interval_primary">6</integer> <integer name="gatt_high_priority_max_interval_primary">8</integer> <integer name="gatt_high_priority_latency_primary">45</integer> <integer name="gatt_balanced_priority_min_interval_primary">6</integer> <integer name="gatt_balanced_priority_max_interval_primary">10</integer> <integer name="gatt_balanced_priority_latency_primary">120</integer> <integer name="gatt_low_power_min_interval_primary">8</integer> <integer name="gatt_low_power_max_interval_primary">10</integer> <integer name="gatt_low_power_latency_primary">150</integer> <!-- Secondary companion --> <integer name="gatt_high_priority_min_interval_secondary">6</integer> <integer name="gatt_high_priority_max_interval_secondary">6</integer> <integer name="gatt_high_priority_latency_secondary">0</integer> <integer name="gatt_balanced_priority_min_interval_secondary">12</integer> <integer name="gatt_balanced_priority_max_interval_secondary">12</integer> <integer name="gatt_balanced_priority_latency_secondary">30</integer> <integer name="gatt_low_power_min_interval_secondary">80</integer> <integer name="gatt_low_power_max_interval_secondary">100</integer> <integer name="gatt_low_power_latency_secondary">15</integer> <!-- ============================================================ --> <!-- Specifies latency parameters for high priority, balanced and low power <!-- Specifies latency parameters for high priority, balanced and low power GATT configurations. These values represents the number of packets a GATT configurations. These values represents the number of packets a peripheral device is allowed to skip. --> peripheral device is allowed to skip. --> Loading
android/app/src/com/android/bluetooth/btservice/AdapterService.java +50 −0 Original line number Original line Diff line number Diff line Loading @@ -300,6 +300,7 @@ public class AdapterService extends Service { private ActiveDeviceManager mActiveDeviceManager; private ActiveDeviceManager mActiveDeviceManager; private DatabaseManager mDatabaseManager; private DatabaseManager mDatabaseManager; private SilenceDeviceManager mSilenceDeviceManager; private SilenceDeviceManager mSilenceDeviceManager; private CompanionManager mBtCompanionManager; private AppOpsManager mAppOps; private AppOpsManager mAppOps; private BluetoothSocketManagerBinder mBluetoothSocketManagerBinder; private BluetoothSocketManagerBinder mBluetoothSocketManagerBinder; Loading Loading @@ -441,6 +442,7 @@ public class AdapterService extends Service { getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE); getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE); getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_DYNAMIC_AUDIO_BUFFER); getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_DYNAMIC_AUDIO_BUFFER); mAdapterStateMachine.sendMessage(AdapterState.BREDR_STARTED); mAdapterStateMachine.sendMessage(AdapterState.BREDR_STARTED); mBtCompanionManager.loadCompanionInfo(); } } break; break; case BluetoothAdapter.STATE_OFF: case BluetoothAdapter.STATE_OFF: Loading Loading @@ -542,6 +544,8 @@ public class AdapterService extends Service { Looper.getMainLooper()); Looper.getMainLooper()); mSilenceDeviceManager.start(); mSilenceDeviceManager.start(); mBtCompanionManager = new CompanionManager(this, new ServiceFactory()); mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this); mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this); mActivityAttributionService = new ActivityAttributionService(); mActivityAttributionService = new ActivityAttributionService(); Loading Loading @@ -1556,6 +1560,32 @@ public class AdapterService extends Service { return !mCleaningUp; return !mCleaningUp; } } /** * Get an metadata of given device and key * * @param device Bluetooth device * @param key Metadata key * @param value Metadata value * @return if metadata is set successfully */ public boolean setMetadata(BluetoothDevice device, int key, byte[] value) { if (value == null || value.length > BluetoothDevice.METADATA_MAX_LENGTH) { return false; } return mDatabaseManager.setCustomMeta(device, key, value); } /** * Get an metadata of given device and key * * @param device Bluetooth device * @param key Metadata key * @return value of given device and key combination */ public byte[] getMetadata(BluetoothDevice device, int key) { return mDatabaseManager.getCustomMeta(device, key); } /** /** * Handlers for incoming service calls * Handlers for incoming service calls */ */ Loading Loading @@ -3171,6 +3201,10 @@ public class AdapterService extends Service { service.mBluetoothKeystoreService.factoryReset(); service.mBluetoothKeystoreService.factoryReset(); } } if (service.mBtCompanionManager != null) { service.mBtCompanionManager.factoryReset(); } return service.factoryResetNative(); return service.factoryResetNative(); } } Loading Loading @@ -5458,6 +5492,22 @@ public class AdapterService extends Service { return getMetricIdNative(Utils.getByteAddress(device)); return getMetricIdNative(Utils.getByteAddress(device)); } } public CompanionManager getCompanionManager() { return mBtCompanionManager; } /** * Call for the AdapterService receives bond state change * * @param device Bluetooth device * @param state bond state */ public void onBondStateChanged(BluetoothDevice device, int state) { if (mBtCompanionManager != null) { mBtCompanionManager.onBondStateChanged(device, state); } } /** /** * Allow audio low latency * Allow audio low latency * * Loading
android/app/src/com/android/bluetooth/btservice/BondStateMachine.java +1 −0 Original line number Original line Diff line number Diff line Loading @@ -470,6 +470,7 @@ final class BondStateMachine extends StateMachine { if (newState == BluetoothDevice.BOND_NONE) { if (newState == BluetoothDevice.BOND_NONE) { intent.putExtra(BluetoothDevice.EXTRA_UNBOND_REASON, reason); intent.putExtra(BluetoothDevice.EXTRA_UNBOND_REASON, reason); } } mAdapterService.onBondStateChanged(device, newState); mAdapterService.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT, mAdapterService.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions()); Utils.getTempAllowlistBroadcastOptions()); infoLog("Bond State Change Intent:" + device + " " + state2str(oldState) + " => " infoLog("Bond State Change Intent:" + device + " " + state2str(oldState) + " => " Loading
android/app/src/com/android/bluetooth/btservice/CompanionManager.java 0 → 100644 +370 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2022 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.bluetooth.btservice; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.VisibleForTesting; import com.android.bluetooth.R; import java.util.HashSet; import java.util.Set; /** A CompanionManager to specify parameters between companion devices and regular devices. 1. A paired device is recognized as a companion device if its METADATA_SOFTWARE_VERSION is set to BluetoothDevice.COMPANION_TYPE_PRIMARY or BluetoothDevice.COMPANION_TYPE_SECONDARY. 2. Only can have one companion device at a time. 3. Remove bond does not remove the companion device record. 4. Factory reset Bluetooth removes the companion device. 5. Companion device has individual GATT connection parameters. */ public class CompanionManager { private static final String TAG = "BluetoothCompanionManager"; private BluetoothDevice mCompanionDevice; private int mCompanionType; private final int[] mGattConnHighPrimary; private final int[] mGattConnBalancePrimary; private final int[] mGattConnLowPrimary; private final int[] mGattConnHighSecondary; private final int[] mGattConnBalanceSecondary; private final int[] mGattConnLowSecondary; private final int[] mGattConnHighDefault; private final int[] mGattConnBalanceDefault; private final int[] mGattConnLowDefault; @VisibleForTesting static final int COMPANION_TYPE_NONE = 0; @VisibleForTesting static final int COMPANION_TYPE_PRIMARY = 1; @VisibleForTesting static final int COMPANION_TYPE_SECONDARY = 2; public static final int GATT_CONN_INTERVAL_MIN = 0; public static final int GATT_CONN_INTERVAL_MAX = 1; public static final int GATT_CONN_LATENCY = 2; @VisibleForTesting static final String COMPANION_INFO = "bluetooth_companion_info"; @VisibleForTesting static final String COMPANION_DEVICE_KEY = "companion_device"; @VisibleForTesting static final String COMPANION_TYPE_KEY = "companion_type"; private final AdapterService mAdapterService; private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter(); private final Set<BluetoothDevice> mMetadataListeningDevices = new HashSet<>(); public CompanionManager(AdapterService service, ServiceFactory factory) { mAdapterService = service; mGattConnHighDefault = new int[] { service.getResources().getInteger(R.integer.gatt_high_priority_min_interval), service.getResources().getInteger(R.integer.gatt_high_priority_max_interval), service.getResources().getInteger(R.integer.gatt_high_priority_latency)}; mGattConnBalanceDefault = new int[] { service.getResources().getInteger(R.integer.gatt_balanced_priority_min_interval), service.getResources().getInteger(R.integer.gatt_balanced_priority_max_interval), service.getResources().getInteger(R.integer.gatt_balanced_priority_latency)}; mGattConnLowDefault = new int[] { service.getResources().getInteger(R.integer.gatt_low_power_min_interval), service.getResources().getInteger(R.integer.gatt_low_power_max_interval), service.getResources().getInteger(R.integer.gatt_low_power_latency)}; mGattConnHighPrimary = new int[] { service.getResources().getInteger( R.integer.gatt_high_priority_min_interval_primary), service.getResources().getInteger( R.integer.gatt_high_priority_max_interval_primary), service.getResources().getInteger( R.integer.gatt_high_priority_latency_primary)}; mGattConnBalancePrimary = new int[] { service.getResources().getInteger( R.integer.gatt_balanced_priority_min_interval_primary), service.getResources().getInteger( R.integer.gatt_balanced_priority_max_interval_primary), service.getResources().getInteger( R.integer.gatt_balanced_priority_latency_primary)}; mGattConnLowPrimary = new int[] { service.getResources().getInteger(R.integer.gatt_low_power_min_interval_primary), service.getResources().getInteger(R.integer.gatt_low_power_max_interval_primary), service.getResources().getInteger(R.integer.gatt_low_power_latency_primary)}; mGattConnHighSecondary = new int[] { service.getResources().getInteger( R.integer.gatt_high_priority_min_interval_secondary), service.getResources().getInteger( R.integer.gatt_high_priority_max_interval_secondary), service.getResources().getInteger(R.integer.gatt_high_priority_latency_secondary)}; mGattConnBalanceSecondary = new int[] { service.getResources().getInteger( R.integer.gatt_balanced_priority_min_interval_secondary), service.getResources().getInteger( R.integer.gatt_balanced_priority_max_interval_secondary), service.getResources().getInteger( R.integer.gatt_balanced_priority_latency_secondary)}; mGattConnLowSecondary = new int[] { service.getResources().getInteger(R.integer.gatt_low_power_min_interval_secondary), service.getResources().getInteger(R.integer.gatt_low_power_max_interval_secondary), service.getResources().getInteger(R.integer.gatt_low_power_latency_secondary)}; } void loadCompanionInfo() { synchronized (mMetadataListeningDevices) { String address = getCompanionPreferences().getString(COMPANION_DEVICE_KEY, ""); try { mCompanionDevice = mAdapter.getRemoteDevice(address); mCompanionType = getCompanionPreferences().getInt( COMPANION_TYPE_KEY, COMPANION_TYPE_NONE); } catch (IllegalArgumentException e) { mCompanionDevice = null; mCompanionType = COMPANION_TYPE_NONE; } } if (mCompanionDevice == null) { // We don't have any companion phone registered, try look from the bonded devices for (BluetoothDevice device : mAdapter.getBondedDevices()) { byte[] metadata = mAdapterService.getMetadata(device, BluetoothDevice.METADATA_SOFTWARE_VERSION); if (metadata == null) { continue; } String valueStr = new String(metadata); if ((valueStr.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY) || valueStr.equals(BluetoothDevice.COMPANION_TYPE_SECONDARY))) { // found the companion device, store and unregister all listeners Log.i(TAG, "Found companion device from the database!"); setCompanionDevice(device, valueStr); break; } registerMetadataListener(device); } } Log.i(TAG, "Companion device is " + mCompanionDevice + ", type=" + mCompanionType); } final BluetoothAdapter.OnMetadataChangedListener mMetadataListener = new BluetoothAdapter.OnMetadataChangedListener() { @Override public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) { String valueStr = new String(value); Log.d(TAG, String.format("Metadata updated in Device %s: %d = %s.", device, key, value == null ? null : valueStr)); if (key == BluetoothDevice.METADATA_SOFTWARE_VERSION && (valueStr.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY) || valueStr.equals(BluetoothDevice.COMPANION_TYPE_SECONDARY))) { setCompanionDevice(device, valueStr); } } }; private void setCompanionDevice(BluetoothDevice companionDevice, String type) { synchronized (mMetadataListeningDevices) { Log.i(TAG, "setCompanionDevice: " + companionDevice + ", type=" + type); mCompanionDevice = companionDevice; mCompanionType = type.equals(BluetoothDevice.COMPANION_TYPE_PRIMARY) ? COMPANION_TYPE_PRIMARY : COMPANION_TYPE_SECONDARY; // unregister all metadata listeners for (BluetoothDevice device : mMetadataListeningDevices) { try { mAdapter.removeOnMetadataChangedListener(device, mMetadataListener); } catch (IllegalArgumentException e) { Log.e(TAG, "failed to unregister metadata listener for " + device + " " + e); } } mMetadataListeningDevices.clear(); SharedPreferences.Editor pref = getCompanionPreferences().edit(); pref.putString(COMPANION_DEVICE_KEY, mCompanionDevice.getAddress()); pref.putInt(COMPANION_TYPE_KEY, mCompanionType); pref.apply(); } } private SharedPreferences getCompanionPreferences() { return mAdapterService.getSharedPreferences(COMPANION_INFO, Context.MODE_PRIVATE); } /** * Bond state change event from the AdapterService * * @param device the Bluetooth device * @param state the new Bluetooth bond state of the device */ public void onBondStateChanged(BluetoothDevice device, int state) { synchronized (mMetadataListeningDevices) { if (mCompanionDevice != null) { // We already have the companion device, do not care bond state change any more. return; } switch (state) { case BluetoothDevice.BOND_BONDING: registerMetadataListener(device); break; case BluetoothDevice.BOND_NONE: removeMetadataListener(device); break; default: break; } } } private void registerMetadataListener(BluetoothDevice device) { synchronized (mMetadataListeningDevices) { Log.d(TAG, "register metadata listener: " + device); try { mAdapter.addOnMetadataChangedListener( device, mAdapterService.getMainExecutor(), mMetadataListener); } catch (IllegalArgumentException e) { Log.e(TAG, "failed to register metadata listener for " + device + " " + e); } mMetadataListeningDevices.add(device); } } private void removeMetadataListener(BluetoothDevice device) { synchronized (mMetadataListeningDevices) { if (!mMetadataListeningDevices.contains(device)) return; Log.d(TAG, "remove metadata listener: " + device); try { mAdapter.removeOnMetadataChangedListener(device, mMetadataListener); } catch (IllegalArgumentException e) { Log.e(TAG, "failed to unregister metadata listener for " + device + " " + e); } mMetadataListeningDevices.remove(device); } } /** * Method to get the stored companion device * * @return the companion Bluetooth device */ public BluetoothDevice getCompanionDevice() { return mCompanionDevice; } /** * Method to check whether it is a companion device * * @param address the address of the device * @return true if the address is a companion device, otherwise false */ public boolean isCompanionDevice(String address) { try { return isCompanionDevice(mAdapter.getRemoteDevice(address)); } catch (IllegalArgumentException e) { return false; } } /** * Method to check whether it is a companion device * * @param device the Bluetooth device * @return true if the device is a companion device, otherwise false */ public boolean isCompanionDevice(BluetoothDevice device) { if (device == null) return false; return device.equals(mCompanionDevice); } /** * Method to reset the stored companion info */ public void factoryReset() { synchronized (mMetadataListeningDevices) { mCompanionDevice = null; mCompanionType = COMPANION_TYPE_NONE; SharedPreferences.Editor pref = getCompanionPreferences().edit(); pref.remove(COMPANION_DEVICE_KEY); pref.remove(COMPANION_TYPE_KEY); pref.apply(); } } /** * Gets the GATT connection parameters of the device * * @param address the address of the Bluetooth device * @param type type of the parameter, can be GATT_CONN_INTERVAL_MIN, GATT_CONN_INTERVAL_MAX * or GATT_CONN_LATENCY * @param priority the priority of the connection, can be * BluetoothGatt.CONNECTION_PRIORITY_HIGH, BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER or * BluetoothGatt.CONNECTION_PRIORITY_BALANCED * @return the connection parameter in integer */ public int getGattConnParameters(String address, int type, int priority) { int companionType = isCompanionDevice(address) ? mCompanionType : COMPANION_TYPE_NONE; int parameter; switch (companionType) { case COMPANION_TYPE_PRIMARY: parameter = getGattConnParameterPrimary(type, priority); break; case COMPANION_TYPE_SECONDARY: parameter = getGattConnParameterSecondary(type, priority); break; default: parameter = getGattConnParameterDefault(type, priority); break; } return parameter; } private int getGattConnParameterPrimary(int type, int priority) { switch (priority) { case BluetoothGatt.CONNECTION_PRIORITY_HIGH: return mGattConnHighPrimary[type]; case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER: return mGattConnLowPrimary[type]; } return mGattConnBalancePrimary[type]; } private int getGattConnParameterSecondary(int type, int priority) { switch (priority) { case BluetoothGatt.CONNECTION_PRIORITY_HIGH: return mGattConnHighSecondary[type]; case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER: return mGattConnLowSecondary[type]; } return mGattConnBalanceSecondary[type]; } private int getGattConnParameterDefault(int type, int mode) { switch (mode) { case BluetoothGatt.CONNECTION_PRIORITY_HIGH: return mGattConnHighDefault[type]; case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER: return mGattConnLowDefault[type]; } return mGattConnBalanceDefault[type]; } }
android/app/src/com/android/bluetooth/gatt/GattService.java +19 −33 Original line number Original line Diff line number Diff line Loading @@ -79,6 +79,7 @@ import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AbstractionLayer; import com.android.bluetooth.btservice.AbstractionLayer; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.BluetoothAdapterProxy; import com.android.bluetooth.btservice.BluetoothAdapterProxy; import com.android.bluetooth.btservice.CompanionManager; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.util.NumberUtils; import com.android.bluetooth.util.NumberUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting; Loading Loading @@ -3865,33 +3866,21 @@ public class GattService extends ProfileService { // Link supervision timeout is measured in N * 10ms // Link supervision timeout is measured in N * 10ms int timeout = 500; // 5s int timeout = 500; // 5s switch (connectionPriority) { case BluetoothGatt.CONNECTION_PRIORITY_HIGH: minInterval = getResources().getInteger(R.integer.gatt_high_priority_min_interval); maxInterval = getResources().getInteger(R.integer.gatt_high_priority_max_interval); latency = getResources().getInteger(R.integer.gatt_high_priority_latency); break; case BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER: CompanionManager manager = minInterval = getResources().getInteger(R.integer.gatt_low_power_min_interval); AdapterService.getAdapterService().getCompanionManager(); maxInterval = getResources().getInteger(R.integer.gatt_low_power_max_interval); latency = getResources().getInteger(R.integer.gatt_low_power_latency); break; default: minInterval = manager.getGattConnParameters( // Using the values for CONNECTION_PRIORITY_BALANCED. address, CompanionManager.GATT_CONN_INTERVAL_MIN, connectionPriority); minInterval = maxInterval = manager.getGattConnParameters( getResources().getInteger(R.integer.gatt_balanced_priority_min_interval); address, CompanionManager.GATT_CONN_INTERVAL_MAX, connectionPriority); maxInterval = latency = manager.getGattConnParameters( getResources().getInteger(R.integer.gatt_balanced_priority_max_interval); address, CompanionManager.GATT_CONN_LATENCY, connectionPriority); latency = getResources().getInteger(R.integer.gatt_balanced_priority_latency); break; } if (DBG) { Log.d(TAG, "connectionParameterUpdate() - address=" + address + " params=" Log.d(TAG, "connectionParameterUpdate() - address=" + address + " params=" + connectionPriority + " interval=" + minInterval + "/" + maxInterval); + connectionPriority + " interval=" + minInterval + "/" + maxInterval } + " timeout=" + timeout); gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, latency, gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, latency, timeout, 0, 0); timeout, 0, 0); } } Loading @@ -3906,14 +3895,11 @@ public class GattService extends ProfileService { return; return; } } if (DBG) { Log.d(TAG, "leConnectionUpdate() - address=" + address + ", intervals=" Log.d(TAG, "leConnectionUpdate() - address=" + address + ", intervals=" + minInterval + "/" + maxInterval + ", latency=" + peripheralLatency + minInterval + "/" + maxInterval + ", latency=" + peripheralLatency + ", timeout=" + supervisionTimeout + "msec" + ", min_ce=" + ", timeout=" + supervisionTimeout + "msec" + ", min_ce=" + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen); + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen); } gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, peripheralLatency, supervisionTimeout, peripheralLatency, supervisionTimeout, minConnectionEventLen, maxConnectionEventLen); minConnectionEventLen, maxConnectionEventLen); Loading