Merge "Prefer LE Audio over ASHA if both devices support both profiles." into tm-qpr-dev
diff --git a/android/app/jni/com_android_bluetooth_hfpclient.cpp b/android/app/jni/com_android_bluetooth_hfpclient.cpp
index a36e19c..da4dff9 100644
--- a/android/app/jni/com_android_bluetooth_hfpclient.cpp
+++ b/android/app/jni/com_android_bluetooth_hfpclient.cpp
@@ -879,6 +879,37 @@
return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
}
+static jboolean sendAndroidAtNative(JNIEnv* env, jobject object,
+ jbyteArray address, jstring arg_str) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
+ if (!sBluetoothHfpClientInterface) return JNI_FALSE;
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ const char* arg = NULL;
+ if (arg_str != NULL) {
+ arg = env->GetStringUTFChars(arg_str, NULL);
+ }
+
+ bt_status_t status = sBluetoothHfpClientInterface->send_android_at(
+ (const RawAddress*)addr, arg);
+
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("FAILED to control volume, status: %d", status);
+ }
+
+ if (arg != NULL) {
+ env->ReleaseStringUTFChars(arg_str, arg);
+ }
+
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
static JNINativeMethod sMethods[] = {
{"classInitNative", "()V", (void*)classInitNative},
{"initializeNative", "()V", (void*)initializeNative},
@@ -903,6 +934,8 @@
{"requestLastVoiceTagNumberNative", "([B)Z",
(void*)requestLastVoiceTagNumberNative},
{"sendATCmdNative", "([BIIILjava/lang/String;)Z", (void*)sendATCmdNative},
+ {"sendAndroidAtNative", "([BLjava/lang/String;)Z",
+ (void*)sendAndroidAtNative},
};
int register_com_android_bluetooth_hfpclient(JNIEnv* env) {
diff --git a/android/app/res/values-ky/strings.xml b/android/app/res/values-ky/strings.xml
index 954556f..014f3c5 100644
--- a/android/app/res/values-ky/strings.xml
+++ b/android/app/res/values-ky/strings.xml
@@ -121,7 +121,7 @@
<string name="bluetooth_map_settings_intro" msgid="4748160773998753325">"Bluetooth аркылуу бөлүшө турган аккаунттарды тандаңыз. Туташкан сайын аккаунттарга кирүүгө уруксат берип турушуңуз керек."</string>
<string name="bluetooth_map_settings_count" msgid="183013143617807702">"Калган көзөнөктөр:"</string>
<string name="bluetooth_map_settings_app_icon" msgid="3501432663809664982">"Колдонмонун сүрөтчөсү"</string>
- <string name="bluetooth_map_settings_title" msgid="4226030082708590023">"Bluetooth билдирүү бөлүшүү жөндөөлөрү"</string>
+ <string name="bluetooth_map_settings_title" msgid="4226030082708590023">"Bluetooth билдирүү бөлүшүү параметрлери"</string>
<string name="bluetooth_map_settings_no_account_slots_left" msgid="755024228476065757">"Аккаунт тандалбай жатат: 0 орун калды"</string>
<string name="bluetooth_connected" msgid="5687474377090799447">"Bluetooth аудио туташты"</string>
<string name="bluetooth_disconnected" msgid="6841396291728343534">"Bluetooth аудио ажыратылды"</string>
diff --git a/android/app/res/values/config.xml b/android/app/res/values/config.xml
index 913c77f..1e062b5 100644
--- a/android/app/res/values/config.xml
+++ b/android/app/res/values/config.xml
@@ -33,6 +33,34 @@
<integer name="gatt_low_power_min_interval">80</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
GATT configurations. These values represents the number of packets a
peripheral device is allowed to skip. -->
diff --git a/android/app/res/values/styles.xml b/android/app/res/values/styles.xml
index 91f402d..ea94695 100644
--- a/android/app/res/values/styles.xml
+++ b/android/app/res/values/styles.xml
@@ -31,7 +31,7 @@
<item name="android:paddingTop">10dip</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:textAppearance">@android:style/TextAppearance.Material.Body1</item>
- <item name="android:textColor">@*android:color/secondary_text_default_material_light</item>
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
<style name="file_transfer_item_content">
@@ -42,7 +42,7 @@
<item name="android:paddingBottom">10dip</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:textAppearance">@android:style/TextAppearance.Material.Subhead</item>
- <item name="android:textColor">@*android:color/primary_text_default_material_light</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="dialog" parent="android:style/Theme.Material.Light.Dialog.Alert" />
diff --git a/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java b/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
index 1eebe99..97df3ce 100644
--- a/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
+++ b/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
@@ -31,6 +31,7 @@
import android.media.session.PlaybackState;
import android.os.Handler;
import android.os.Looper;
+import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
@@ -146,10 +147,45 @@
mContext.getMainExecutor(), mMediaKeyEventSessionChangedListener);
}
+ private void constructCurrentPlayers() {
+ // Construct the list of current players
+ d("Initializing list of current media players");
+ List<android.media.session.MediaController> controllers =
+ mMediaSessionManager.getActiveSessions(null);
+
+ for (android.media.session.MediaController controller : controllers) {
+ addMediaPlayer(controller);
+ }
+
+ // If there were any active players and we don't already have one due to the Media
+ // Framework Callbacks then set the highest priority one to active
+ if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) {
+ String packageName = mMediaSessionManager.getMediaKeyEventSessionPackageName();
+ if (!TextUtils.isEmpty(packageName) && haveMediaPlayer(packageName)) {
+ Log.i(TAG, "Set active player to MediaKeyEvent session = " + packageName);
+ setActivePlayer(mMediaPlayerIds.get(packageName));
+ } else {
+ Log.i(TAG, "Set active player to first default");
+ setActivePlayer(1);
+ }
+ }
+ }
+
public void init(MediaUpdateCallback callback) {
Log.v(TAG, "Initializing MediaPlayerList");
mCallback = callback;
+ if (!SystemProperties.getBoolean("bluetooth.avrcp.browsable_media_player.enabled", true)) {
+ // Allow to disable BrowsablePlayerConnector with systemproperties.
+ // This is useful when for watches because:
+ // 1. It is not a regular use case
+ // 2. Registering to all players is a very loading task
+
+ Log.i(TAG, "init: without Browsable Player");
+ constructCurrentPlayers();
+ return;
+ }
+
// Build the list of browsable players and afterwards, build the list of media players
Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE);
List<ResolveInfo> playerList =
@@ -185,27 +221,7 @@
});
}
- // Construct the list of current players
- d("Initializing list of current media players");
- List<android.media.session.MediaController> controllers =
- mMediaSessionManager.getActiveSessions(null);
-
- for (android.media.session.MediaController controller : controllers) {
- addMediaPlayer(controller);
- }
-
- // If there were any active players and we don't already have one due to the Media
- // Framework Callbacks then set the highest priority one to active
- if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) {
- String packageName = mMediaSessionManager.getMediaKeyEventSessionPackageName();
- if (!TextUtils.isEmpty(packageName) && haveMediaPlayer(packageName)) {
- Log.i(TAG, "Set active player to MediaKeyEvent session = " + packageName);
- setActivePlayer(mMediaPlayerIds.get(packageName));
- } else {
- Log.i(TAG, "Set active player to first default");
- setActivePlayer(1);
- }
- }
+ constructCurrentPlayers();
});
}
diff --git a/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java b/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
index f043394..127fa31 100644
--- a/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
+++ b/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
@@ -20,6 +20,7 @@
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHeadset;
@@ -689,10 +690,14 @@
if (headsetService == null) {
return;
}
- if (!headsetService.setActiveDevice(device)) {
- return;
+ BluetoothAudioPolicy audioPolicy = headsetService.getHfpCallAudioPolicy(device);
+ if (audioPolicy == null || audioPolicy.getConnectingTimePolicy()
+ != BluetoothAudioPolicy.POLICY_NOT_ALLOWED) {
+ if (!headsetService.setActiveDevice(device)) {
+ return;
+ }
+ mHfpActiveDevice = device;
}
- mHfpActiveDevice = device;
}
private void setHearingAidActiveDevice(BluetoothDevice device) {
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index c09e164..9d9f887 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -42,6 +42,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAdapter.ActiveDeviceProfile;
import android.bluetooth.BluetoothAdapter.ActiveDeviceUse;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothFrameworkInitializer;
@@ -300,6 +301,7 @@
private ActiveDeviceManager mActiveDeviceManager;
private DatabaseManager mDatabaseManager;
private SilenceDeviceManager mSilenceDeviceManager;
+ private CompanionManager mBtCompanionManager;
private AppOpsManager mAppOps;
private BluetoothSocketManagerBinder mBluetoothSocketManagerBinder;
@@ -441,6 +443,7 @@
getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE);
getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_DYNAMIC_AUDIO_BUFFER);
mAdapterStateMachine.sendMessage(AdapterState.BREDR_STARTED);
+ mBtCompanionManager.loadCompanionInfo();
}
break;
case BluetoothAdapter.STATE_OFF:
@@ -542,6 +545,8 @@
Looper.getMainLooper());
mSilenceDeviceManager.start();
+ mBtCompanionManager = new CompanionManager(this, new ServiceFactory());
+
mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this);
mActivityAttributionService = new ActivityAttributionService();
@@ -1557,6 +1562,32 @@
}
/**
+ * 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
*/
private AdapterServiceBinder mBinder;
@@ -3171,6 +3202,10 @@
service.mBluetoothKeystoreService.factoryReset();
}
+ if (service.mBtCompanionManager != null) {
+ service.mBtCompanionManager.factoryReset();
+ }
+
return service.factoryResetNative();
}
@@ -3636,6 +3671,73 @@
}
@Override
+ public void getAudioPolicyRemoteSupported(BluetoothDevice device,
+ AttributionSource source, SynchronousResultReceiver receiver) {
+ try {
+ receiver.send(getAudioPolicyRemoteSupported(device, source));
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+ private int getAudioPolicyRemoteSupported(BluetoothDevice device,
+ AttributionSource source) {
+ AdapterService service = getService();
+ if (service == null
+ || !callerIsSystemOrActiveOrManagedUser(service, TAG,
+ "getAudioPolicyRemoteSupported")
+ || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) {
+ return BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE;
+ }
+ enforceBluetoothPrivilegedPermission(service);
+ return service.getAudioPolicyRemoteSupported(device);
+ }
+
+ @Override
+ public void setAudioPolicy(BluetoothDevice device, BluetoothAudioPolicy policies,
+ AttributionSource source, SynchronousResultReceiver receiver) {
+ try {
+ receiver.send(setAudioPolicy(device, policies, source));
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+ private int setAudioPolicy(BluetoothDevice device, BluetoothAudioPolicy policies,
+ AttributionSource source) {
+ AdapterService service = getService();
+ if (service == null) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ } else if (!callerIsSystemOrActiveOrManagedUser(service, TAG, "setAudioPolicy")) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED;
+ } else if (!Utils.checkConnectPermissionForDataDelivery(
+ service, source, TAG)) {
+ return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION;
+ }
+ enforceBluetoothPrivilegedPermission(service);
+ return service.setAudioPolicy(device, policies);
+ }
+
+ @Override
+ public void getAudioPolicy(BluetoothDevice device,
+ AttributionSource source, SynchronousResultReceiver receiver) {
+ try {
+ receiver.send(getAudioPolicy(device, source));
+ } catch (RuntimeException e) {
+ receiver.propagateException(e);
+ }
+ }
+ private BluetoothAudioPolicy getAudioPolicy(BluetoothDevice device,
+ AttributionSource source) {
+ AdapterService service = getService();
+ if (service == null
+ || !callerIsSystemOrActiveOrManagedUser(service, TAG, "getAudioPolicy")
+ || !Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) {
+ return null;
+ }
+ enforceBluetoothPrivilegedPermission(service);
+ return service.getAudioPolicy(device);
+ }
+
+ @Override
public void requestActivityInfo(IBluetoothActivityEnergyInfoListener listener,
AttributionSource source) {
BluetoothActivityEnergyInfo info = reportActivityInfo(source);
@@ -5458,6 +5560,84 @@
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);
+ }
+ }
+
+ /**
+ * Get audio policy feature support status
+ *
+ * @param device Bluetooth device to be checked for audio policy support
+ * @return int status of the remote support for audio policy feature
+ */
+ public int getAudioPolicyRemoteSupported(BluetoothDevice device) {
+ if (mHeadsetClientService != null) {
+ return mHeadsetClientService.getAudioPolicyRemoteSupported(device);
+ } else {
+ Log.e(TAG, "No audio transport connected");
+ return BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE;
+ }
+ }
+
+ /**
+ * Set audio policy for remote device
+ *
+ * @param device Bluetooth device to be set policy for
+ * @return int result status for setAudioPolicy API
+ */
+ public int setAudioPolicy(BluetoothDevice device, BluetoothAudioPolicy policies) {
+ DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(device);
+ if (deviceProp == null) {
+ return BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED;
+ }
+
+ if (mHeadsetClientService != null) {
+ if (getAudioPolicyRemoteSupported(device)
+ != BluetoothAudioPolicy.FEATURE_SUPPORTED_BY_REMOTE) {
+ Log.w(TAG, "Audio Policy feature not supported by AG");
+ return BluetoothStatusCodes.FEATURE_NOT_SUPPORTED;
+ }
+ deviceProp.setHfAudioPolicyForRemoteAg(policies);
+ mHeadsetClientService.setAudioPolicy(device, policies);
+ return BluetoothStatusCodes.SUCCESS;
+ } else {
+ Log.e(TAG, "HeadsetClient not connected");
+ return BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED;
+ }
+ }
+
+ /**
+ * Get audio policy for remote device
+ *
+ * @param device Bluetooth device to be set policy for
+ * @return {@link BluetoothAudioPolicy} policy stored for the device
+ */
+ public BluetoothAudioPolicy getAudioPolicy(BluetoothDevice device) {
+ DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(device);
+ if (deviceProp == null) {
+ return null;
+ }
+
+ if (mHeadsetClientService != null) {
+ return deviceProp.getHfAudioPolicyForRemoteAg();
+ } else {
+ Log.e(TAG, "HeadsetClient not connected");
+ return null;
+ }
+ }
+
/**
* Allow audio low latency
*
diff --git a/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java b/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java
index 713545f..c6633da 100644
--- a/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java
+++ b/android/app/src/com/android/bluetooth/btservice/BondStateMachine.java
@@ -470,6 +470,7 @@
if (newState == BluetoothDevice.BOND_NONE) {
intent.putExtra(BluetoothDevice.EXTRA_UNBOND_REASON, reason);
}
+ mAdapterService.onBondStateChanged(device, newState);
mAdapterService.sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT,
Utils.getTempAllowlistBroadcastOptions());
infoLog("Bond State Change Intent:" + device + " " + state2str(oldState) + " => "
diff --git a/android/app/src/com/android/bluetooth/btservice/CompanionManager.java b/android/app/src/com/android/bluetooth/btservice/CompanionManager.java
new file mode 100644
index 0000000..da3cd63
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/btservice/CompanionManager.java
@@ -0,0 +1,370 @@
+/*
+ * 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];
+ }
+}
diff --git a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
index 3fb1080..dd1297c 100644
--- a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
+++ b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
@@ -22,6 +22,7 @@
import android.app.admin.SecurityLog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAssignedNumbers;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -312,6 +313,7 @@
@VisibleForTesting int mBondState;
@VisibleForTesting int mDeviceType;
@VisibleForTesting ParcelUuid[] mUuids;
+ private BluetoothAudioPolicy mAudioPolicy;
DeviceProperties() {
mBondState = BluetoothDevice.BOND_NONE;
@@ -499,6 +501,14 @@
return mIsCoordinatedSetMember;
}
}
+
+ public void setHfAudioPolicyForRemoteAg(BluetoothAudioPolicy policies) {
+ mAudioPolicy = policies;
+ }
+
+ public BluetoothAudioPolicy getHfAudioPolicyForRemoteAg() {
+ return mAudioPolicy;
+ }
}
private void sendUuidIntent(BluetoothDevice device, DeviceProperties prop) {
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/AudioPolicyEntity.java b/android/app/src/com/android/bluetooth/btservice/storage/AudioPolicyEntity.java
new file mode 100644
index 0000000..8a302ff
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/btservice/storage/AudioPolicyEntity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 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.storage;
+
+import android.bluetooth.BluetoothAudioPolicy;
+
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+
+@Entity
+class AudioPolicyEntity {
+ @ColumnInfo(name = "call_establish_audio_policy")
+ public int callEstablishAudioPolicy;
+ @ColumnInfo(name = "connecting_time_audio_policy")
+ public int connectingTimeAudioPolicy;
+ @ColumnInfo(name = "in_band_ringtone_audio_policy")
+ public int inBandRingtoneAudioPolicy;
+
+ AudioPolicyEntity() {
+ callEstablishAudioPolicy = BluetoothAudioPolicy.POLICY_UNCONFIGURED;
+ connectingTimeAudioPolicy = BluetoothAudioPolicy.POLICY_UNCONFIGURED;
+ inBandRingtoneAudioPolicy = BluetoothAudioPolicy.POLICY_UNCONFIGURED;
+ }
+
+ AudioPolicyEntity(int callEstablishAudioPolicy, int connectingTimeAudioPolicy,
+ int inBandRingtoneAudioPolicy) {
+ this.callEstablishAudioPolicy = callEstablishAudioPolicy;
+ this.connectingTimeAudioPolicy = connectingTimeAudioPolicy;
+ this.inBandRingtoneAudioPolicy = inBandRingtoneAudioPolicy;
+ }
+
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("callEstablishAudioPolicy=")
+ .append(metadataToString(callEstablishAudioPolicy))
+ .append("|connectingTimeAudioPolicy=")
+ .append(metadataToString(connectingTimeAudioPolicy))
+ .append("|inBandRingtoneAudioPolicy=")
+ .append(metadataToString(inBandRingtoneAudioPolicy));
+
+ return builder.toString();
+ }
+
+ private String metadataToString(int metadata) {
+ return String.valueOf(metadata);
+ }
+}
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java b/android/app/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java
index b3746a2..e2f0765 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java
@@ -47,6 +47,8 @@
public byte[] spatial_audio;
public byte[] fastpair_customized;
public byte[] le_audio;
+ public byte[] gmcs_cccd;
+ public byte[] gtbs_cccd;
public String toString() {
StringBuilder builder = new StringBuilder();
@@ -103,7 +105,11 @@
.append("|fastpair_customized=")
.append(metadataToString(fastpair_customized))
.append("|le_audio=")
- .append(metadataToString(le_audio));
+ .append(metadataToString(le_audio))
+ .append("|gmcs_cccd=")
+ .append(metadataToString(gmcs_cccd))
+ .append("|gtbs_cccd=")
+ .append(metadataToString(gtbs_cccd));
return builder.toString();
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java b/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
index fa85049..df8505e 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
@@ -20,6 +20,7 @@
import android.bluetooth.BluetoothA2dp.OptionalCodecsPreferenceStatus;
import android.bluetooth.BluetoothA2dp.OptionalCodecsSupportStatus;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProtoEnums;
@@ -284,6 +285,59 @@
}
/**
+ * Set audio policy metadata to database with requested key
+ */
+ @VisibleForTesting
+ public boolean setAudioPolicyMetadata(BluetoothDevice device, BluetoothAudioPolicy policies) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "setAudioPolicyMetadata: device is null");
+ return false;
+ }
+
+ String address = device.getAddress();
+ if (!mMetadataCache.containsKey(address)) {
+ createMetadata(address, false);
+ }
+ Metadata data = mMetadataCache.get(address);
+ AudioPolicyEntity entity = data.audioPolicyMetadata;
+ entity.callEstablishAudioPolicy = policies.getCallEstablishPolicy();
+ entity.connectingTimeAudioPolicy = policies.getConnectingTimePolicy();
+ entity.inBandRingtoneAudioPolicy = policies.getInBandRingtonePolicy();
+
+ updateDatabase(data);
+ return true;
+ }
+ }
+
+ /**
+ * Get audio policy metadata from database with requested key
+ */
+ @VisibleForTesting
+ public BluetoothAudioPolicy getAudioPolicyMetadata(BluetoothDevice device) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "getAudioPolicyMetadata: device is null");
+ return null;
+ }
+
+ String address = device.getAddress();
+
+ if (!mMetadataCache.containsKey(address)) {
+ Log.d(TAG, "getAudioPolicyMetadata: device " + address + " is not in cache");
+ return null;
+ }
+
+ AudioPolicyEntity entity = mMetadataCache.get(address).audioPolicyMetadata;
+ return new BluetoothAudioPolicy.Builder()
+ .setCallEstablishPolicy(entity.callEstablishAudioPolicy)
+ .setConnectingTimePolicy(entity.connectingTimeAudioPolicy)
+ .setInBandRingtonePolicy(entity.inBandRingtoneAudioPolicy)
+ .build();
+ }
+ }
+
+ /**
* Set the device profile connection policy
*
* @param device {@link BluetoothDevice} wish to set
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
index f64f35e..756b6d7 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
@@ -58,6 +58,25 @@
public long last_active_time;
public boolean is_active_a2dp_device;
+ @Embedded
+ public AudioPolicyEntity audioPolicyMetadata;
+
+ /**
+ * The preferred profile to be used for {@link BluetoothDevice#AUDIO_MODE_OUTPUT_ONLY}. This can
+ * be either {@link BluetoothProfile#A2DP} or {@link BluetoothProfile#LE_AUDIO}. This value is
+ * only used if the remote device supports both A2DP and LE Audio and both transports are
+ * connected and active.
+ */
+ public int preferred_output_only_profile;
+
+ /**
+ * The preferred profile to be used for {@link BluetoothDevice#AUDIO_MODE_DUPLEX}. This can
+ * be either {@link BluetoothProfile#HEADSET} or {@link BluetoothProfile#LE_AUDIO}. This value
+ * is only used if the remote device supports both HFP and LE Audio and both transports are
+ * connected and active.
+ */
+ public int preferred_duplex_profile;
+
Metadata(String address) {
this.address = address;
migrated = false;
@@ -67,6 +86,9 @@
a2dpOptionalCodecsEnabled = BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN;
last_active_time = MetadataDatabase.sCurrentConnectionNumber++;
is_active_a2dp_device = true;
+ audioPolicyMetadata = new AudioPolicyEntity();
+ preferred_output_only_profile = 0;
+ preferred_duplex_profile = 0;
}
/**
@@ -290,6 +312,12 @@
case BluetoothDevice.METADATA_LE_AUDIO:
publicMetadata.le_audio = value;
break;
+ case BluetoothDevice.METADATA_GMCS_CCCD:
+ publicMetadata.gmcs_cccd = value;
+ break;
+ case BluetoothDevice.METADATA_GTBS_CCCD:
+ publicMetadata.gtbs_cccd = value;
+ break;
}
}
@@ -381,6 +409,12 @@
case BluetoothDevice.METADATA_LE_AUDIO:
value = publicMetadata.le_audio;
break;
+ case BluetoothDevice.METADATA_GMCS_CCCD:
+ value = publicMetadata.gmcs_cccd;
+ break;
+ case BluetoothDevice.METADATA_GTBS_CCCD:
+ value = publicMetadata.gtbs_cccd;
+ break;
}
return value;
}
@@ -407,6 +441,8 @@
.append(a2dpOptionalCodecsEnabled)
.append("), custom metadata(")
.append(publicMetadata)
+ .append("), hfp client audio policy(")
+ .append(audioPolicyMetadata)
.append(")}");
return builder.toString();
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
index a71a548..2776e65 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
@@ -33,7 +33,7 @@
/**
* MetadataDatabase is a Room database stores Bluetooth persistence data
*/
-@Database(entities = {Metadata.class}, version = 114)
+@Database(entities = {Metadata.class}, version = 117)
public abstract class MetadataDatabase extends RoomDatabase {
/**
* The metadata database file name
@@ -67,6 +67,9 @@
.addMigrations(MIGRATION_111_112)
.addMigrations(MIGRATION_112_113)
.addMigrations(MIGRATION_113_114)
+ .addMigrations(MIGRATION_114_115)
+ .addMigrations(MIGRATION_115_116)
+ .addMigrations(MIGRATION_116_117)
.allowMainThreadQueries()
.build();
}
@@ -500,4 +503,67 @@
}
}
};
+
+ @VisibleForTesting
+ static final Migration MIGRATION_114_115 = new Migration(114, 115) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ try {
+ database.execSQL(
+ "ALTER TABLE metadata ADD COLUMN `call_establish_audio_policy` "
+ + "INTEGER DEFAULT 0");
+ database.execSQL(
+ "ALTER TABLE metadata ADD COLUMN `connecting_time_audio_policy` "
+ + "INTEGER DEFAULT 0");
+ database.execSQL(
+ "ALTER TABLE metadata ADD COLUMN `in_band_ringtone_audio_policy` "
+ + "INTEGER DEFAULT 0");
+ } catch (SQLException ex) {
+ // Check if user has new schema, but is just missing the version update
+ Cursor cursor = database.query("SELECT * FROM metadata");
+ if (cursor == null
+ || cursor.getColumnIndex("call_establish_audio_policy") == -1) {
+ throw ex;
+ }
+ }
+ }
+ };
+
+ @VisibleForTesting
+ static final Migration MIGRATION_115_116 = new Migration(115, 116) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ try {
+ database.execSQL("ALTER TABLE metadata ADD COLUMN `preferred_output_only_profile` "
+ + "INTEGER NOT NULL DEFAULT 0");
+ database.execSQL("ALTER TABLE metadata ADD COLUMN `preferred_duplex_profile` "
+ + "INTEGER NOT NULL DEFAULT 0");
+ } catch (SQLException ex) {
+ // Check if user has new schema, but is just missing the version update
+ Cursor cursor = database.query("SELECT * FROM metadata");
+ if (cursor == null
+ || cursor.getColumnIndex("preferred_output_only_profile") == -1
+ || cursor.getColumnIndex("preferred_duplex_profile") == -1) {
+ throw ex;
+ }
+ }
+ }
+ };
+
+ @VisibleForTesting
+ static final Migration MIGRATION_116_117 = new Migration(116, 117) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ try {
+ database.execSQL("ALTER TABLE metadata ADD COLUMN `gmcs_cccd` BLOB");
+ database.execSQL("ALTER TABLE metadata ADD COLUMN `gtbs_cccd` BLOB");
+ } catch (SQLException ex) {
+ // Check if user has new schema, but is just missing the version update
+ Cursor cursor = database.query("SELECT * FROM metadata");
+ if (cursor == null || cursor.getColumnIndex("gmcs_cccd") == -1) {
+ throw ex;
+ }
+ }
+ }
+ };
}
diff --git a/android/app/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java
index 21f596d..1b8c61d 100644
--- a/android/app/src/com/android/bluetooth/gatt/GattService.java
+++ b/android/app/src/com/android/bluetooth/gatt/GattService.java
@@ -79,6 +79,7 @@
import com.android.bluetooth.btservice.AbstractionLayer;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.BluetoothAdapterProxy;
+import com.android.bluetooth.btservice.CompanionManager;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.util.NumberUtils;
import com.android.internal.annotations.VisibleForTesting;
@@ -3865,33 +3866,21 @@
// Link supervision timeout is measured in N * 10ms
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:
- minInterval = getResources().getInteger(R.integer.gatt_low_power_min_interval);
- maxInterval = getResources().getInteger(R.integer.gatt_low_power_max_interval);
- latency = getResources().getInteger(R.integer.gatt_low_power_latency);
- break;
+ CompanionManager manager =
+ AdapterService.getAdapterService().getCompanionManager();
- default:
- // Using the values for CONNECTION_PRIORITY_BALANCED.
- minInterval =
- getResources().getInteger(R.integer.gatt_balanced_priority_min_interval);
- maxInterval =
- getResources().getInteger(R.integer.gatt_balanced_priority_max_interval);
- latency = getResources().getInteger(R.integer.gatt_balanced_priority_latency);
- break;
- }
+ minInterval = manager.getGattConnParameters(
+ address, CompanionManager.GATT_CONN_INTERVAL_MIN, connectionPriority);
+ maxInterval = manager.getGattConnParameters(
+ address, CompanionManager.GATT_CONN_INTERVAL_MAX, connectionPriority);
+ latency = manager.getGattConnParameters(
+ address, CompanionManager.GATT_CONN_LATENCY, connectionPriority);
- if (DBG) {
- Log.d(TAG, "connectionParameterUpdate() - address=" + address + "params="
- + connectionPriority + " interval=" + minInterval + "/" + maxInterval);
- }
+ Log.d(TAG, "connectionParameterUpdate() - address=" + address + " params="
+ + connectionPriority + " interval=" + minInterval + "/" + maxInterval
+ + " timeout=" + timeout);
+
gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval, latency,
timeout, 0, 0);
}
@@ -3906,14 +3895,11 @@
return;
}
- if (DBG) {
- Log.d(TAG, "leConnectionUpdate() - address=" + address + ", intervals="
- + minInterval + "/" + maxInterval + ", latency=" + peripheralLatency
- + ", timeout=" + supervisionTimeout + "msec" + ", min_ce="
- + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen);
+ Log.d(TAG, "leConnectionUpdate() - address=" + address + ", intervals="
+ + minInterval + "/" + maxInterval + ", latency=" + peripheralLatency
+ + ", timeout=" + supervisionTimeout + "msec" + ", min_ce="
+ + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen);
-
- }
gattConnectionParameterUpdateNative(clientIf, address, minInterval, maxInterval,
peripheralLatency, supervisionTimeout,
minConnectionEventLen, maxConnectionEventLen);
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
index 19bffa0..536c054 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
@@ -23,6 +23,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -1364,6 +1365,24 @@
}
/**
+ * Get the Bluetooth Audio Policy stored in the state machine
+ *
+ * @param device the device to change silence mode
+ * @return a {@link BluetoothAudioPolicy} object
+ */
+ public BluetoothAudioPolicy getHfpCallAudioPolicy(BluetoothDevice device) {
+ synchronized (mStateMachines) {
+ final HeadsetStateMachine stateMachine = mStateMachines.get(device);
+ if (stateMachine == null) {
+ Log.e(TAG, "getHfpCallAudioPolicy(), " + device
+ + " does not have a state machine");
+ return null;
+ }
+ return stateMachine.getHfpCallAudioPolicy();
+ }
+ }
+
+ /**
* Remove the active device
*/
private void removeActiveDevice() {
@@ -1891,7 +1910,24 @@
mSystemInterface.getAudioManager().setA2dpSuspended(false);
}
});
-
+ if (callState == HeadsetHalConstants.CALL_STATE_IDLE) {
+ final HeadsetStateMachine stateMachine = mStateMachines.get(mActiveDevice);
+ if (stateMachine == null) {
+ Log.d(TAG, "phoneStateChanged: CALL_STATE_IDLE, mActiveDevice is Null");
+ } else {
+ BluetoothAudioPolicy currentPolicy = stateMachine.getHfpCallAudioPolicy();
+ if (currentPolicy != null && currentPolicy.getConnectingTimePolicy()
+ == BluetoothAudioPolicy.POLICY_NOT_ALLOWED) {
+ /**
+ * If the active device was set because of the pick up audio policy
+ * and the connecting policy is NOT_ALLOWED, then after the call is
+ * terminated, we must de-activate this device.
+ * If there is a fallback mechanism, we should follow it.
+ */
+ removeActiveDevice();
+ }
+ }
+ }
}
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -1940,8 +1976,23 @@
public boolean isInbandRingingEnabled() {
boolean isInbandRingingSupported = getResources().getBoolean(
com.android.bluetooth.R.bool.config_bluetooth_hfp_inband_ringing_support);
+
+ boolean inbandRingtoneAllowedByPolicy = true;
+ List<BluetoothDevice> audioConnectableDevices = getConnectedDevices();
+ if (audioConnectableDevices.size() == 1) {
+ BluetoothDevice connectedDevice = audioConnectableDevices.get(0);
+ BluetoothAudioPolicy callAudioPolicy =
+ getHfpCallAudioPolicy(connectedDevice);
+ if (callAudioPolicy != null && callAudioPolicy.getInBandRingtonePolicy()
+ == BluetoothAudioPolicy.POLICY_NOT_ALLOWED) {
+ inbandRingtoneAllowedByPolicy = false;
+ }
+ }
+
return isInbandRingingSupported && !SystemProperties.getBoolean(
- DISABLE_INBAND_RINGING_PROPERTY, false) && !mInbandRingingRuntimeDisable;
+ DISABLE_INBAND_RINGING_PROPERTY, false)
+ && !mInbandRingingRuntimeDisable
+ && inbandRingtoneAllowedByPolicy;
}
/**
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
index b237e7d..cec0a39 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
@@ -21,6 +21,7 @@
import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAssignedNumbers;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
@@ -43,6 +44,7 @@
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -134,6 +136,7 @@
private final AdapterService mAdapterService;
private final HeadsetNativeInterface mNativeInterface;
private final HeadsetSystemInterface mSystemInterface;
+ private DatabaseManager mDatabaseManager;
// Runtime states
@VisibleForTesting
@@ -155,6 +158,10 @@
// Audio disconnect timeout retry count
private int mAudioDisconnectRetry = 0;
+ static final int HFP_SET_AUDIO_POLICY = 1;
+
+ private BluetoothAudioPolicy mHsClientAudioPolicy;
+
// Keys are AT commands, and values are the company IDs.
private static final Map<String, Integer> VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID;
@@ -187,7 +194,21 @@
mSystemInterface =
Objects.requireNonNull(systemInterface, "systemInterface cannot be null");
mAdapterService = Objects.requireNonNull(adapterService, "AdapterService cannot be null");
+ mDatabaseManager = Objects.requireNonNull(
+ AdapterService.getAdapterService().getDatabase(),
+ "DatabaseManager cannot be null when HeadsetClientStateMachine is created");
mDeviceSilenced = false;
+
+ BluetoothAudioPolicy storedAudioPolicy = mDatabaseManager.getAudioPolicyMetadata(device);
+ if (storedAudioPolicy == null) {
+ Log.w(TAG, "Audio Policy not created in database! Creating...");
+ mHsClientAudioPolicy = new BluetoothAudioPolicy.Builder().build();
+ mDatabaseManager.setAudioPolicyMetadata(device, mHsClientAudioPolicy);
+ } else {
+ Log.i(TAG, "Audio Policy found in database!");
+ mHsClientAudioPolicy = storedAudioPolicy;
+ }
+
// Create phonebook helper
mPhonebook = new AtPhonebook(mHeadsetService, mNativeInterface);
// Initialize state machine
@@ -241,6 +262,8 @@
ProfileService.println(sb, " mMicVolume: " + mMicVolume);
ProfileService.println(sb,
" mConnectingTimestampMs(uptimeMillis): " + mConnectingTimestampMs);
+ ProfileService.println(sb, " mHsClientAudioPolicy: " + mHsClientAudioPolicy.toString());
+
ProfileService.println(sb, " StateMachine: " + this);
// Dump the state machine logs
StringWriter stringWriter = new StringWriter();
@@ -1909,6 +1932,119 @@
}
/**
+ * Process Android specific AT commands.
+ *
+ * @param atString AT command after the "AT+" prefix. Starts with "ANDROID"
+ * @param device Remote device that has sent this command
+ */
+ private void processAndroidAt(String atString, BluetoothDevice device) {
+ log("processAndroidSpecificAt - atString = " + atString);
+
+ if (atString.equals("+ANDROID=?")) {
+ // feature request type command
+ processAndroidAtFeatureRequest(device);
+ } else if (atString.startsWith("+ANDROID=")) {
+ // set type command
+ int equalIndex = atString.indexOf("=");
+ String arg = atString.substring(equalIndex + 1);
+
+ if (arg.isEmpty()) {
+ Log.e(TAG, "Command Invalid!");
+ mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
+ return;
+ }
+
+ Object[] args = generateArgs(arg);
+
+ if (!(args[0] instanceof Integer)) {
+ Log.e(TAG, "Type ID is invalid");
+ mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
+ return;
+ }
+
+ int type = (Integer) args[0];
+
+ if (type == HFP_SET_AUDIO_POLICY) {
+ processAndroidAtSetAudioPolicy(args, device);
+ } else {
+ Log.w(TAG, "Undefined AT+ANDROID command");
+ mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
+ return;
+ }
+ } else {
+ Log.e(TAG, "Undefined AT+ANDROID command");
+ mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
+ return;
+ }
+ mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0);
+ }
+
+ private void processAndroidAtFeatureRequest(BluetoothDevice device) {
+ /*
+ replying with +ANDROID=1
+ here, 1 is the feature id for audio policy
+
+ currently we only support one type of feature
+ */
+ mNativeInterface.atResponseString(device,
+ BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID
+ + ": " + HFP_SET_AUDIO_POLICY);
+ }
+
+ /**
+ * Process AT+ANDROID AT command
+ *
+ * @param args command arguments after the equal sign
+ * @param device Remote device that has sent this command
+ */
+ private void processAndroidAtSetAudioPolicy(Object[] args, BluetoothDevice device) {
+ if (args.length != 4) {
+ Log.e(TAG, "processAndroidAtSetAudioPolicy() args length must be 4: "
+ + String.valueOf(args.length));
+ return;
+ }
+ if (!(args[1] instanceof Integer) || !(args[2] instanceof Integer)
+ || !(args[3] instanceof Integer)) {
+ Log.e(TAG, "processAndroidAtSetAudioPolicy() argument types not matched");
+ return;
+ }
+
+ if (!mDevice.equals(device)) {
+ Log.e(TAG, "processAndroidAtSetAudioPolicy(): argument device " + device
+ + " doesn't match mDevice " + mDevice);
+ return;
+ }
+
+ int callEstablishPolicy = (Integer) args[1];
+ int connectingTimePolicy = (Integer) args[2];
+ int inbandPolicy = (Integer) args[3];
+
+ setHfpCallAudioPolicy(new BluetoothAudioPolicy.Builder()
+ .setCallEstablishPolicy(callEstablishPolicy)
+ .setConnectingTimePolicy(connectingTimePolicy)
+ .setInBandRingtonePolicy(inbandPolicy)
+ .build());
+ }
+
+ /**
+ * sets the audio policy of the client device and stores in the database
+ *
+ * @param policies policies to be set and stored
+ */
+ public void setHfpCallAudioPolicy(BluetoothAudioPolicy policies) {
+ mHsClientAudioPolicy = policies;
+ mDatabaseManager.setAudioPolicyMetadata(mDevice, policies);
+ }
+
+ /**
+ * get the audio policy of the client device
+ *
+ */
+ public BluetoothAudioPolicy getHfpCallAudioPolicy() {
+ return mHsClientAudioPolicy;
+ }
+
+ /**
* Process AT+XAPL AT command
*
* @param args command arguments after the equal sign
@@ -1959,6 +2095,8 @@
processAtCpbs(atCommand.substring(5), commandType, device);
} else if (atCommand.startsWith("+CPBR")) {
processAtCpbr(atCommand.substring(5), commandType, device);
+ } else if (atCommand.startsWith("+ANDROID")) {
+ processAndroidAt(atCommand, device);
} else {
processVendorSpecificAt(atCommand, device);
}
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java b/android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
index 08f4bdb..0820544 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.content.ActivityNotFoundException;
@@ -155,13 +156,19 @@
@VisibleForTesting
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
public void answerCall(BluetoothDevice device) {
+ Log.d(TAG, "answerCall");
if (device == null) {
Log.w(TAG, "answerCall device is null");
return;
}
BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
if (bluetoothInCallService != null) {
- mHeadsetService.setActiveDevice(device);
+ BluetoothAudioPolicy callAudioPolicy =
+ mHeadsetService.getHfpCallAudioPolicy(device);
+ if (callAudioPolicy == null || callAudioPolicy.getCallEstablishPolicy()
+ != BluetoothAudioPolicy.POLICY_NOT_ALLOWED) {
+ mHeadsetService.setActiveDevice(device);
+ }
bluetoothInCallService.answerCall();
} else {
Log.e(TAG, "Handsfree phone proxy null for answering call");
diff --git a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
index db43df5..bce1c79 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
@@ -17,6 +17,7 @@
package com.android.bluetooth.hfpclient;
import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHeadsetClientCall;
@@ -59,7 +60,7 @@
* @hide
*/
public class HeadsetClientService extends ProfileService {
- private static final boolean DBG = false;
+ private static final boolean DBG = true;
private static final String TAG = "HeadsetClientService";
// This is also used as a lock for shared data in {@link HeadsetClientService}
@@ -612,8 +613,10 @@
List<BluetoothHeadsetClientCall> defaultValue = new ArrayList<>();
if (service != null) {
List<HfpClientCall> calls = service.getCurrentCalls(device);
- for (HfpClientCall call : calls) {
- defaultValue.add(toLegacyCall(call));
+ if (calls != null) {
+ for (HfpClientCall call : calls) {
+ defaultValue.add(toLegacyCall(call));
+ }
}
}
receiver.send(defaultValue);
@@ -923,6 +926,53 @@
return false;
}
+ /**
+ * sends the {@link BluetoothAudioPolicy} object to the state machine of the corresponding
+ * device to store and send to the remote device using Android specific AT commands.
+ *
+ * @param device for whom the policies to be set
+ * @param policies to be set policies
+ */
+ public void setAudioPolicy(BluetoothDevice device, BluetoothAudioPolicy policies) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ Log.i(TAG, "setAudioPolicy: device=" + device + ", " + policies.toString() + ", "
+ + Utils.getUidPidString());
+ HeadsetClientStateMachine sm = getStateMachine(device);
+ if (sm != null) {
+ sm.setAudioPolicy(policies);
+ }
+ }
+
+ /**
+ * sets the audio policy feature support status for the corresponding device.
+ *
+ * @param device for whom the policies to be set
+ * @param supported support status
+ */
+ public void setAudioPolicyRemoteSupported(BluetoothDevice device, boolean supported) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ Log.i(TAG, "setAudioPolicyRemoteSupported: " + supported);
+ HeadsetClientStateMachine sm = getStateMachine(device);
+ if (sm != null) {
+ sm.setAudioPolicyRemoteSupported(supported);
+ }
+ }
+
+ /**
+ * gets the audio policy feature support status for the corresponding device.
+ *
+ * @param device for whom the policies to be set
+ * @return int support status
+ */
+ public int getAudioPolicyRemoteSupported(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ HeadsetClientStateMachine sm = getStateMachine(device);
+ if (sm != null) {
+ return sm.getAudioPolicyRemoteSupported();
+ }
+ return BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE;
+ }
+
public boolean connectAudio(BluetoothDevice device) {
Log.i(TAG, "connectAudio: device=" + device + ", " + Utils.getUidPidString());
HeadsetClientStateMachine sm = getStateMachine(device);
diff --git a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
index d7d86fa..581b258 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
@@ -37,6 +37,7 @@
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHeadsetClient.NetworkServiceState;
@@ -109,6 +110,7 @@
public static final int DISABLE_NREC = 20;
public static final int SEND_VENDOR_AT_COMMAND = 21;
public static final int SEND_BIEV = 22;
+ public static final int SEND_ANDROID_AT_COMMAND = 23;
// internal actions
@VisibleForTesting
@@ -180,6 +182,12 @@
int mAudioState;
// Indicates whether audio can be routed to the device
private boolean mAudioRouteAllowed;
+
+ private static final int CALL_AUDIO_POLICY_FEATURE_ID = 1;
+
+ public int mAudioPolicyRemoteSupported;
+ private BluetoothAudioPolicy mHsClientAudioPolicy;
+
private boolean mAudioWbs;
private int mVoiceRecognitionActive;
private final BluetoothAdapter mAdapter;
@@ -227,6 +235,8 @@
ProfileService.println(sb, " mOperatorName: " + mOperatorName);
ProfileService.println(sb, " mSubscriberInfo: " + mSubscriberInfo);
ProfileService.println(sb, " mAudioRouteAllowed: " + mAudioRouteAllowed);
+ ProfileService.println(sb, " mAudioPolicyRemoteSupported: " + mAudioPolicyRemoteSupported);
+ ProfileService.println(sb, " mHsClientAudioPolicy: " + mHsClientAudioPolicy);
ProfileService.println(sb, " mCalls:");
if (mCalls != null) {
@@ -867,6 +877,8 @@
mAudioRouteAllowed = context.getResources().getBoolean(
R.bool.headset_client_initial_audio_route_allowed);
+ mHsClientAudioPolicy = new BluetoothAudioPolicy.Builder().build();
+
mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE;
mIndicatorNetworkType = HeadsetClientHalConstants.SERVICE_TYPE_HOME;
mIndicatorNetworkSignal = 0;
@@ -1149,6 +1161,40 @@
deferMessage(message);
break;
case StackEvent.EVENT_TYPE_CMD_RESULT:
+ logD("Connecting: CMD_RESULT valueInt:" + event.valueInt
+ + " mQueuedActions.size=" + mQueuedActions.size());
+ if (!mQueuedActions.isEmpty()) {
+ logD("queuedAction:" + mQueuedActions.peek().first);
+ }
+ Pair<Integer, Object> queuedAction = mQueuedActions.poll();
+ if (queuedAction == null || queuedAction.first == NO_ACTION) {
+ break;
+ }
+ switch (queuedAction.first) {
+ case SEND_ANDROID_AT_COMMAND:
+ if (event.valueInt == StackEvent.CMD_RESULT_TYPE_OK) {
+ Log.w(TAG, "Received OK instead of +ANDROID");
+ } else {
+ Log.w(TAG, "Received ERROR instead of +ANDROID");
+ }
+ setAudioPolicyRemoteSupported(false);
+ transitionTo(mConnected);
+ break;
+ default:
+ Log.w(TAG, "Ignored CMD Result");
+ break;
+ }
+ break;
+
+ case StackEvent.EVENT_TYPE_UNKNOWN_EVENT:
+ if (mVendorProcessor.processEvent(event.valueString, event.device)) {
+ mQueuedActions.poll();
+ transitionTo(mConnected);
+ } else {
+ Log.e(TAG, "Unknown event :" + event.valueString
+ + " for device " + event.device);
+ }
+ break;
case StackEvent.EVENT_TYPE_SUBSCRIBER_INFO:
case StackEvent.EVENT_TYPE_CURRENT_CALLS:
case StackEvent.EVENT_TYPE_OPERATOR_NAME:
@@ -1212,7 +1258,11 @@
mAudioManager.isMicrophoneMute() ? 0 : 15, 0));
// query subscriber info
deferMessage(obtainMessage(HeadsetClientStateMachine.SUBSCRIBER_INFO));
- transitionTo(mConnected);
+
+ if (!queryRemoteSupportedFeatures()) {
+ Log.w(TAG, "Couldn't query Android AT remote supported!");
+ transitionTo(mConnected);
+ }
break;
case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED:
@@ -1597,6 +1647,8 @@
oldState, mVoiceRecognitionActive);
}
break;
+ case SEND_ANDROID_AT_COMMAND:
+ logD("Connected: Received OK for AT+ANDROID");
default:
Log.w(TAG, "Unhandled AT OK " + event);
break;
@@ -2098,9 +2150,81 @@
public void setAudioRouteAllowed(boolean allowed) {
mAudioRouteAllowed = allowed;
+
+ int establishPolicy = allowed
+ ? BluetoothAudioPolicy.POLICY_ALLOWED :
+ BluetoothAudioPolicy.POLICY_NOT_ALLOWED;
+
+ /*
+ * Backward compatibility for mAudioRouteAllowed
+ */
+ setAudioPolicy(new BluetoothAudioPolicy.Builder(mHsClientAudioPolicy)
+ .setCallEstablishPolicy(establishPolicy).build());
}
public boolean getAudioRouteAllowed() {
return mAudioRouteAllowed;
}
+
+ private String createMaskString(BluetoothAudioPolicy policies) {
+ StringBuilder mask = new StringBuilder();
+ mask.append(Integer.toString(CALL_AUDIO_POLICY_FEATURE_ID));
+ mask.append("," + policies.getCallEstablishPolicy());
+ mask.append("," + policies.getConnectingTimePolicy());
+ mask.append("," + policies.getInBandRingtonePolicy());
+ return mask.toString();
+ }
+
+ /**
+ * sets the {@link BluetoothAudioPolicy} object device and send to the remote
+ * device using Android specific AT commands.
+ *
+ * @param policies to be set policies
+ */
+ public void setAudioPolicy(BluetoothAudioPolicy policies) {
+ logD("setAudioPolicy: " + policies);
+ mHsClientAudioPolicy = policies;
+
+ if (mAudioPolicyRemoteSupported != BluetoothAudioPolicy.FEATURE_SUPPORTED_BY_REMOTE) {
+ Log.e(TAG, "Audio Policy feature not supported!");
+ return;
+ }
+
+ if (!mNativeInterface.sendAndroidAt(mCurrentDevice,
+ "+ANDROID=" + createMaskString(policies))) {
+ Log.e(TAG, "ERROR: Couldn't send call audio policies");
+ }
+ }
+
+ private boolean queryRemoteSupportedFeatures() {
+ Log.i(TAG, "queryRemoteSupportedFeatures");
+ if (!mNativeInterface.sendAndroidAt(mCurrentDevice, "+ANDROID=?")) {
+ Log.e(TAG, "ERROR: Couldn't send audio policy feature query");
+ return false;
+ }
+ addQueuedAction(SEND_ANDROID_AT_COMMAND);
+ return true;
+ }
+
+ /**
+ * sets the audio policy feature support status
+ *
+ * @param supported support status
+ */
+ public void setAudioPolicyRemoteSupported(boolean supported) {
+ if (supported) {
+ mAudioPolicyRemoteSupported = BluetoothAudioPolicy.FEATURE_SUPPORTED_BY_REMOTE;
+ } else {
+ mAudioPolicyRemoteSupported = BluetoothAudioPolicy.FEATURE_NOT_SUPPORTED_BY_REMOTE;
+ }
+ }
+
+ /**
+ * gets the audio policy feature support status
+ *
+ * @return int support status
+ */
+ public int getAudioPolicyRemoteSupported() {
+ return mAudioPolicyRemoteSupported;
+ }
}
diff --git a/android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java b/android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java
index 1cb2cf7..2615b73 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/NativeInterface.java
@@ -266,6 +266,17 @@
return sendATCmdNative(getByteAddress(device), atCmd, val1, val2, arg);
}
+ /**
+ * Set call audio policy to the specified paired device
+ *
+ * @param cmd Android specific command string
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean sendAndroidAt(BluetoothDevice device, String cmd) {
+ return sendAndroidAtNative(getByteAddress(device), cmd);
+ }
+
// Native methods that call into the JNI interface
private static native void classInitNative();
@@ -306,6 +317,8 @@
private static native boolean sendATCmdNative(byte[] address, int atCmd, int val1, int val2,
String arg);
+ private static native boolean sendAndroidAtNative(byte[] address, String cmd);
+
private BluetoothDevice getDevice(byte[] address) {
return mAdapterService.getDeviceFromByte(address);
}
diff --git a/android/app/src/com/android/bluetooth/hfpclient/StackEvent.java b/android/app/src/com/android/bluetooth/hfpclient/StackEvent.java
index a0b8ce6..4c08946 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/StackEvent.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/StackEvent.java
@@ -51,6 +51,9 @@
public static final int EVENT_TYPE_RING_INDICATION = 21;
public static final int EVENT_TYPE_UNKNOWN_EVENT = 22;
+ public static final int CMD_RESULT_TYPE_OK = 0;
+ public static final int CMD_RESULT_TYPE_CME_ERROR = 7;
+
public int type = EVENT_TYPE_NONE;
public int valueInt = 0;
public int valueInt2 = 0;
diff --git a/android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java b/android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java
index 205ba40..cd2ec0d 100644
--- a/android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java
+++ b/android/app/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java
@@ -69,6 +69,9 @@
SUPPORTED_VENDOR_EVENTS.put(
"+XAPL=",
BluetoothAssignedNumbers.APPLE);
+ SUPPORTED_VENDOR_EVENTS.put(
+ "+ANDROID:",
+ BluetoothAssignedNumbers.GOOGLE);
}
VendorCommandResponseProcessor(HeadsetClientService context, NativeInterface nativeInterface) {
@@ -148,10 +151,14 @@
if (vendorId == null) {
Log.e(TAG, "Invalid response: " + atString + ". " + eventCode);
return false;
+ } else if (vendorId == BluetoothAssignedNumbers.GOOGLE) {
+ Log.i(TAG, "received +ANDROID event. Setting Audio policy to true");
+ mService.setAudioPolicyRemoteSupported(device, true);
+ } else {
+ broadcastVendorSpecificEventIntent(vendorId, eventCode, atString, device);
+ logD("process vendor event " + vendorId + ", " + eventCode + ", "
+ + atString + " for device" + device);
}
- broadcastVendorSpecificEventIntent(vendorId, eventCode, atString, device);
- logD("process vendor event " + vendorId + ", " + eventCode + ", "
- + atString + " for device" + device);
return true;
}
diff --git a/android/app/src/com/android/bluetooth/mapclient/MapClientService.java b/android/app/src/com/android/bluetooth/mapclient/MapClientService.java
index ff38353..25a8a2f 100644
--- a/android/app/src/com/android/bluetooth/mapclient/MapClientService.java
+++ b/android/app/src/com/android/bluetooth/mapclient/MapClientService.java
@@ -166,9 +166,6 @@
}
private synchronized void addDeviceToMapAndConnect(BluetoothDevice device) {
- if (Utils.isInstrumentationTestMode()) {
- Log.d(TAG, "addDeviceToMapAndConnect: device=" + device, new Exception());
- }
// When creating a new statemachine, its state is set to CONNECTING - which will trigger
// connect.
MceStateMachine mapStateMachine = new MceStateMachine(this, device);
@@ -358,6 +355,7 @@
}
stateMachine.doQuit();
}
+ mMapInstanceMap.clear();
return true;
}
@@ -367,10 +365,6 @@
Log.d(TAG, "in Cleanup");
}
removeUncleanAccounts();
- mMapInstanceMap.clear();
- if (Utils.isInstrumentationTestMode()) {
- Log.d(TAG, "cleanup() called.", new Exception());
- }
// TODO(b/72948646): should be moved to stop()
setMapClientService(null);
}
diff --git a/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java b/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java
index 0f1fef7..b37cbaa 100644
--- a/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java
+++ b/android/app/src/com/android/bluetooth/mcp/MediaControlGattService.java
@@ -17,6 +17,7 @@
package com.android.bluetooth.mcp;
+import static android.bluetooth.BluetoothDevice.METADATA_GMCS_CCCD;
import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED;
import static android.bluetooth.BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED;
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY;
@@ -28,6 +29,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -37,12 +39,17 @@
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.IBluetoothStateChangeCallback;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
+import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.hearingaid.HearingAidService;
@@ -441,7 +448,7 @@
} else {
status = BluetoothGatt.GATT_SUCCESS;
setCcc(device, op.mDescriptor.getCharacteristic().getUuid(), op.mOffset,
- op.mValue);
+ op.mValue, true);
}
if (op.mResponseNeeded) {
@@ -522,6 +529,34 @@
}
}
+ private void restoreCccValuesForStoredDevices() {
+ for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
+ byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD);
+
+ if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) {
+ return;
+ }
+
+ List<ParcelUuid> uuidList = Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd));
+
+ /* Restore CCCD values for device */
+ for (ParcelUuid uuid : uuidList) {
+ setCcc(device, uuid.getUuid(), 0,
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE, false);
+ }
+ }
+ }
+
+ private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
+ new IBluetoothStateChangeCallback.Stub() {
+ public void onBluetoothStateChange(boolean up) {
+ if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
+ if (up) {
+ restoreCccValuesForStoredDevices();
+ }
+ }
+ };
+
@VisibleForTesting
final BluetoothGattServerCallback mServerCallback = new BluetoothGattServerCallback() {
@Override
@@ -551,6 +586,7 @@
mCharacteristics.get(CharId.CONTENT_CONTROL_ID)
.setValue(mCcid, BluetoothGattCharacteristic.FORMAT_UINT8, 0);
+ restoreCccValuesForStoredDevices();
setInitialCharacteristicValuesAndNotify();
initialStateRequest();
}
@@ -796,6 +832,15 @@
mMcpService = mcpService;
mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
"AdapterService shouldn't be null when creating MediaControlCattService");
+
+ IBluetoothManager mgr = BluetoothAdapter.getDefaultAdapter().getBluetoothManager();
+ if (mgr != null) {
+ try {
+ mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
protected boolean init(UUID scvUuid) {
@@ -987,16 +1032,77 @@
return mBluetoothGattServer.addService(mGattService);
}
+ private void removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device) {
+ List<ParcelUuid> uuidList;
+ byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD);
+
+ if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) {
+ uuidList = new ArrayList<ParcelUuid>();
+ } else {
+ uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd)));
+
+ if (!uuidList.contains(charUuid)) {
+ Log.d(TAG, "Characteristic CCCD can't be removed (not cached): "
+ + charUuid.toString());
+ return;
+ }
+ }
+
+ uuidList.remove(charUuid);
+
+ if (!device.setMetadata(METADATA_GMCS_CCCD,
+ Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) {
+ Log.e(TAG, "Can't set CCCD for GMCS characteristic UUID: " + charUuid.toString()
+ + ", (remove)");
+ }
+ }
+
+ private void addUuidToMetadata(ParcelUuid charUuid, BluetoothDevice device) {
+ List<ParcelUuid> uuidList;
+ byte[] gmcs_cccd = device.getMetadata(METADATA_GMCS_CCCD);
+
+ if ((gmcs_cccd == null) || (gmcs_cccd.length == 0)) {
+ uuidList = new ArrayList<ParcelUuid>();
+ } else {
+ uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gmcs_cccd)));
+
+ if (uuidList.contains(charUuid)) {
+ Log.d(TAG, "Characteristic CCCD already added: " + charUuid.toString());
+ return;
+ }
+ }
+
+ uuidList.add(charUuid);
+
+ if (!device.setMetadata(METADATA_GMCS_CCCD,
+ Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) {
+ Log.e(TAG, "Can't set CCCD for GMCS characteristic UUID: " + charUuid.toString()
+ + ", (add)");
+ }
+ }
+
@VisibleForTesting
- void setCcc(BluetoothDevice device, UUID charUuid, int offset, byte[] value) {
+ void setCcc(BluetoothDevice device, UUID charUuid, int offset, byte[] value, boolean store) {
HashMap<UUID, Short> characteristicCcc = mCccDescriptorValues.get(device.getAddress());
if (characteristicCcc == null) {
characteristicCcc = new HashMap<>();
mCccDescriptorValues.put(device.getAddress(), characteristicCcc);
}
- characteristicCcc.put(
- charUuid, ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getShort());
+ characteristicCcc.put(charUuid,
+ ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).getShort());
+
+ if (!store) {
+ return;
+ }
+
+ if (Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
+ addUuidToMetadata(new ParcelUuid(charUuid), device);
+ } else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
+ removeUuidFromMetadata(new ParcelUuid(charUuid), device);
+ } else {
+ Log.e(TAG, "Not handled CCC value: " + Arrays.toString(value));
+ }
}
private byte[] getCccBytes(BluetoothDevice device, UUID charUuid) {
diff --git a/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java b/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java
index a68af61..59688a6 100644
--- a/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java
+++ b/android/app/src/com/android/bluetooth/tbs/BluetoothGattServerProxy.java
@@ -27,6 +27,7 @@
import android.content.Context;
import java.util.List;
+import java.util.UUID;
/**
* A proxy class that facilitates testing of the TbsService class.
@@ -63,6 +64,21 @@
return mBluetoothGattServer.addService(service);
}
+ /**
+ * A proxy that Returns a {@link BluetoothGattService} from the list of services offered
+ * by this device.
+ *
+ * <p>If multiple instances of the same service (as identified by UUID)
+ * exist, the first instance of the service is returned.
+ *
+ * @param uuid UUID of the requested service
+ * @return BluetoothGattService if supported, or null if the requested service is not offered by
+ * this device.
+ */
+ public BluetoothGattService getService(UUID uuid) {
+ return mBluetoothGattServer.getService(uuid);
+ }
+
public boolean sendResponse(BluetoothDevice device, int requestId, int status, int offset,
byte[] value) {
return mBluetoothGattServer.sendResponse(device, requestId, status, offset, value);
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
index 7c3f9fc..4c1833a 100644
--- a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
+++ b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
@@ -17,17 +17,27 @@
package com.android.bluetooth.tbs;
+import static android.bluetooth.BluetoothDevice.METADATA_GTBS_CCCD;
+
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.IBluetoothStateChangeCallback;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
import android.util.Log;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.Utils;
import com.android.internal.annotations.VisibleForTesting;
import java.io.ByteArrayOutputStream;
@@ -35,6 +45,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.UUID;
public class TbsGatt {
@@ -131,9 +142,11 @@
private final GattCharacteristic mTerminationReasonCharacteristic;
private final GattCharacteristic mIncomingCallCharacteristic;
private final GattCharacteristic mCallFriendlyNameCharacteristic;
+ private List<BluetoothDevice> mSubscribers = new ArrayList<>();
private BluetoothGattServerProxy mBluetoothGattServer;
private Handler mHandler;
private Callback mCallback;
+ private AdapterService mAdapterService;
public static abstract class Callback {
@@ -144,6 +157,17 @@
}
TbsGatt(Context context) {
+ mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
+ "AdapterService shouldn't be null when creating MediaControlCattService");
+ IBluetoothManager mgr = BluetoothAdapter.getDefaultAdapter().getBluetoothManager();
+ if (mgr != null) {
+ try {
+ mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
mContext = context;
mBearerProviderNameCharacteristic = new GattCharacteristic(UUID_BEARER_PROVIDER_NAME,
BluetoothGattCharacteristic.PROPERTY_READ
@@ -252,11 +276,55 @@
return mContext;
}
+ private void removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device) {
+ List<ParcelUuid> uuidList;
+ byte[] gtbs_cccd = device.getMetadata(METADATA_GTBS_CCCD);
+
+ if ((gtbs_cccd == null) || (gtbs_cccd.length == 0)) {
+ uuidList = new ArrayList<ParcelUuid>();
+ } else {
+ uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gtbs_cccd)));
+
+ if (!uuidList.contains(charUuid)) {
+ Log.d(TAG, "Characteristic CCCD can't be removed (not cached): "
+ + charUuid.toString());
+ return;
+ }
+ }
+
+ uuidList.remove(charUuid);
+
+ if (!device.setMetadata(METADATA_GTBS_CCCD,
+ Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) {
+ Log.e(TAG, "Can't set CCCD for GTBS characteristic UUID: " + charUuid + ", (remove)");
+ }
+ }
+
+ private void addUuidToMetadata(ParcelUuid charUuid, BluetoothDevice device) {
+ List<ParcelUuid> uuidList;
+ byte[] gtbs_cccd = device.getMetadata(METADATA_GTBS_CCCD);
+
+ if ((gtbs_cccd == null) || (gtbs_cccd.length == 0)) {
+ uuidList = new ArrayList<ParcelUuid>();
+ } else {
+ uuidList = new ArrayList<>(Arrays.asList(Utils.byteArrayToUuid(gtbs_cccd)));
+
+ if (uuidList.contains(charUuid)) {
+ Log.d(TAG, "Characteristic CCCD already add: " + charUuid.toString());
+ return;
+ }
+ }
+
+ uuidList.add(charUuid);
+
+ if (!device.setMetadata(METADATA_GTBS_CCCD,
+ Utils.uuidsToByteArray(uuidList.toArray(new ParcelUuid[0])))) {
+ Log.e(TAG, "Can't set CCCD for GTBS characteristic UUID: " + charUuid + ", (add)");
+ }
+ }
+
/** Class that handles GATT characteristic notifications */
private class BluetoothGattCharacteristicNotifier {
-
- private List<BluetoothDevice> mSubscribers = new ArrayList<>();
-
public int setSubscriptionConfiguration(BluetoothDevice device, byte[] configuration) {
if (Arrays.equals(configuration, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
mSubscribers.remove(device);
@@ -455,6 +523,14 @@
return BluetoothGatt.GATT_FAILURE;
}
+ if (Arrays.equals(value, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
+ addUuidToMetadata(new ParcelUuid(characteristic.getUuid()), device);
+ } else if (Arrays.equals(value, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
+ removeUuidFromMetadata(new ParcelUuid(characteristic.getUuid()), device);
+ } else {
+ Log.e(TAG, "Not handled CCC value: " + Arrays.toString(value));
+ }
+
return characteristic.setSubscriptionConfiguration(device, value);
}
}
@@ -659,13 +735,56 @@
return UUID.fromString(UUID_PREFIX + uuid16 + UUID_SUFFIX);
}
+ private void restoreCccValuesForStoredDevices() {
+ BluetoothGattService gattService = mBluetoothGattServer.getService(UUID_GTBS);
+
+ for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
+ byte[] gtbs_cccd = device.getMetadata(METADATA_GTBS_CCCD);
+
+ if ((gtbs_cccd == null) || (gtbs_cccd.length == 0)) {
+ return;
+ }
+
+ List<ParcelUuid> uuidList = Arrays.asList(Utils.byteArrayToUuid(gtbs_cccd));
+
+ /* Restore CCCD values for device */
+ for (ParcelUuid uuid : uuidList) {
+ BluetoothGattCharacteristic characteristic =
+ gattService.getCharacteristic(uuid.getUuid());
+ if (characteristic == null) {
+ Log.e(TAG, "Invalid UUID stored in metadata: " + uuid.toString());
+ continue;
+ }
+
+ BluetoothGattDescriptor descriptor =
+ characteristic.getDescriptor(UUID_CLIENT_CHARACTERISTIC_CONFIGURATION);
+ if (descriptor == null) {
+ Log.e(TAG, "Invalid characteristic, does not include CCCD");
+ continue;
+ }
+
+ descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+ mSubscribers.add(device);
+ }
+ }
+ }
+
+ private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
+ new IBluetoothStateChangeCallback.Stub() {
+ public void onBluetoothStateChange(boolean up) {
+ if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
+ if (up) {
+ restoreCccValuesForStoredDevices();
+ }
+ }
+ };
+
/**
* Callback to handle incoming requests to the GATT server. All read/write requests for
* characteristics and descriptors are handled here.
*/
@VisibleForTesting
final BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
-
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
if (DBG) {
@@ -674,6 +793,8 @@
if (mCallback != null) {
mCallback.onServiceAdded(status == BluetoothGatt.GATT_SUCCESS);
}
+
+ restoreCccValuesForStoredDevices();
}
@Override
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
index 0854962..5574b87 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
@@ -26,6 +26,7 @@
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHeadset;
@@ -123,6 +124,8 @@
mMostRecentDevice = null;
when(mA2dpService.setActiveDevice(any())).thenReturn(true);
+ when(mHeadsetService.getHfpCallAudioPolicy(any())).thenReturn(
+ new BluetoothAudioPolicy.Builder().build());
when(mHeadsetService.setActiveDevice(any())).thenReturn(true);
when(mHearingAidService.setActiveDevice(any())).thenReturn(true);
when(mLeAudioService.setActiveDevice(any())).thenReturn(true);
@@ -310,6 +313,23 @@
}
/**
+ * A headset device with connecting audio policy set to NOT ALLOWED.
+ */
+ @Test
+ public void notAllowedConnectingPolicyHeadsetConnected_noSetActiveDevice() {
+ // setting connecting policy to NOT ALLOWED
+ when(mHeadsetService.getHfpCallAudioPolicy(mHeadsetDevice))
+ .thenReturn(new BluetoothAudioPolicy.Builder()
+ .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_NOT_ALLOWED)
+ .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .build());
+
+ headsetConnected(mHeadsetDevice);
+ verify(mHeadsetService, never()).setActiveDevice(mHeadsetDevice);
+ }
+
+ /**
* A combo (A2DP + Headset) device is connected. Then a Hearing Aid is connected.
*/
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
index 4d30966..a229499 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
@@ -280,6 +280,10 @@
.thenReturn(mBatteryStatsManager);
when(mMockContext.getSystemServiceName(BatteryStatsManager.class))
.thenReturn(Context.BATTERY_STATS_SERVICE);
+ when(mMockContext.getSharedPreferences(anyString(), anyInt()))
+ .thenReturn(InstrumentationRegistry.getTargetContext()
+ .getSharedPreferences("AdapterServiceTestPrefs", Context.MODE_PRIVATE));
+
when(mMockContext.getAttributionSource()).thenReturn(mAttributionSource);
doAnswer(invocation -> {
Object[] args = invocation.getArguments();
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/CompanionManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/CompanionManagerTest.java
new file mode 100644
index 0000000..41746f0
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/CompanionManagerTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 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 static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.HandlerThread;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.TestUtils;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CompanionManagerTest {
+
+ private static final String TEST_DEVICE = "11:22:33:44:55:66";
+
+ private AdapterProperties mAdapterProperties;
+ private Context mTargetContext;
+ private CompanionManager mCompanionManager;
+
+ private HandlerThread mHandlerThread;
+
+ @Mock
+ private AdapterService mAdapterService;
+ @Mock
+ SharedPreferences mSharedPreferences;
+ @Mock
+ SharedPreferences.Editor mEditor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ // Prepare the TestUtils
+ TestUtils.setAdapterService(mAdapterService);
+ // Start handler thread for this test
+ mHandlerThread = new HandlerThread("CompanionManagerTestHandlerThread");
+ mHandlerThread.start();
+ // Mock the looper
+ doReturn(mHandlerThread.getLooper()).when(mAdapterService).getMainLooper();
+ // Mock SharedPreferences
+ when(mSharedPreferences.edit()).thenReturn(mEditor);
+ doReturn(mSharedPreferences).when(mAdapterService).getSharedPreferences(eq(
+ CompanionManager.COMPANION_INFO), eq(Context.MODE_PRIVATE));
+ // Tell the AdapterService that it is a mock (see isMock documentation)
+ doReturn(true).when(mAdapterService).isMock();
+ // Use the resources in the instrumentation instead of the mocked AdapterService
+ when(mAdapterService.getResources()).thenReturn(mTargetContext.getResources());
+
+ // Must be called to initialize services
+ mCompanionManager = new CompanionManager(mAdapterService, null);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mHandlerThread.quit();
+ TestUtils.clearAdapterService(mAdapterService);
+ }
+
+ @Test
+ public void testLoadCompanionInfo_hasCompanionDeviceKey() {
+ loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_PRIMARY);
+ }
+
+ @Test
+ public void testLoadCompanionInfo_noCompanionDeviceSetButHaveBondedDevices_shouldNotCrash() {
+ BluetoothDevice[] devices = new BluetoothDevice[2];
+ doReturn(devices).when(mAdapterService).getBondedDevices();
+ doThrow(new IllegalArgumentException())
+ .when(mSharedPreferences)
+ .getInt(eq(CompanionManager.COMPANION_TYPE_KEY), anyInt());
+ mCompanionManager.loadCompanionInfo();
+ }
+
+ @Test
+ public void testIsCompanionDevice() {
+ loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_NONE);
+ Assert.assertTrue(mCompanionManager.isCompanionDevice(TEST_DEVICE));
+
+ loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_PRIMARY);
+ Assert.assertTrue(mCompanionManager.isCompanionDevice(TEST_DEVICE));
+
+ loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_SECONDARY);
+ Assert.assertTrue(mCompanionManager.isCompanionDevice(TEST_DEVICE));
+ }
+
+ @Test
+ public void testGetGattConnParameterPrimary() {
+ loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_PRIMARY);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+
+ loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_SECONDARY);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+
+ loadCompanionInfoHelper(TEST_DEVICE, CompanionManager.COMPANION_TYPE_NONE);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+ checkReasonableConnParameterHelper(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+ }
+
+ private void loadCompanionInfoHelper(String address, int companionType) {
+ doReturn(address)
+ .when(mSharedPreferences)
+ .getString(eq(CompanionManager.COMPANION_DEVICE_KEY), anyString());
+ doReturn(companionType)
+ .when(mSharedPreferences)
+ .getInt(eq(CompanionManager.COMPANION_TYPE_KEY), anyInt());
+ mCompanionManager.loadCompanionInfo();
+ }
+
+ private void checkReasonableConnParameterHelper(int priority) {
+ // Max/Min values from the Bluetooth spec Version 5.3 | Vol 4, Part E | 7.8.18
+ final int minInterval = 6; // 0x0006
+ final int maxInterval = 3200; // 0x0C80
+ final int minLatency = 0; // 0x0000
+ final int maxLatency = 499; // 0x01F3
+
+ int min = mCompanionManager.getGattConnParameters(
+ TEST_DEVICE, CompanionManager.GATT_CONN_INTERVAL_MIN,
+ priority);
+ int max = mCompanionManager.getGattConnParameters(
+ TEST_DEVICE, CompanionManager.GATT_CONN_INTERVAL_MAX,
+ priority);
+ int latency = mCompanionManager.getGattConnParameters(
+ TEST_DEVICE, CompanionManager.GATT_CONN_LATENCY,
+ priority);
+
+ Assert.assertTrue(max >= min);
+ Assert.assertTrue(max >= minInterval);
+ Assert.assertTrue(min >= minInterval);
+ Assert.assertTrue(max <= maxInterval);
+ Assert.assertTrue(min <= maxInterval);
+ Assert.assertTrue(latency >= minLatency);
+ Assert.assertTrue(latency <= maxLatency);
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
index 42e93e5..22df72c 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
@@ -6,6 +6,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAssignedNumbers;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
@@ -21,6 +22,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties;
import com.android.bluetooth.hfp.HeadsetHalConstants;
import org.junit.After;
@@ -517,6 +519,26 @@
Assert.assertNull(mRemoteDevices.getDeviceProperties(mDevice1));
}
+ @Test
+ public void testSetgetHfAudioPolicyForRemoteAg() {
+ // Verify that device property is null initially
+ Assert.assertNull(mRemoteDevices.getDeviceProperties(mDevice1));
+
+ mRemoteDevices.addDeviceProperties(Utils.getBytesFromAddress(TEST_BT_ADDR_1));
+
+ DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(mDevice1);
+ BluetoothAudioPolicy policies = new BluetoothAudioPolicy.Builder()
+ .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .build();
+ deviceProp.setHfAudioPolicyForRemoteAg(policies);
+
+ // Verify that the audio policy properties are set and get propperly
+ Assert.assertEquals(policies, mRemoteDevices.getDeviceProperties(mDevice1)
+ .getHfAudioPolicyForRemoteAg());
+ }
+
private static void verifyBatteryLevelChangedIntent(BluetoothDevice device, int batteryLevel,
ArgumentCaptor<Intent> intentArgument) {
verifyBatteryLevelChangedIntent(device, batteryLevel, intentArgument.getValue());
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index 28357b6..2bf7302 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -28,6 +28,7 @@
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.ContentValues;
@@ -387,6 +388,10 @@
value, true);
testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_LE_AUDIO,
value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_GMCS_CCCD,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_GTBS_CCCD,
+ value, true);
testSetGetCustomMetaCase(false, badKey, value, false);
// Device is in database
@@ -447,6 +452,24 @@
value, true);
testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_LE_AUDIO,
value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_GMCS_CCCD,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_GTBS_CCCD,
+ value, true);
+ }
+ @Test
+ public void testSetGetAudioPolicyMetaData() {
+ int badKey = 100;
+ BluetoothAudioPolicy value = new BluetoothAudioPolicy.Builder()
+ .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_NOT_ALLOWED)
+ .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .build();
+
+ // Device is not in database
+ testSetGetAudioPolicyMetadataCase(false, value, true);
+ // Device is in database
+ testSetGetAudioPolicyMetadataCase(true, value, true);
}
@Test
@@ -1143,7 +1166,7 @@
@Test
public void testDatabaseMigration_111_112() throws IOException {
String testString = "TEST STRING";
- // Create a database with version 109
+ // Create a database with version 111
SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 111);
// insert a device to the database
ContentValues device = new ContentValues();
@@ -1189,7 +1212,7 @@
@Test
public void testDatabaseMigration_113_114() throws IOException {
- // Create a database with version 112
+ // Create a database with version 113
SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 113);
// insert a device to the database
ContentValues device = new ContentValues();
@@ -1209,6 +1232,83 @@
}
}
+ @Test
+ public void testDatabaseMigration_114_115() throws IOException {
+ // Create a database with version 114
+ SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 114);
+ // insert a device to the database
+ ContentValues device = new ContentValues();
+ device.put("address", TEST_BT_ADDR);
+ device.put("migrated", false);
+ assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+ CoreMatchers.not(-1));
+
+ // Migrate database from 114 to 115
+ db.close();
+ db = testHelper.runMigrationsAndValidate(DB_NAME, 115, true,
+ MetadataDatabase.MIGRATION_114_115);
+ Cursor cursor = db.query("SELECT * FROM metadata");
+
+ assertHasColumn(cursor, "call_establish_audio_policy", true);
+ assertHasColumn(cursor, "connecting_time_audio_policy", true);
+ assertHasColumn(cursor, "in_band_ringtone_audio_policy", true);
+ while (cursor.moveToNext()) {
+ // Check the new columns was added with default value
+ assertColumnBlobData(cursor, "call_establish_audio_policy", null);
+ assertColumnBlobData(cursor, "connecting_time_audio_policy", null);
+ assertColumnBlobData(cursor, "in_band_ringtone_audio_policy", null);
+ }
+ }
+
+ @Test
+ public void testDatabaseMigration_115_116() throws IOException {
+ // Create a database with version 115
+ SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 115);
+ // insert a device to the database
+ ContentValues device = new ContentValues();
+ device.put("address", TEST_BT_ADDR);
+ device.put("migrated", false);
+ assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+ CoreMatchers.not(-1));
+
+ // Migrate database from 115 to 116
+ db.close();
+ db = testHelper.runMigrationsAndValidate(DB_NAME, 116, true,
+ MetadataDatabase.MIGRATION_115_116);
+ Cursor cursor = db.query("SELECT * FROM metadata");
+ assertHasColumn(cursor, "preferred_output_only_profile", true);
+ assertHasColumn(cursor, "preferred_duplex_profile", true);
+ while (cursor.moveToNext()) {
+ // Check the new columns was added with default value
+ assertColumnIntData(cursor, "preferred_output_only_profile", 0);
+ assertColumnIntData(cursor, "preferred_duplex_profile", 0);
+ }
+ }
+
+ @Test
+ public void testDatabaseMigration_116_117() throws IOException {
+ // Create a database with version 116
+ SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 116);
+ // insert a device to the database
+ ContentValues device = new ContentValues();
+ device.put("address", TEST_BT_ADDR);
+ device.put("migrated", false);
+ assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+ CoreMatchers.not(-1));
+ // Migrate database from 116 to 117
+ db.close();
+ db = testHelper.runMigrationsAndValidate(DB_NAME, 117, true,
+ MetadataDatabase.MIGRATION_116_117);
+ Cursor cursor = db.query("SELECT * FROM metadata");
+ assertHasColumn(cursor, "gmcs_cccd", true);
+ assertHasColumn(cursor, "gtbs_cccd", true);
+ while (cursor.moveToNext()) {
+ // Check the new columns was added with default value
+ assertColumnBlobData(cursor, "gmcs_cccd", null);
+ assertColumnBlobData(cursor, "gtbs_cccd", null);
+ }
+ }
+
/**
* Helper function to check whether the database has the expected column
*/
@@ -1379,4 +1479,38 @@
// Wait for clear database
TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
}
+
+ void testSetGetAudioPolicyMetadataCase(boolean stored,
+ BluetoothAudioPolicy policy, boolean expectedResult) {
+ BluetoothAudioPolicy testPolicy = new BluetoothAudioPolicy.Builder().build();
+ if (stored) {
+ Metadata data = new Metadata(TEST_BT_ADDR);
+ mDatabaseManager.mMetadataCache.put(TEST_BT_ADDR, data);
+ mDatabase.insert(data);
+ Assert.assertEquals(expectedResult,
+ mDatabaseManager.setAudioPolicyMetadata(mTestDevice, testPolicy));
+ }
+ Assert.assertEquals(expectedResult,
+ mDatabaseManager.setAudioPolicyMetadata(mTestDevice, policy));
+ if (expectedResult) {
+ // Check for callback and get value
+ Assert.assertEquals(policy,
+ mDatabaseManager.getAudioPolicyMetadata(mTestDevice));
+ } else {
+ Assert.assertNull(mDatabaseManager.getAudioPolicyMetadata(mTestDevice));
+ return;
+ }
+ // Wait for database update
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+ // Check whether the value is saved in database
+ restartDatabaseManagerHelper();
+ Assert.assertEquals(policy,
+ mDatabaseManager.getAudioPolicyMetadata(mTestDevice));
+
+ mDatabaseManager.factoryReset();
+ mDatabaseManager.mMetadataCache.clear();
+ // Wait for clear database
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+ }
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/115.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/115.json
new file mode 100644
index 0000000..5d576dc
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/115.json
@@ -0,0 +1,358 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 115,
+ "identityHash": "c61976c8f6248cefd19ef8f25f543e01",
+ "entities": [
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `bass_client_connection_policy` INTEGER, `battery_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, `spatial_audio` BLOB, `fastpair_customized` BLOB, `le_audio` BLOB, `call_establish_audio_policy` INTEGER, `connecting_time_audio_policy` INTEGER, `in_band_ringtone_audio_policy` INTEGER, PRIMARY KEY(`address`))",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrated",
+ "columnName": "migrated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpSupportsOptionalCodecs",
+ "columnName": "a2dpSupportsOptionalCodecs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpOptionalCodecsEnabled",
+ "columnName": "a2dpOptionalCodecsEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "last_active_time",
+ "columnName": "last_active_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "is_active_a2dp_device",
+ "columnName": "is_active_a2dp_device",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+ "columnName": "a2dp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+ "columnName": "a2dp_sink_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+ "columnName": "hfp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+ "columnName": "hfp_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+ "columnName": "hid_host_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+ "columnName": "pan_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+ "columnName": "pbap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+ "columnName": "pbap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_connection_policy",
+ "columnName": "map_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+ "columnName": "sap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+ "columnName": "hearing_aid_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hap_client_connection_policy",
+ "columnName": "hap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+ "columnName": "map_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+ "columnName": "le_audio_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+ "columnName": "volume_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+ "columnName": "csip_set_coordinator_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+ "columnName": "le_call_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.bass_client_connection_policy",
+ "columnName": "bass_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.battery_connection_policy",
+ "columnName": "battery_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.manufacturer_name",
+ "columnName": "manufacturer_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.model_name",
+ "columnName": "model_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.software_version",
+ "columnName": "software_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.hardware_version",
+ "columnName": "hardware_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.companion_app",
+ "columnName": "companion_app",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_icon",
+ "columnName": "main_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.is_untethered_headset",
+ "columnName": "is_untethered_headset",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_icon",
+ "columnName": "untethered_left_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_icon",
+ "columnName": "untethered_right_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_icon",
+ "columnName": "untethered_case_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_battery",
+ "columnName": "untethered_left_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_battery",
+ "columnName": "untethered_right_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_battery",
+ "columnName": "untethered_case_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_charging",
+ "columnName": "untethered_left_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_charging",
+ "columnName": "untethered_right_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_charging",
+ "columnName": "untethered_case_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+ "columnName": "enhanced_settings_ui_uri",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.device_type",
+ "columnName": "device_type",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_battery",
+ "columnName": "main_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_charging",
+ "columnName": "main_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_low_battery_threshold",
+ "columnName": "main_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+ "columnName": "untethered_left_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+ "columnName": "untethered_right_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+ "columnName": "untethered_case_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.spatial_audio",
+ "columnName": "spatial_audio",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.fastpair_customized",
+ "columnName": "fastpair_customized",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.le_audio",
+ "columnName": "le_audio",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.callEstablishAudioPolicy",
+ "columnName": "call_establish_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.connectingTimeAudioPolicy",
+ "columnName": "connecting_time_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.inBandRingtoneAudioPolicy",
+ "columnName": "in_band_ringtone_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "address"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c61976c8f6248cefd19ef8f25f543e01')"
+ ]
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/116.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/116.json
new file mode 100644
index 0000000..2e85b51
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/116.json
@@ -0,0 +1,370 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 116,
+ "identityHash": "0b8549de3acad8b14fe6f7198206ea02",
+ "entities": [
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `preferred_output_only_profile` INTEGER NOT NULL, `preferred_duplex_profile` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `bass_client_connection_policy` INTEGER, `battery_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, `spatial_audio` BLOB, `fastpair_customized` BLOB, `le_audio` BLOB, `call_establish_audio_policy` INTEGER, `connecting_time_audio_policy` INTEGER, `in_band_ringtone_audio_policy` INTEGER, PRIMARY KEY(`address`))",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrated",
+ "columnName": "migrated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpSupportsOptionalCodecs",
+ "columnName": "a2dpSupportsOptionalCodecs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpOptionalCodecsEnabled",
+ "columnName": "a2dpOptionalCodecsEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "last_active_time",
+ "columnName": "last_active_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "is_active_a2dp_device",
+ "columnName": "is_active_a2dp_device",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "preferred_output_only_profile",
+ "columnName": "preferred_output_only_profile",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "preferred_duplex_profile",
+ "columnName": "preferred_duplex_profile",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+ "columnName": "a2dp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+ "columnName": "a2dp_sink_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+ "columnName": "hfp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+ "columnName": "hfp_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+ "columnName": "hid_host_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+ "columnName": "pan_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+ "columnName": "pbap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+ "columnName": "pbap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_connection_policy",
+ "columnName": "map_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+ "columnName": "sap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+ "columnName": "hearing_aid_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hap_client_connection_policy",
+ "columnName": "hap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+ "columnName": "map_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+ "columnName": "le_audio_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+ "columnName": "volume_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+ "columnName": "csip_set_coordinator_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+ "columnName": "le_call_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.bass_client_connection_policy",
+ "columnName": "bass_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.battery_connection_policy",
+ "columnName": "battery_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.manufacturer_name",
+ "columnName": "manufacturer_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.model_name",
+ "columnName": "model_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.software_version",
+ "columnName": "software_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.hardware_version",
+ "columnName": "hardware_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.companion_app",
+ "columnName": "companion_app",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_icon",
+ "columnName": "main_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.is_untethered_headset",
+ "columnName": "is_untethered_headset",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_icon",
+ "columnName": "untethered_left_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_icon",
+ "columnName": "untethered_right_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_icon",
+ "columnName": "untethered_case_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_battery",
+ "columnName": "untethered_left_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_battery",
+ "columnName": "untethered_right_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_battery",
+ "columnName": "untethered_case_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_charging",
+ "columnName": "untethered_left_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_charging",
+ "columnName": "untethered_right_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_charging",
+ "columnName": "untethered_case_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+ "columnName": "enhanced_settings_ui_uri",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.device_type",
+ "columnName": "device_type",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_battery",
+ "columnName": "main_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_charging",
+ "columnName": "main_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_low_battery_threshold",
+ "columnName": "main_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+ "columnName": "untethered_left_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+ "columnName": "untethered_right_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+ "columnName": "untethered_case_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.spatial_audio",
+ "columnName": "spatial_audio",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.fastpair_customized",
+ "columnName": "fastpair_customized",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.le_audio",
+ "columnName": "le_audio",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.callEstablishAudioPolicy",
+ "columnName": "call_establish_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.connectingTimeAudioPolicy",
+ "columnName": "connecting_time_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.inBandRingtoneAudioPolicy",
+ "columnName": "in_band_ringtone_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "address"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0b8549de3acad8b14fe6f7198206ea02')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/117.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/117.json
new file mode 100644
index 0000000..d4c1f7e
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/117.json
@@ -0,0 +1,382 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 117,
+ "identityHash": "b3363a857e6d4f3ece8ba92d57d52c26",
+ "entities": [
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `preferred_output_only_profile` INTEGER NOT NULL, `preferred_duplex_profile` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `bass_client_connection_policy` INTEGER, `battery_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, `spatial_audio` BLOB, `fastpair_customized` BLOB, `le_audio` BLOB, `gmcs_cccd` BLOB, `gtbs_cccd` BLOB, `call_establish_audio_policy` INTEGER, `connecting_time_audio_policy` INTEGER, `in_band_ringtone_audio_policy` INTEGER, PRIMARY KEY(`address`))",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrated",
+ "columnName": "migrated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpSupportsOptionalCodecs",
+ "columnName": "a2dpSupportsOptionalCodecs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpOptionalCodecsEnabled",
+ "columnName": "a2dpOptionalCodecsEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "last_active_time",
+ "columnName": "last_active_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "is_active_a2dp_device",
+ "columnName": "is_active_a2dp_device",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "preferred_output_only_profile",
+ "columnName": "preferred_output_only_profile",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "preferred_duplex_profile",
+ "columnName": "preferred_duplex_profile",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+ "columnName": "a2dp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+ "columnName": "a2dp_sink_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+ "columnName": "hfp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+ "columnName": "hfp_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+ "columnName": "hid_host_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+ "columnName": "pan_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+ "columnName": "pbap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+ "columnName": "pbap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_connection_policy",
+ "columnName": "map_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+ "columnName": "sap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+ "columnName": "hearing_aid_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hap_client_connection_policy",
+ "columnName": "hap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+ "columnName": "map_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+ "columnName": "le_audio_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+ "columnName": "volume_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+ "columnName": "csip_set_coordinator_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+ "columnName": "le_call_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.bass_client_connection_policy",
+ "columnName": "bass_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.battery_connection_policy",
+ "columnName": "battery_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.manufacturer_name",
+ "columnName": "manufacturer_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.model_name",
+ "columnName": "model_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.software_version",
+ "columnName": "software_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.hardware_version",
+ "columnName": "hardware_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.companion_app",
+ "columnName": "companion_app",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_icon",
+ "columnName": "main_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.is_untethered_headset",
+ "columnName": "is_untethered_headset",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_icon",
+ "columnName": "untethered_left_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_icon",
+ "columnName": "untethered_right_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_icon",
+ "columnName": "untethered_case_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_battery",
+ "columnName": "untethered_left_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_battery",
+ "columnName": "untethered_right_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_battery",
+ "columnName": "untethered_case_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_charging",
+ "columnName": "untethered_left_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_charging",
+ "columnName": "untethered_right_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_charging",
+ "columnName": "untethered_case_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+ "columnName": "enhanced_settings_ui_uri",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.device_type",
+ "columnName": "device_type",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_battery",
+ "columnName": "main_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_charging",
+ "columnName": "main_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_low_battery_threshold",
+ "columnName": "main_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+ "columnName": "untethered_left_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+ "columnName": "untethered_right_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+ "columnName": "untethered_case_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.spatial_audio",
+ "columnName": "spatial_audio",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.fastpair_customized",
+ "columnName": "fastpair_customized",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.le_audio",
+ "columnName": "le_audio",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.gmcs_cccd",
+ "columnName": "gmcs_cccd",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.gtbs_cccd",
+ "columnName": "gtbs_cccd",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.callEstablishAudioPolicy",
+ "columnName": "call_establish_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.connectingTimeAudioPolicy",
+ "columnName": "connecting_time_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "audioPolicyMetadata.inBandRingtoneAudioPolicy",
+ "columnName": "in_band_ringtone_audio_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "address"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b3363a857e6d4f3ece8ba92d57d52c26')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
index 141ee85..8cdc684 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
@@ -40,6 +40,7 @@
import android.bluetooth.le.ScanSettings;
import android.content.AttributionSource;
import android.content.Context;
+import android.content.res.Resources;
import android.os.Binder;
import android.os.ParcelUuid;
import android.os.RemoteException;
@@ -53,6 +54,7 @@
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.CompanionManager;
import org.junit.After;
import org.junit.Assert;
@@ -99,7 +101,9 @@
private BluetoothAdapter mAdapter;
private AttributionSource mAttributionSource;
+ @Mock private Resources mResources;
@Mock private AdapterService mAdapterService;
+ private CompanionManager mBtCompanionManager;
@Before
public void setUp() throws Exception {
@@ -113,6 +117,15 @@
mAttributionSource = mAdapter.getAttributionSource();
mDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(REMOTE_DEVICE_ADDRESS);
+ when(mAdapterService.getResources()).thenReturn(mResources);
+ when(mResources.getInteger(anyInt())).thenReturn(0);
+ when(mAdapterService.getSharedPreferences(anyString(), anyInt()))
+ .thenReturn(InstrumentationRegistry.getTargetContext()
+ .getSharedPreferences("GattServiceTestPrefs", Context.MODE_PRIVATE));
+
+ mBtCompanionManager = new CompanionManager(mAdapterService, null);
+ doReturn(mBtCompanionManager).when(mAdapterService).getCompanionManager();
+
TestUtils.startService(mServiceRule, GattService.class);
mService = GattService.getGattService();
Assert.assertNotNull(mService);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
index c89e135..fb27a2a 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
@@ -26,6 +26,7 @@
import android.app.Activity;
import android.app.Instrumentation;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
@@ -181,6 +182,8 @@
.getBondState(any(BluetoothDevice.class));
doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
mAdapterService).getBondedDevices();
+ doReturn(new BluetoothAudioPolicy.Builder().build()).when(mAdapterService)
+ .getAudioPolicy(any(BluetoothDevice.class));
// Mock system interface
doNothing().when(mSystemInterface).stop();
when(mSystemInterface.getHeadsetPhoneState()).thenReturn(mPhoneState);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
index e071563..817ea8b 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
@@ -19,6 +19,7 @@
import static org.mockito.Mockito.*;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
@@ -116,6 +117,8 @@
Set<BluetoothDevice> keys = mStateMachines.keySet();
return keys.toArray(new BluetoothDevice[keys.size()]);
}).when(mAdapterService).getBondedDevices();
+ doReturn(new BluetoothAudioPolicy.Builder().build()).when(mAdapterService)
+ .getAudioPolicy(any(BluetoothDevice.class));
// Mock system interface
doNothing().when(mSystemInterface).stop();
when(mSystemInterface.getHeadsetPhoneState()).thenReturn(mPhoneState);
@@ -991,6 +994,42 @@
mHeadsetService.dump(sb);
}
+ @Test
+ public void testConnectDeviceNotAllowedInbandRingPolicy_InbandRingStatus() {
+ when(mDatabaseManager.getProfileConnectionPolicy(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+ Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
+ when(mStateMachines.get(mCurrentDevice).getDevice()).thenReturn(mCurrentDevice);
+ when(mStateMachines.get(mCurrentDevice).getConnectionState()).thenReturn(
+ BluetoothProfile.STATE_CONNECTED);
+ when(mStateMachines.get(mCurrentDevice).getConnectingTimestampMs()).thenReturn(
+ SystemClock.uptimeMillis());
+ Assert.assertEquals(Collections.singletonList(mCurrentDevice),
+ mHeadsetService.getConnectedDevices());
+ mHeadsetService.onConnectionStateChangedFromStateMachine(mCurrentDevice,
+ BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED);
+
+ when(mStateMachines.get(mCurrentDevice).getHfpCallAudioPolicy()).thenReturn(
+ new BluetoothAudioPolicy.Builder()
+ .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .build()
+ );
+ Assert.assertEquals(true, mHeadsetService.isInbandRingingEnabled());
+
+ when(mStateMachines.get(mCurrentDevice).getHfpCallAudioPolicy()).thenReturn(
+ new BluetoothAudioPolicy.Builder()
+ .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_NOT_ALLOWED)
+ .build()
+ );
+ Assert.assertEquals(false, mHeadsetService.isInbandRingingEnabled());
+ }
+
private void addConnectedDeviceHelper(BluetoothDevice device) {
mCurrentDevice = device;
when(mDatabaseManager.getProfileConnectionPolicy(any(BluetoothDevice.class),
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
index a3e58d9..025fe05 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
@@ -51,6 +51,7 @@
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.hamcrest.core.IsInstanceOf;
import org.junit.After;
@@ -86,6 +87,7 @@
private ArgumentCaptor<Intent> mIntentArgument = ArgumentCaptor.forClass(Intent.class);
@Mock private AdapterService mAdapterService;
+ @Mock private DatabaseManager mDatabaseManager;
@Mock private HeadsetService mHeadsetService;
@Mock private HeadsetSystemInterface mSystemInterface;
@Mock private AudioManager mAudioManager;
@@ -109,6 +111,9 @@
mAdapter = BluetoothAdapter.getDefaultAdapter();
// Get a device for testing
mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
+ // Get a database
+ doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
+ doReturn(true).when(mDatabaseManager).setAudioPolicyMetadata(anyObject(), anyObject());
// Spy on native interface
mNativeInterface = spy(HeadsetNativeInterface.getInstance());
doNothing().when(mNativeInterface).init(anyInt(), anyBoolean());
@@ -1437,6 +1442,22 @@
}
/**
+ * A test to validate received Android AT commands and processing
+ */
+ @Test
+ public void testProcessAndroidAt() {
+ setUpConnectedState();
+ // setup Audio Policy Feature
+ setUpAudioPolicy();
+ // receive and set android policy
+ mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
+ new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT,
+ "+ANDROID=1,1,1,1", mTestDevice));
+ verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
+ .setAudioPolicyMetadata(anyObject(), anyObject());
+ }
+
+ /**
* Setup Connecting State
* @return number of times mHeadsetService.sendBroadcastAsUser() has been invoked
*/
@@ -1550,4 +1571,12 @@
IsInstanceOf.instanceOf(HeadsetStateMachine.Disconnecting.class));
return numBroadcastsSent;
}
+
+ private void setUpAudioPolicy() {
+ mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
+ new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT,
+ "+ANDROID=?", mTestDevice));
+ verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseString(
+ anyObject(), anyString());
+ }
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
index e85dc17..bc335a5 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
@@ -16,6 +16,7 @@
package com.android.bluetooth.hfpclient;
+import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doReturn;
@@ -25,6 +26,8 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.Intent;
@@ -148,4 +151,17 @@
mService.dump(new StringBuilder());
}
+
+ @Test
+ public void testSetCallAudioPolicy() {
+ // Put mock state machine
+ BluetoothDevice device =
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:01:02:03:04:05");
+ mService.getStateMachineMap().put(device, mStateMachine);
+
+ mService.setAudioPolicy(device, new BluetoothAudioPolicy.Builder().build());
+
+ verify(mStateMachine, timeout(STANDARD_WAIT_MILLIS).times(1))
+ .setAudioPolicy(any(BluetoothAudioPolicy.class));
+ }
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
index 0aa98f2..f64cda7 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
@@ -13,6 +13,7 @@
import android.bluetooth.BluetoothAssignedNumbers;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
@@ -97,6 +98,7 @@
TestUtils.setAdapterService(mAdapterService);
mNativeInterface = spy(NativeInterface.getInstance());
+ doReturn(true).when(mNativeInterface).sendAndroidAt(anyObject(), anyString());
// This line must be called to make sure relevant objects are initialized properly
mAdapter = BluetoothAdapter.getDefaultAdapter();
@@ -201,6 +203,9 @@
slcEvent.valueInt2 = HeadsetClientHalConstants.PEER_FEAT_ECS;
slcEvent.device = mTestDevice;
mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent);
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+
+ setUpAndroidAt(false);
// Verify that one connection state broadcast is executed
ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
@@ -293,6 +298,9 @@
slcEvent.valueInt2 = HeadsetClientHalConstants.PEER_FEAT_ECS;
slcEvent.device = mTestDevice;
mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent);
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+
+ setUpAndroidAt(false);
verify(mHeadsetClientService,
timeout(STANDARD_WAIT_MILLIS).times(expectedBroadcastMultiplePermissionsIndex++))
@@ -396,6 +404,10 @@
/* Utility function to simulate SLC connection. */
private int setUpServiceLevelConnection(int startBroadcastIndex) {
+ return setUpServiceLevelConnection(startBroadcastIndex, false);
+ }
+
+ private int setUpServiceLevelConnection(int startBroadcastIndex, boolean androidAtSupported) {
// Trigger SLC connection
StackEvent slcEvent = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
slcEvent.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_SLC_CONNECTED;
@@ -403,16 +415,46 @@
slcEvent.valueInt2 |= HeadsetClientHalConstants.PEER_FEAT_HF_IND;
slcEvent.device = mTestDevice;
mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent);
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+
+ setUpAndroidAt(androidAtSupported);
+
ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(startBroadcastIndex))
.sendBroadcastMultiplePermissions(intentArgument.capture(),
any(String[].class), any(BroadcastOptions.class));
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
intentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+ Assert.assertThat(mHeadsetClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HeadsetClientStateMachine.Connected.class));
+
startBroadcastIndex++;
return startBroadcastIndex;
}
+ /**
+ * Set up and verify AT Android related commands and events.
+ * Make sure this method is invoked after SLC is setup.
+ */
+ private void setUpAndroidAt(boolean androidAtSupported) {
+ verify(mNativeInterface).sendAndroidAt(mTestDevice, "+ANDROID=?");
+ if (androidAtSupported) {
+ StackEvent unknownEvt = new StackEvent(StackEvent.EVENT_TYPE_UNKNOWN_EVENT);
+ unknownEvt.valueString = "+ANDROID: 1";
+ unknownEvt.device = mTestDevice;
+ mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, unknownEvt);
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+ verify(mHeadsetClientService).setAudioPolicyRemoteSupported(mTestDevice, true);
+ mHeadsetClientStateMachine.setAudioPolicyRemoteSupported(true);
+ } else {
+ // receive CMD_RESULT CME_ERROR due to remote not supporting Android AT
+ StackEvent cmdResEvt = new StackEvent(StackEvent.EVENT_TYPE_CMD_RESULT);
+ cmdResEvt.valueInt = StackEvent.CMD_RESULT_TYPE_CME_ERROR;
+ cmdResEvt.device = mTestDevice;
+ mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, cmdResEvt);
+ }
+ }
+
/* Utility function: supported AT command should lead to native call */
private void runSupportedVendorAtCommand(String atCommand, int vendorId) {
// Return true for priority.
@@ -991,4 +1033,44 @@
Assert.assertEquals(HeadsetClientStateMachine.getMessageName(unknownMessageInt),
"UNKNOWN(" + unknownMessageInt + ")");
}
+ /**
+ * Tests and verify behavior of the case where remote device doesn't support
+ * At Android but tries to send audio policy.
+ */
+ @Test
+ public void testAndroidAtRemoteNotSupported_StateTransition_setAudioPolicy() {
+ // Setup connection state machine to be in connected state
+ when(mHeadsetClientService.getConnectionPolicy(any(BluetoothDevice.class))).thenReturn(
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ int expectedBroadcastIndex = 1;
+
+ expectedBroadcastIndex = setUpHfpClientConnection(expectedBroadcastIndex);
+ expectedBroadcastIndex = setUpServiceLevelConnection(expectedBroadcastIndex);
+
+ BluetoothAudioPolicy dummyAudioPolicy = new BluetoothAudioPolicy.Builder().build();
+ mHeadsetClientStateMachine.setAudioPolicy(dummyAudioPolicy);
+ verify(mNativeInterface, never()).sendAndroidAt(mTestDevice, "+ANDROID:1,0,0,0");
+ }
+
+ @SmallTest
+ @Test
+ public void testSetGetCallAudioPolicy() {
+ // Return true for priority.
+ when(mHeadsetClientService.getConnectionPolicy(any(BluetoothDevice.class))).thenReturn(
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+
+ int expectedBroadcastIndex = 1;
+
+ expectedBroadcastIndex = setUpHfpClientConnection(expectedBroadcastIndex);
+ expectedBroadcastIndex = setUpServiceLevelConnection(expectedBroadcastIndex, true);
+
+ BluetoothAudioPolicy dummyAudioPolicy = new BluetoothAudioPolicy.Builder()
+ .setCallEstablishPolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .setConnectingTimePolicy(BluetoothAudioPolicy.POLICY_NOT_ALLOWED)
+ .setInBandRingtonePolicy(BluetoothAudioPolicy.POLICY_ALLOWED)
+ .build();
+
+ mHeadsetClientStateMachine.setAudioPolicy(dummyAudioPolicy);
+ verify(mNativeInterface).sendAndroidAt(mTestDevice, "+ANDROID=1,1,2,1");
+ }
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSettingsTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSettingsTest.java
index c1d7bc8..df751e3 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSettingsTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSettingsTest.java
@@ -37,6 +37,7 @@
import com.android.bluetooth.R;
import org.junit.After;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -52,11 +53,11 @@
@Before
public void setUp() {
+ Assume.assumeTrue("Ignore test when BluetoothMapService is not enabled",
+ BluetoothMapService.isEnabled());
enableActivity(true);
-
mIntent = new Intent();
mIntent.setClass(mTargetContext, BluetoothMapSettings.class);
-
mActivityScenario = ActivityScenario.launch(mIntent);
}
@@ -67,7 +68,6 @@
Thread.sleep(1_000);
mActivityScenario.close();
}
-
enableActivity(false);
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceTest.java
index 07e1266..93365e5 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientServiceTest.java
@@ -301,7 +301,7 @@
MceStateMachine sm = mock(MceStateMachine.class);
mService.getInstanceMap().put(mRemoteDevice, sm);
- Intent intent = new Intent(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+ Intent intent = new Intent(BluetoothDevice.ACTION_SDP_RECORD);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED);
intent.putExtra(BluetoothDevice.EXTRA_UUID, BluetoothUuid.MAS);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
index 3249a42..7c80bff 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
@@ -24,7 +24,6 @@
import android.bluetooth.IBluetoothMapClient;
import android.content.Context;
import android.os.UserHandle;
-import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
@@ -123,7 +122,6 @@
// is the statemachine created
Map<BluetoothDevice, MceStateMachine> map = mService.getInstanceMap();
- Log.d("MapClientTest", "map=" + map);
Assert.assertEquals(1, map.size());
Assert.assertNotNull(map.get(device));
diff --git a/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlGattServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlGattServiceTest.java
index ff7406a..d840126 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlGattServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlGattServiceTest.java
@@ -90,6 +90,7 @@
mAdapter = BluetoothAdapter.getDefaultAdapter();
doReturn(true).when(mMockGattServer).addService(any(BluetoothGattService.class));
+ doReturn(new BluetoothDevice[0]).when(mAdapterService).getBondedDevices();
mMcpService = new MediaControlGattService(mMockMcpService, mMockMcsCallbacks, TEST_CCID);
mMcpService.setBluetoothGattServerForTesting(mMockGattServer);
@@ -122,7 +123,7 @@
List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
devices.add(mCurrentDevice);
doReturn(devices).when(mMockGattServer).getConnectedDevices();
- mMcpService.setCcc(mCurrentDevice, characteristic.getUuid(), 0, value);
+ mMcpService.setCcc(mCurrentDevice, characteristic.getUuid(), 0, value, true);
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapUtilsTest.java b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapUtilsTest.java
index 6ea294f..875042f 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapUtilsTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapUtilsTest.java
@@ -48,7 +48,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -78,13 +77,13 @@
BluetoothMethodProxy.setInstanceForTesting(mProxy);
when(mContext.getResources()).thenReturn(mResources);
- initStaticFields();
+ clearStaticFields();
}
@After
public void tearDown() {
BluetoothMethodProxy.setInstanceForTesting(null);
- initStaticFields();
+ clearStaticFields();
}
@Test
@@ -310,15 +309,11 @@
}
}
- @Ignore("b/262486295")
@Test
public void updateSecondaryVersionCounter_whenContactsAreUpdated() {
MatrixCursor contactCursor = new MatrixCursor(
new String[] {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP});
contactCursor.addRow(new Object[] {"id1", Calendar.getInstance().getTimeInMillis()});
- contactCursor.addRow(new Object[] {"id2", Calendar.getInstance().getTimeInMillis()});
- contactCursor.addRow(new Object[] {"id3", Calendar.getInstance().getTimeInMillis()});
- contactCursor.addRow(new Object[] {"id4", Calendar.getInstance().getTimeInMillis()});
doReturn(contactCursor).when(mProxy).contentResolverQuery(
any(), eq(Contacts.CONTENT_URI), any(), any(), any(), any());
@@ -326,31 +321,28 @@
dataCursor.addRow(new Object[] {"id1", Phone.CONTENT_ITEM_TYPE, "01234567"});
dataCursor.addRow(new Object[] {"id1", Email.CONTENT_ITEM_TYPE, "android@android.com"});
dataCursor.addRow(new Object[] {"id1", StructuredPostal.CONTENT_ITEM_TYPE, "01234"});
- dataCursor.addRow(new Object[] {"id2", StructuredName.CONTENT_ITEM_TYPE, "And Roid"});
+ dataCursor.addRow(new Object[] {"id1", StructuredName.CONTENT_ITEM_TYPE, "And Roid"});
doReturn(dataCursor).when(mProxy).contentResolverQuery(
any(), eq(Data.CONTENT_URI), any(), any(), any(), any());
+ assertThat(BluetoothPbapUtils.sSecondaryVersionCounter).isEqualTo(0);
- HandlerThread handlerThread = new HandlerThread("BluetoothPbapUtilsTest");
- handlerThread.start();
- Handler handler = new Handler(handlerThread.getLooper());
+ BluetoothPbapUtils.sTotalContacts = 1;
+ BluetoothPbapUtils.setContactFields(BluetoothPbapUtils.TYPE_NAME, "id1",
+ "test_previous_name_before_update");
- try {
- BluetoothPbapUtils.sTotalContacts = 4;
+ BluetoothPbapUtils.updateSecondaryVersionCounter(mContext, null);
- BluetoothPbapUtils.updateSecondaryVersionCounter(mContext, handler);
-
- assertThat(BluetoothPbapUtils.sContactDataset).isNotEmpty();
- } finally {
- handlerThread.quit();
- }
+ assertThat(BluetoothPbapUtils.sSecondaryVersionCounter).isEqualTo(1);
}
- private static void initStaticFields() {
+ private static void clearStaticFields() {
BluetoothPbapUtils.sPrimaryVersionCounter = 0;
BluetoothPbapUtils.sSecondaryVersionCounter = 0;
BluetoothPbapUtils.sContactSet.clear();
+ BluetoothPbapUtils.sContactDataset.clear();
BluetoothPbapUtils.sTotalContacts = 0;
BluetoothPbapUtils.sTotalFields = 0;
BluetoothPbapUtils.sTotalSvcFields = 0;
+ BluetoothPbapUtils.sContactsLastUpdated = 0;
}
}
diff --git a/framework/java/android/bluetooth/BluetoothAudioPolicy.java b/framework/java/android/bluetooth/BluetoothAudioPolicy.java
new file mode 100644
index 0000000..873abcd
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothAudioPolicy.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 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 android.bluetooth;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents Bluetooth Audio Policies of a Handsfree (HF) device (if HFP is used)
+ * and Call Terminal (CT) device (if BLE Audio is used), which describes the
+ * preferences of allowing or disallowing audio based on the use cases. The HF/CT
+ * devices shall send objects of this class to send its preference to the AG/CG
+ * devices.
+ *
+ * <p>HF/CT side applications on can use {@link BluetoothDevice#setAudioPolicy}
+ * API to set and send a {@link BluetoothAudioPolicy} object containing the
+ * preference/policy values. This object will be stored in the memory of HF/CT
+ * side, will be send to the AG/CG side using Android Specific AT Commands and will
+ * be stored in the AG side memory and database.
+ *
+ * <p>HF/CT side API {@link BluetoothDevice#getAudioPolicy} can be used to retrieve
+ * the stored audio policies currently.
+ *
+ * <p>Note that the setter APIs of this class will only set the values of the
+ * object. To actually set the policies, API {@link BluetoothDevice#setAudioPolicy}
+ * must need to be invoked with the {@link BluetoothAudioPolicy} object.
+ *
+ * <p>Note that any API related to this feature should be used after configuring
+ * the support of the AG device and after checking whether the AG device supports
+ * this feature or not by invoking {@link BluetoothDevice#getAudioPolicyRemoteSupported}.
+ * Only after getting a {@link BluetoothAudioPolicy#FEATURE_SUPPORTED_BY_REMOTE} response
+ * from the API should the APIs related to this feature be used.
+ *
+ *
+ * @hide
+ */
+public final class BluetoothAudioPolicy implements Parcelable {
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = {"POLICY_"},
+ value = {
+ POLICY_UNCONFIGURED,
+ POLICY_ALLOWED,
+ POLICY_NOT_ALLOWED,
+ }
+ )
+ public @interface AudioPolicyValues{}
+
+ /**
+ * Audio behavior not configured for the policy.
+ *
+ * If a policy is set with this value, it means that the policy is not
+ * configured with a value yet and should not be used to make any decision.
+ * @hide
+ */
+ public static final int POLICY_UNCONFIGURED = 0;
+
+ /**
+ * Audio is preferred by HF device for the policy.
+ *
+ * If a policy is set with this value, then the HF device will prefer the
+ * audio for the policy use case. For example, if the Call Establish audio
+ * policy is set with this value, then the HF will prefer the audio
+ * during making or picking up a call.
+ * @hide
+ */
+ public static final int POLICY_ALLOWED = 1;
+
+ /**
+ * Audio is not preferred by HF device for the policy.
+ *
+ * If a policy is set with this value, then the HF device will not prefer the
+ * audio for the policy use case. For example, if the Call Establish audio
+ * policy is set with this value, then the HF will not prefer the audio
+ * during making or picking up a call.
+ * @hide
+ */
+ public static final int POLICY_NOT_ALLOWED = 2;
+
+ /**
+ * Remote support status of audio policy feature is unknown/unconfigured.
+ *
+ * @hide
+ */
+ public static final int FEATURE_UNCONFIGURED_BY_REMOTE = 0;
+
+ /**
+ * Remote support status of audio policy feature is supported.
+ *
+ * @hide
+ */
+ public static final int FEATURE_SUPPORTED_BY_REMOTE = 1;
+
+ /**
+ * Remote support status of audio policy feature is not supported.
+ *
+ * @hide
+ */
+ public static final int FEATURE_NOT_SUPPORTED_BY_REMOTE = 2;
+
+
+ @AudioPolicyValues private final int mCallEstablishPolicy;
+ @AudioPolicyValues private final int mConnectingTimePolicy;
+ @AudioPolicyValues private final int mInBandRingtonePolicy;
+
+ /**
+ * @hide
+ */
+ public BluetoothAudioPolicy(int callEstablishPolicy,
+ int connectingTimePolicy, int inBandRingtonePolicy) {
+ mCallEstablishPolicy = callEstablishPolicy;
+ mConnectingTimePolicy = connectingTimePolicy;
+ mInBandRingtonePolicy = inBandRingtonePolicy;
+ }
+
+ /**
+ * Get Call pick up audio policy.
+ *
+ * @return the call pick up audio policy value
+ *
+ */
+ public @AudioPolicyValues int getCallEstablishPolicy() {
+ return mCallEstablishPolicy;
+ }
+
+ /**
+ * Get during connection audio up policy.
+ *
+ * @return the during connection audio policy value
+ *
+ */
+ public @AudioPolicyValues int getConnectingTimePolicy() {
+ return mConnectingTimePolicy;
+ }
+
+ /**
+ * Get In band ringtone audio up policy.
+ *
+ * @return the in band ringtone audio policy value
+ *
+ */
+ public @AudioPolicyValues int getInBandRingtonePolicy() {
+ return mInBandRingtonePolicy;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("BluetoothAudioPolicy{");
+ builder.append("mCallEstablishPolicy: ");
+ builder.append(mCallEstablishPolicy);
+ builder.append(", mConnectingTimePolicy: ");
+ builder.append(mConnectingTimePolicy);
+ builder.append(", mInBandRingtonePolicy: ");
+ builder.append(mInBandRingtonePolicy);
+ builder.append("}");
+ return builder.toString();
+ }
+
+ /**
+ * {@link Parcelable.Creator} interface implementation.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothAudioPolicy>
+ CREATOR = new Parcelable.Creator<BluetoothAudioPolicy>() {
+ @Override
+ public BluetoothAudioPolicy createFromParcel(@NonNull Parcel in) {
+ return new BluetoothAudioPolicy(
+ in.readInt(), in.readInt(), in.readInt());
+ }
+
+ @Override
+ public BluetoothAudioPolicy[] newArray(int size) {
+ return new BluetoothAudioPolicy[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mCallEstablishPolicy);
+ out.writeInt(mConnectingTimePolicy);
+ out.writeInt(mInBandRingtonePolicy);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothAudioPolicy) {
+ BluetoothAudioPolicy other = (BluetoothAudioPolicy) o;
+ return (other.mCallEstablishPolicy == mCallEstablishPolicy
+ && other.mConnectingTimePolicy == mConnectingTimePolicy
+ && other.mInBandRingtonePolicy == mInBandRingtonePolicy);
+ }
+ return false;
+ }
+
+ /**
+ * Returns a hash representation of this BluetoothCodecConfig
+ * based on all the config values.
+ *
+ * @hide
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCallEstablishPolicy, mConnectingTimePolicy, mInBandRingtonePolicy);
+ }
+
+ /**
+ * Builder for {@link BluetoothAudioPolicy}.
+ * <p> By default, the audio policy values will be set to
+ * {@link BluetoothAudioPolicy#POLICY_UNCONFIGURED}.
+ */
+ public static final class Builder {
+ private int mCallEstablishPolicy = POLICY_UNCONFIGURED;
+ private int mConnectingTimePolicy = POLICY_UNCONFIGURED;
+ private int mInBandRingtonePolicy = POLICY_UNCONFIGURED;
+
+ public Builder() {
+
+ }
+
+ public Builder(@NonNull BluetoothAudioPolicy policies) {
+ mCallEstablishPolicy = policies.mCallEstablishPolicy;
+ mConnectingTimePolicy = policies.mConnectingTimePolicy;
+ mInBandRingtonePolicy = policies.mInBandRingtonePolicy;
+ }
+
+ /**
+ * Set Call Establish (pick up and answer) policy.
+ * <p>This policy is used to determine the audio preference when the
+ * HF device makes or answers a call. That is, if this device
+ * makes or answers a call, is the audio preferred by HF should be decided
+ * by this policy.
+ *
+ * @return reference to the current object
+ */
+ public @NonNull Builder setCallEstablishPolicy(
+ @AudioPolicyValues int callEstablishPolicy) {
+ mCallEstablishPolicy = callEstablishPolicy;
+ return this;
+ }
+
+ /**
+ * Set during connection audio up policy.
+ * <p>This policy is used to determine the audio preference when the
+ * HF device connects with the AG device. That is, when the
+ * HF device gets connected, should the HF become active and get audio
+ * is decided by this policy. This also covers the case of during a call.
+ * If the HF connects with the AG during an ongoing call, should the call
+ * audio be routed to the HF will be decided by this policy.
+ *
+ * @return reference to the current object
+ */
+ public @NonNull Builder setConnectingTimePolicy(
+ @AudioPolicyValues int connectingTimePolicy) {
+ mConnectingTimePolicy = connectingTimePolicy;
+ return this;
+ }
+
+ /**
+ * Set In band ringtone audio up policy.
+ * <p>This policy is used to determine the audio preference of the
+ * in band ringtone. That is, if there is an incoming call, should the
+ * inband ringtone audio be routed to the HF will be decided by this policy.
+ *
+ * @return reference to the current object
+ *
+ */
+ public @NonNull Builder setInBandRingtonePolicy(
+ @AudioPolicyValues int inBandRingtonePolicy) {
+ mInBandRingtonePolicy = inBandRingtonePolicy;
+ return this;
+ }
+
+ /**
+ * Build {@link BluetoothAudioPolicy}.
+ * @return new BluetoothAudioPolicy object
+ */
+ public @NonNull BluetoothAudioPolicy build() {
+ return new BluetoothAudioPolicy(
+ mCallEstablishPolicy, mConnectingTimePolicy, mInBandRingtonePolicy);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothDevice.java b/framework/java/android/bluetooth/BluetoothDevice.java
index f7ab590..5646efb 100644
--- a/framework/java/android/bluetooth/BluetoothDevice.java
+++ b/framework/java/android/bluetooth/BluetoothDevice.java
@@ -510,7 +510,9 @@
METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD,
METADATA_SPATIAL_AUDIO,
METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
- METADATA_LE_AUDIO})
+ METADATA_LE_AUDIO,
+ METADATA_GMCS_CCCD,
+ METADATA_GTBS_CCCD})
@Retention(RetentionPolicy.SOURCE)
public @interface MetadataKey{}
@@ -663,6 +665,21 @@
public static final int METADATA_ENHANCED_SETTINGS_UI_URI = 16;
/**
+ * @hide
+ */
+ public static final String COMPANION_TYPE_PRIMARY = "COMPANION_PRIMARY";
+
+ /**
+ * @hide
+ */
+ public static final String COMPANION_TYPE_SECONDARY = "COMPANION_SECONDARY";
+
+ /**
+ * @hide
+ */
+ public static final String COMPANION_TYPE_NONE = "COMPANION_NONE";
+
+ /**
* Type of the Bluetooth device, must be within the list of
* BluetoothDevice.DEVICE_TYPE_*
* Data type should be {@String} as {@link Byte} array.
@@ -742,6 +759,21 @@
*/
public static final int METADATA_LE_AUDIO = 26;
+ /**
+ * The UUIDs (16-bit) of registered to CCC characteristics from Media Control services.
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ public static final int METADATA_GMCS_CCCD = 27;
+
+ /**
+ * The UUIDs (16-bit) of registered to CCC characteristics from Telephony Bearer service.
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ public static final int METADATA_GTBS_CCCD = 28;
+
+ private static final int METADATA_MAX_KEY = METADATA_GTBS_CCCD;
/**
* Device type which is used in METADATA_DEVICE_TYPE
@@ -3210,7 +3242,149 @@
* @hide
*/
public static @MetadataKey int getMaxMetadataKey() {
- return METADATA_LE_AUDIO;
+ return METADATA_MAX_KEY;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = { "REMOTE_STATUS_" },
+ value = {
+ /** Remote support status of audio policy feature is unknown/unconfigured **/
+ BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE,
+ /** Remote support status of audio policy feature is supported **/
+ BluetoothAudioPolicy.FEATURE_SUPPORTED_BY_REMOTE,
+ /** Remote support status of audio policy feature is not supported **/
+ BluetoothAudioPolicy.FEATURE_NOT_SUPPORTED_BY_REMOTE,
+ }
+ )
+
+ public @interface AudioPolicyRemoteSupport {}
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED,
+ BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED,
+ })
+ public @interface AudioPolicyReturnValues{}
+
+ /**
+ * Returns whether the audio policy feature is supported by the remote.
+ * This requires a vendor specific command, so the API returns
+ * {@link BluetoothAudioPolicy#FEATURE_UNCONFIGURED_BY_REMOTE} to indicate the remote
+ * device has not yet relayed this information. After the internal configuration,
+ * the support status will be set to either
+ * {@link BluetoothAudioPolicy#FEATURE_NOT_SUPPORTED_BY_REMOTE} or
+ * {@link BluetoothAudioPolicy#FEATURE_SUPPORTED_BY_REMOTE}.
+ * The rest of the APIs related to this feature in both {@link BluetoothDevice}
+ * and {@link BluetoothAudioPolicy} should be invoked only after getting a
+ * {@link BluetoothAudioPolicy#FEATURE_SUPPORTED_BY_REMOTE} response from this API.
+ *
+ * @return if call audio policy feature is supported or not
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @AudioPolicyRemoteSupport int getAudioPolicyRemoteSupported() {
+ if (DBG) log("getAudioPolicyRemoteSupported()");
+ final IBluetooth service = getService();
+ final int defaultValue = BluetoothAudioPolicy.FEATURE_UNCONFIGURED_BY_REMOTE;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot retrieve audio policy support status.");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.getAudioPolicyRemoteSupported(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets call audio preferences and sends them to the remote device.
+ *
+ * @param policies call audio policy preferences
+ * @return whether audio policy was set successfully or not
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @AudioPolicyReturnValues int setAudioPolicy(@NonNull BluetoothAudioPolicy policies) {
+ if (DBG) log("setAudioPolicy");
+ final IBluetooth service = getService();
+ final int defaultValue = BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot set Audio Policy.");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
+ service.setAudioPolicy(this, policies, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the call audio preferences for the remote device.
+ * <p>Note that the caller should check if the feature is supported by
+ * invoking {@link BluetoothDevice#getAudioPolicyRemoteSupported} first.
+ * <p>This API will return null if
+ * 1. The bleutooth service is not started yet,
+ * 2. It is invoked for a device which is not bonded, or
+ * 3. The used transport, for example, HFP Client profile is not enabled or
+ * connected yet.
+ *
+ * @return call audio policy as {@link BluetoothAudioPolicy} object
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Nullable BluetoothAudioPolicy getAudioPolicy() {
+ if (DBG) log("getAudioPolicy");
+ final IBluetooth service = getService();
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot get Audio Policy.");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else {
+ try {
+ final SynchronousResultReceiver<BluetoothAudioPolicy>
+ recv = SynchronousResultReceiver.get();
+ service.getAudioPolicy(this, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return null;
}
/**
diff --git a/system/binder/android/bluetooth/BluetoothAudioPolicy.aidl b/system/binder/android/bluetooth/BluetoothAudioPolicy.aidl
new file mode 100644
index 0000000..d06c286
--- /dev/null
+++ b/system/binder/android/bluetooth/BluetoothAudioPolicy.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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 android.bluetooth;
+
+parcelable BluetoothAudioPolicy;
diff --git a/system/binder/android/bluetooth/IBluetooth.aidl b/system/binder/android/bluetooth/IBluetooth.aidl
index 68237c6..d55dbc1 100644
--- a/system/binder/android/bluetooth/IBluetooth.aidl
+++ b/system/binder/android/bluetooth/IBluetooth.aidl
@@ -25,6 +25,7 @@
import android.bluetooth.IBluetoothSocketManager;
import android.bluetooth.IBluetoothStateChangeCallback;
import android.bluetooth.BluetoothActivityEnergyInfo;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.IncomingRfcommSocketInfo;
@@ -267,6 +268,13 @@
oneway void allowLowLatencyAudio(in boolean allowed, in BluetoothDevice device, in SynchronousResultReceiver receiver);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void getAudioPolicyRemoteSupported(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void setAudioPolicy(in BluetoothDevice device, in BluetoothAudioPolicy policies, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void getAudioPolicy(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
oneway void startRfcommListener(String name, in ParcelUuid uuid, in PendingIntent intent, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
oneway void stopRfcommListener(in ParcelUuid uuid, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
diff --git a/system/binder/android/bluetooth/IBluetoothHeadsetClient.aidl b/system/binder/android/bluetooth/IBluetoothHeadsetClient.aidl
index 3e1e83b..d0adfdf 100644
--- a/system/binder/android/bluetooth/IBluetoothHeadsetClient.aidl
+++ b/system/binder/android/bluetooth/IBluetoothHeadsetClient.aidl
@@ -17,6 +17,7 @@
package android.bluetooth;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothAudioPolicy;
import android.bluetooth.BluetoothHeadsetClientCall;
import android.content.AttributionSource;
@@ -86,6 +87,7 @@
void setAudioRouteAllowed(in BluetoothDevice device, boolean allowed, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
void getAudioRouteAllowed(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
void sendVendorAtCommand(in BluetoothDevice device, int vendorId, String atCommand, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
diff --git a/system/bta/dm/bta_dm_act.cc b/system/bta/dm/bta_dm_act.cc
index 3cbb536..cb56089 100644
--- a/system/bta/dm/bta_dm_act.cc
+++ b/system/bta/dm/bta_dm_act.cc
@@ -353,8 +353,10 @@
* graceful shutdown.
*/
bta_dm_search_cb.search_timer = alarm_new("bta_dm_search.search_timer");
+ bool delay_close_gatt =
+ osi_property_get_bool("bluetooth.gatt.delay_close.enabled", true);
bta_dm_search_cb.gatt_close_timer =
- alarm_new("bta_dm_search.gatt_close_timer");
+ delay_close_gatt ? alarm_new("bta_dm_search.gatt_close_timer") : nullptr;
bta_dm_search_cb.pending_discovery_queue = fixed_queue_new(SIZE_MAX);
memset(&bta_dm_conn_srvcs, 0, sizeof(bta_dm_conn_srvcs));
@@ -4001,11 +4003,25 @@
bta_sys_sendmsg(p_msg);
if (conn_id != GATT_INVALID_CONN_ID) {
- /* start a GATT channel close delay timer */
- bta_sys_start_timer(bta_dm_search_cb.gatt_close_timer,
- BTA_DM_GATT_CLOSE_DELAY_TOUT,
- BTA_DM_DISC_CLOSE_TOUT_EVT, 0);
bta_dm_search_cb.pending_close_bda = bta_dm_search_cb.peer_bdaddr;
+ // Gatt will be close immediately if bluetooth.gatt.delay_close.enabled is
+ // set to false. If property is true / unset there will be a delay
+ if (bta_dm_search_cb.gatt_close_timer != nullptr) {
+ /* start a GATT channel close delay timer */
+ bta_sys_start_timer(bta_dm_search_cb.gatt_close_timer,
+ BTA_DM_GATT_CLOSE_DELAY_TOUT,
+ BTA_DM_DISC_CLOSE_TOUT_EVT, 0);
+ } else {
+ p_msg = (tBTA_DM_MSG*)osi_malloc(sizeof(tBTA_DM_MSG));
+ p_msg->hdr.event = BTA_DM_DISC_CLOSE_TOUT_EVT;
+ p_msg->hdr.layer_specific = 0;
+ bta_sys_sendmsg(p_msg);
+ }
+ } else {
+ if (bluetooth::common::init_flags::
+ bta_dm_clear_conn_id_on_client_close_is_enabled()) {
+ bta_dm_search_cb.conn_id = GATT_INVALID_CONN_ID;
+ }
}
bta_dm_search_cb.gatt_disc_active = false;
}
@@ -4138,7 +4154,8 @@
break;
case BTA_GATTC_CLOSE_EVT:
- LOG_DEBUG("BTA_GATTC_CLOSE_EVT reason = %d", p_data->close.reason);
+ LOG_INFO("BTA_GATTC_CLOSE_EVT reason = %d", p_data->close.reason);
+
/* in case of disconnect before search is completed */
if ((bta_dm_search_cb.state != BTA_DM_SEARCH_IDLE) &&
(bta_dm_search_cb.state != BTA_DM_SEARCH_ACTIVE) &&
diff --git a/system/bta/dm/bta_dm_main.cc b/system/bta/dm/bta_dm_main.cc
index e844790..59bf00c 100644
--- a/system/bta/dm/bta_dm_main.cc
+++ b/system/bta/dm/bta_dm_main.cc
@@ -61,8 +61,8 @@
*
******************************************************************************/
bool bta_dm_search_sm_execute(BT_HDR_RIGID* p_msg) {
- APPL_TRACE_EVENT("bta_dm_search_sm_execute state:%d, event:0x%x",
- bta_dm_search_cb.state, p_msg->event);
+ LOG_INFO("bta_dm_search_sm_execute state:%d, event:0x%x",
+ bta_dm_search_get_state(), p_msg->event);
tBTA_DM_MSG* message = (tBTA_DM_MSG*)p_msg;
switch (bta_dm_search_cb.state) {
@@ -123,6 +123,16 @@
bta_dm_search_cancel_notify();
bta_dm_execute_queued_request();
break;
+ case BTA_DM_DISC_CLOSE_TOUT_EVT:
+ if (bluetooth::common::init_flags::
+ bta_dm_clear_conn_id_on_client_close_is_enabled()) {
+ bta_dm_close_gatt_conn(message);
+ break;
+ }
+ [[fallthrough]];
+ default:
+ LOG_INFO("Received unexpected event 0x%x in state %d", p_msg->event,
+ bta_dm_search_cb.state);
}
break;
case BTA_DM_DISCOVER_ACTIVE:
@@ -145,6 +155,16 @@
case BTA_DM_API_DISCOVER_EVT:
bta_dm_queue_disc(message);
break;
+ case BTA_DM_DISC_CLOSE_TOUT_EVT:
+ if (bluetooth::common::init_flags::
+ bta_dm_clear_conn_id_on_client_close_is_enabled()) {
+ bta_dm_close_gatt_conn(message);
+ break;
+ }
+ [[fallthrough]];
+ default:
+ LOG_INFO("Received unexpected event 0x%x in state %d", p_msg->event,
+ bta_dm_search_cb.state);
}
break;
}
diff --git a/system/bta/hf_client/bta_hf_client_at.cc b/system/bta/hf_client/bta_hf_client_at.cc
index bb9eec3..7a710bf 100644
--- a/system/bta/hf_client/bta_hf_client_at.cc
+++ b/system/bta/hf_client/bta_hf_client_at.cc
@@ -20,11 +20,11 @@
#define LOG_TAG "bt_hf_client"
#include "bt_trace.h" // Legacy trace logging
-
#include "bta/hf_client/bta_hf_client_int.h"
#include "osi/include/allocator.h"
#include "osi/include/compat.h"
#include "osi/include/log.h"
+#include "osi/include/properties.h"
#include "stack/include/port_api.h"
/* Uncomment to enable AT traffic dumping */
@@ -124,7 +124,7 @@
tBTA_HF_CLIENT_AT_QCMD* new_cmd =
(tBTA_HF_CLIENT_AT_QCMD*)osi_malloc(sizeof(tBTA_HF_CLIENT_AT_QCMD));
- APPL_TRACE_DEBUG("%s", __func__);
+ APPL_TRACE_DEBUG("%s: cmd:%d", __func__, (int)cmd);
new_cmd->cmd = cmd;
new_cmd->buf_len = buf_len;
@@ -169,7 +169,7 @@
static void bta_hf_client_send_at(tBTA_HF_CLIENT_CB* client_cb,
tBTA_HF_CLIENT_AT_CMD cmd, const char* buf,
uint16_t buf_len) {
- APPL_TRACE_DEBUG("%s", __func__);
+ APPL_TRACE_DEBUG("%s %d", __func__, cmd);
if ((client_cb->at_cb.current_cmd == BTA_HF_CLIENT_AT_NONE ||
!client_cb->svc_conn) &&
!alarm_is_scheduled(client_cb->at_cb.hold_timer)) {
@@ -197,6 +197,7 @@
return;
}
+ APPL_TRACE_DEBUG("%s: busy! queued: %d", __func__, cmd);
bta_hf_client_queue_at(client_cb, cmd, buf, buf_len);
}
@@ -240,7 +241,8 @@
******************************************************************************/
static void bta_hf_client_handle_ok(tBTA_HF_CLIENT_CB* client_cb) {
- APPL_TRACE_DEBUG("%s", __func__);
+ APPL_TRACE_DEBUG("%s: current_cmd:%d", __func__,
+ client_cb->at_cb.current_cmd);
bta_hf_client_stop_at_resp_timer(client_cb);
@@ -265,6 +267,9 @@
case BTA_HF_CLIENT_AT_NONE:
bta_hf_client_stop_at_hold_timer(client_cb);
break;
+ case BTA_HF_CLIENT_AT_ANDROID:
+ bta_hf_client_at_result(client_cb, BTA_HF_CLIENT_AT_RESULT_OK, 0);
+ break;
default:
if (client_cb->send_at_reply) {
bta_hf_client_at_result(client_cb, BTA_HF_CLIENT_AT_RESULT_OK, 0);
@@ -280,7 +285,8 @@
static void bta_hf_client_handle_error(tBTA_HF_CLIENT_CB* client_cb,
tBTA_HF_CLIENT_AT_RESULT_TYPE type,
uint16_t cme) {
- APPL_TRACE_DEBUG("%s: %u %u", __func__, type, cme);
+ APPL_TRACE_DEBUG("%s: type:%u cme:%u current_cmd:%d", __func__, type, cme,
+ client_cb->at_cb.current_cmd);
bta_hf_client_stop_at_resp_timer(client_cb);
@@ -301,6 +307,9 @@
client_cb->send_at_reply = true;
}
break;
+ case BTA_HF_CLIENT_AT_ANDROID:
+ bta_hf_client_at_result(client_cb, type, cme);
+ break;
default:
if (client_cb->send_at_reply) {
bta_hf_client_at_result(client_cb, type, cme);
@@ -2139,19 +2148,19 @@
at_len = snprintf(buf, sizeof(buf), "AT+BIA=");
+ const int32_t position = osi_property_get_int32(
+ "bluetooth.headsetclient.disable_indicator.position", -1);
+
for (i = 0; i < BTA_HF_CLIENT_AT_INDICATOR_COUNT; i++) {
int sup = client_cb->at_cb.indicator_lookup[i] == -1 ? 0 : 1;
-/* If this value matches the position of SIGNAL in the indicators array,
- * then hardcode disable signal strength indicators.
- * indicator_lookup[i] points to the position in the bta_hf_client_indicators
- * array defined at the top of this file */
-#ifdef BTA_HF_CLIENT_INDICATOR_SIGNAL_POS
- if (client_cb->at_cb.indicator_lookup[i] ==
- BTA_HF_CLIENT_INDICATOR_SIGNAL_POS) {
+ /* If this value matches the position of SIGNAL in the indicators array,
+ * then hardcode disable signal strength indicators.
+ * indicator_lookup[i] points to the position in the
+ * bta_hf_client_indicators array defined at the top of this file */
+ if (client_cb->at_cb.indicator_lookup[i] == position) {
sup = 0;
}
-#endif
at_len += snprintf(buf + at_len, sizeof(buf) - at_len, "%u,", sup);
}
@@ -2185,6 +2194,22 @@
at_len);
}
+void bta_hf_client_send_at_android(tBTA_HF_CLIENT_CB* client_cb,
+ const char* str) {
+ char buf[BTA_HF_CLIENT_AT_MAX_LEN];
+ int at_len;
+
+ APPL_TRACE_DEBUG("%s", __func__);
+
+ at_len = snprintf(buf, sizeof(buf), "AT%s\r", str);
+ if (at_len < 0) {
+ APPL_TRACE_ERROR("%s: AT command Framing error", __func__);
+ return;
+ }
+
+ bta_hf_client_send_at(client_cb, BTA_HF_CLIENT_AT_ANDROID, buf, at_len);
+}
+
void bta_hf_client_at_init(tBTA_HF_CLIENT_CB* client_cb) {
alarm_free(client_cb->at_cb.resp_timer);
alarm_free(client_cb->at_cb.hold_timer);
@@ -2284,6 +2309,9 @@
case BTA_HF_CLIENT_AT_CMD_VENDOR_SPECIFIC_CMD:
bta_hf_client_send_at_vendor_specific_cmd(client_cb, p_val->str);
break;
+ case BTA_HF_CLIENT_AT_CMD_ANDROID:
+ bta_hf_client_send_at_android(client_cb, p_val->str);
+ break;
default:
APPL_TRACE_ERROR("Default case");
snprintf(buf, BTA_HF_CLIENT_AT_MAX_LEN,
diff --git a/system/bta/hf_client/bta_hf_client_int.h b/system/bta/hf_client/bta_hf_client_int.h
index 3c13e6c..cfa63bf 100755
--- a/system/bta/hf_client/bta_hf_client_int.h
+++ b/system/bta/hf_client/bta_hf_client_int.h
@@ -105,6 +105,7 @@
BTA_HF_CLIENT_AT_BIND_READ_ENABLED_IND,
BTA_HF_CLIENT_AT_BIEV,
BTA_HF_CLIENT_AT_VENDOR_SPECIFIC,
+ BTA_HF_CLIENT_AT_ANDROID,
};
/*****************************************************************************
diff --git a/system/bta/include/bta_hf_client_api.h b/system/bta/include/bta_hf_client_api.h
index f37be30..0b01da6 100644
--- a/system/bta/include/bta_hf_client_api.h
+++ b/system/bta/include/bta_hf_client_api.h
@@ -172,6 +172,7 @@
#define BTA_HF_CLIENT_AT_CMD_NREC 15
#define BTA_HF_CLIENT_AT_CMD_VENDOR_SPECIFIC_CMD 16
#define BTA_HF_CLIENT_AT_CMD_BIEV 17
+#define BTA_HF_CLIENT_AT_CMD_ANDROID 18
typedef uint8_t tBTA_HF_CLIENT_AT_CMD_TYPE;
diff --git a/system/bta/le_audio/devices.cc b/system/bta/le_audio/devices.cc
index f12c923..13cfa78 100644
--- a/system/bta/le_audio/devices.cc
+++ b/system/bta/le_audio/devices.cc
@@ -867,7 +867,8 @@
}
bool LeAudioDeviceGroup::IsReleasingOrIdle(void) {
- return target_state_ == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE;
+ return (target_state_ == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) ||
+ (current_state_ == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
}
bool LeAudioDeviceGroup::IsGroupStreamReady(void) {
diff --git a/system/bta/le_audio/state_machine.cc b/system/bta/le_audio/state_machine.cc
index d891850..49f80f5 100644
--- a/system/bta/le_audio/state_machine.cc
+++ b/system/bta/le_audio/state_machine.cc
@@ -665,6 +665,11 @@
*/
group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
group->SetTargetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+
+ /* Clear group pending status */
+ group->ClearPendingAvailableContextsChange();
+ group->ClearPendingConfiguration();
+
if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
ReleaseCisIds(group);
state_machine_callbacks_->StatusReportCb(group->group_id_,
@@ -892,6 +897,10 @@
* In such an event, there is need to notify upper layer about state
* from here.
*/
+ if (alarm_is_scheduled(watchdog_)) {
+ alarm_cancel(watchdog_);
+ }
+
if (current_group_state == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
LOG_INFO(
"Cises disconnected for group %d, we are good in Idle state.",
@@ -906,12 +915,11 @@
"Cises disconnected for group: %d, we are good in Configured "
"state, reconfig=%d.",
group->group_id_, reconfig);
+
if (reconfig) {
group->ClearPendingConfiguration();
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::CONFIGURED_BY_USER);
- /* No more transition for group */
- alarm_cancel(watchdog_);
} else {
/* This is Autonomous change if both, target and current state
* is CODEC_CONFIGURED
@@ -1873,6 +1881,8 @@
return;
}
+ if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
+
state_machine_callbacks_->StatusReportCb(
group->group_id_, GroupStreamStatus::CONFIGURED_AUTONOMOUS);
}
diff --git a/system/bta/le_audio/state_machine_test.cc b/system/bta/le_audio/state_machine_test.cc
index 18e8125..1cafe06 100644
--- a/system/bta/le_audio/state_machine_test.cc
+++ b/system/bta/le_audio/state_machine_test.cc
@@ -449,6 +449,11 @@
}
void TearDown() override {
+ /* Clear the alarm on tear down in case test case ends when the
+ * alarm is scheduled
+ */
+ alarm_cancel(nullptr);
+
iso_manager_->Stop();
mock_iso_manager_ = nullptr;
codec_manager_->Stop();
@@ -1238,6 +1243,9 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+ /* Cancel is called when group goes to streaming. */
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testConfigureCodecMulti) {
@@ -1280,6 +1288,9 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+ /* Cancel is called when group goes to streaming. */
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testConfigureQosSingle) {
@@ -1322,6 +1333,8 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testConfigureQosSingleRecoverCig) {
@@ -1368,6 +1381,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testConfigureQosMultiple) {
@@ -1413,6 +1427,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testStreamSingle) {
@@ -1463,6 +1478,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testStreamSkipEnablingSink) {
@@ -1511,6 +1527,8 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testStreamSkipEnablingSinkSource) {
@@ -1562,6 +1580,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testStreamMultipleConversational) {
@@ -1615,6 +1634,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testStreamMultiple) {
@@ -1667,6 +1687,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testUpdateMetadataMultiple) {
@@ -1722,6 +1743,9 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Make sure all devices get the metadata update
leAudioDevice = group->GetFirstDevice();
expected_devices_written = 0;
@@ -1741,6 +1765,9 @@
ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
group, static_cast<LeAudioContextType>(context_type),
metadata_context_type));
+
+ /* This is just update metadata - watchdog is not used */
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testDisableSingle) {
@@ -1781,6 +1808,11 @@
InjectInitialIdleNotification(group);
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::STREAMING));
+
// Start the configuration and stream Media content
LeAudioGroupStateMachine::Get()->StartStream(
group, static_cast<LeAudioContextType>(context_type),
@@ -1790,6 +1822,10 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Validate GroupStreamStatus
EXPECT_CALL(
mock_callbacks_,
@@ -1806,6 +1842,9 @@
// Check if group has transition to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+
+ testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testDisableMultiple) {
@@ -1857,6 +1896,8 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
// Validate GroupStreamStatus
EXPECT_CALL(
@@ -1874,6 +1915,8 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
+ testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testDisableBidirectional) {
@@ -1958,6 +2001,19 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
+ // Validate GroupStreamStatus
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::SUSPENDING));
+ EXPECT_CALL(
+ mock_callbacks_,
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::SUSPENDED));
+
// Suspend the stream
LeAudioGroupStateMachine::Get()->SuspendStream(group);
@@ -1966,6 +2022,9 @@
types::AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
ASSERT_EQ(removed_bidirectional, true);
ASSERT_EQ(removed_unidirectional, true);
+
+ testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testReleaseSingle) {
@@ -2012,7 +2071,8 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
-
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
// Validate GroupStreamStatus
EXPECT_CALL(
mock_callbacks_,
@@ -2027,6 +2087,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testReleaseCachingSingle) {
@@ -2090,12 +2151,17 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Stop the stream
LeAudioGroupStateMachine::Get()->StopStream(group);
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest,
@@ -2167,6 +2233,9 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Stop the stream
LeAudioGroupStateMachine::Get()->StopStream(group);
@@ -2174,6 +2243,9 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Start the configuration and stream Media content
LeAudioGroupStateMachine::Get()->StartStream(
group, static_cast<LeAudioContextType>(context_type),
@@ -2182,6 +2254,9 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
}
TEST_F(StateMachineTest,
@@ -2267,6 +2342,9 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Stop the stream
LeAudioGroupStateMachine::Get()->StopStream(group);
@@ -2274,6 +2352,9 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Start the configuration and stream Media content
context_type = kContextTypeMedia;
LeAudioGroupStateMachine::Get()->StartStream(
@@ -2283,6 +2364,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testReleaseMultiple) {
@@ -2332,6 +2414,9 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Validate GroupStreamStatus
EXPECT_CALL(
mock_callbacks_,
@@ -2346,6 +2431,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testReleaseBidirectional) {
@@ -2393,11 +2479,16 @@
ASSERT_EQ(group->GetState(),
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Stop the stream
LeAudioGroupStateMachine::Get()->StopStream(group);
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
}
TEST_F(StateMachineTest, testDisableAndReleaseBidirectional) {
@@ -2558,6 +2649,9 @@
/* Single disconnect as it is bidirectional Cis*/
EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(2);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
for (auto* device = group->GetFirstDevice(); device != nullptr;
device = group->GetNextDevice(device)) {
for (auto& ase : device->ases_) {
@@ -2581,6 +2675,8 @@
ASSERT_EQ(ase.state, types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
}
}
+
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, testAseAutonomousRelease2Devices) {
@@ -3120,6 +3216,9 @@
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Validate GroupStreamStatus
EXPECT_CALL(
mock_callbacks_,
@@ -3136,6 +3235,9 @@
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Restart stream
EXPECT_CALL(
mock_callbacks_,
@@ -3147,6 +3249,7 @@
group, context_type, types::AudioContexts(context_type));
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, BoundedHeadphonesConversationalToMediaChannelCount_2) {
@@ -3304,6 +3407,9 @@
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
// Validate GroupStreamStatus
EXPECT_CALL(
mock_callbacks_,
@@ -3319,6 +3425,8 @@
LeAudioGroupStateMachine::Get()->StopStream(group);
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
// Restart stream
EXPECT_CALL(
@@ -3331,6 +3439,7 @@
group, new_context_type, types::AudioContexts(new_context_type));
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, lateCisDisconnectedEvent_ConfiguredByUser) {
@@ -3383,6 +3492,9 @@
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
/* Prepare DisconnectCis mock to not symulate CisDisconnection */
ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());
@@ -3404,6 +3516,8 @@
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
+
EXPECT_CALL(mock_callbacks_,
StatusReportCb(
leaudio_group_id,
@@ -3412,6 +3526,7 @@
// Inject CIS and ACL disconnection of first device
InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, lateCisDisconnectedEvent_AutonomousConfigured) {
@@ -3464,6 +3579,9 @@
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
+
/* Prepare DisconnectCis mock to not symulate CisDisconnection */
ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());
@@ -3489,6 +3607,8 @@
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
+
EXPECT_CALL(
mock_callbacks_,
StatusReportCb(
@@ -3498,6 +3618,7 @@
// Inject CIS and ACL disconnection of first device
InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
TEST_F(StateMachineTest, lateCisDisconnectedEvent_Idle) {
@@ -3550,6 +3671,8 @@
types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
+ mock_function_count_map["alarm_cancel"] = 0;
/* Prepare DisconnectCis mock to not symulate CisDisconnection */
ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());
@@ -3569,6 +3692,7 @@
// Check if group has transitioned to a proper state
ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
+ ASSERT_EQ(0, mock_function_count_map["alarm_cancel"]);
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
@@ -3579,6 +3703,7 @@
// Inject CIS and ACL disconnection of first device
InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
+ ASSERT_EQ(1, mock_function_count_map["alarm_cancel"]);
}
} // namespace internal
} // namespace le_audio
diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc
index dd608b1..196816e 100644
--- a/system/bta/vc/vc.cc
+++ b/system/bta/vc/vc.cc
@@ -102,6 +102,21 @@
volume_control_devices_.Add(address, true);
} else {
device->connecting_actively = true;
+
+ if (device->IsConnected()) {
+ LOG(WARNING) << __func__ << ": address=" << address
+ << ", connection_id=" << device->connection_id
+ << " already connected.";
+
+ if (device->IsReady()) {
+ callbacks_->OnConnectionState(ConnectionState::CONNECTED,
+ device->address);
+ } else {
+ OnGattConnected(GATT_SUCCESS, device->connection_id, gatt_if_,
+ device->address, BT_TRANSPORT_LE, GATT_MAX_MTU_SIZE);
+ }
+ return;
+ }
}
BTA_GATTC_Open(gatt_if_, address, BTM_BLE_DIRECT_CONNECTION, false);
@@ -628,13 +643,22 @@
return;
}
+ if (!device->IsConnected()) {
+ LOG(ERROR) << __func__
+ << " Skipping disconnect of the already disconnected device, "
+ "connection_id="
+ << loghex(connection_id);
+ return;
+ }
+
// If we get here, it means, device has not been exlicitly disconnected.
bool device_ready = device->IsReady();
device_cleanup_helper(device, device->connecting_actively);
if (device_ready) {
- volume_control_devices_.Add(remote_bda, true);
+ device->first_connection = true;
+ device->connecting_actively = true;
/* Add device into BG connection to accept remote initiated connection */
BTA_GATTC_Open(gatt_if_, remote_bda, BTM_BLE_BKG_CONNECT_ALLOW_LIST,
@@ -1068,7 +1092,6 @@
if (notify)
callbacks_->OnConnectionState(ConnectionState::DISCONNECTED,
device->address);
- volume_control_devices_.Remove(device->address);
}
void devices_control_point_helper(std::vector<RawAddress>& devices,
diff --git a/system/bta/vc/vc_test.cc b/system/bta/vc/vc_test.cc
index 9faa319..ca1bb02 100644
--- a/system/bta/vc/vc_test.cc
+++ b/system/bta/vc/vc_test.cc
@@ -547,6 +547,55 @@
TestAppUnregister();
}
+TEST_F(VolumeControlTest, test_reconnect_after_interrupted_discovery) {
+ const RawAddress test_address = GetTestAddress(0);
+
+ // Initial connection - no callback calls yet as we want to disconnect in the
+ // middle
+ SetSampleDatabaseVOCS(1);
+ TestAppRegister();
+ TestConnect(test_address);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, 2)).Times(0);
+ GetConnectedEvent(test_address, 1);
+ Mock::VerifyAndClearExpectations(callbacks.get());
+
+ // Remote disconnects in the middle of the service discovery
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+ GetDisconnectedEvent(test_address, 1);
+ Mock::VerifyAndClearExpectations(callbacks.get());
+
+ // This time let the service discovery pass
+ ON_CALL(gatt_interface, ServiceSearchRequest(_, _))
+ .WillByDefault(Invoke(
+ [&](uint16_t conn_id, const bluetooth::Uuid* p_srvc_uuid) -> void {
+ if (*p_srvc_uuid == kVolumeControlUuid)
+ GetSearchCompleteEvent(conn_id);
+ }));
+
+ // Remote is being connected by another GATT client
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, 2));
+ GetConnectedEvent(test_address, 1);
+ Mock::VerifyAndClearExpectations(callbacks.get());
+
+ // Request connect when the remote was already connected by another service
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, 2)).Times(0);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ VolumeControl::Get()->Connect(test_address);
+ // The GetConnectedEvent(test_address, 1); should not be triggered here, since
+ // GATT implementation will not send this event for the already connected
+ // device
+ Mock::VerifyAndClearExpectations(callbacks.get());
+
+ TestAppUnregister();
+}
+
TEST_F(VolumeControlTest, test_add_from_storage) {
TestAppRegister();
TestAddFromStorage(GetTestAddress(0), true);
diff --git a/system/btif/co/bta_hh_co.cc b/system/btif/co/bta_hh_co.cc
index 6115627..90fe1df 100644
--- a/system/btif/co/bta_hh_co.cc
+++ b/system/btif/co/bta_hh_co.cc
@@ -673,16 +673,15 @@
ev.type = UHID_FEATURE_ANSWER;
ev.u.feature_answer.id = *get_rpt_id;
ev.u.feature_answer.err = status;
- ev.u.feature_answer.size = len - GET_RPT_RSP_OFFSET;
+ ev.u.feature_answer.size = len;
osi_free(get_rpt_id);
- if (len > GET_RPT_RSP_OFFSET) {
- if (len - GET_RPT_RSP_OFFSET > UHID_DATA_MAX) {
+ if (len > 0) {
+ if (len > UHID_DATA_MAX) {
APPL_TRACE_WARNING("%s: Report size greater than allowed size",
__func__);
return;
}
- memcpy(ev.u.feature_answer.data, p_rpt + GET_RPT_RSP_OFFSET,
- len - GET_RPT_RSP_OFFSET);
+ memcpy(ev.u.feature_answer.data, p_rpt + GET_RPT_RSP_OFFSET, len);
uhid_write(p_dev->fd, &ev);
}
}
diff --git a/system/btif/src/btif_hf_client.cc b/system/btif/src/btif_hf_client.cc
index b7a93fa..177ea7c 100644
--- a/system/btif/src/btif_hf_client.cc
+++ b/system/btif/src/btif_hf_client.cc
@@ -743,6 +743,27 @@
return BT_STATUS_SUCCESS;
}
+/*******************************************************************************
+ *
+ * Function send_hfp_audio_policy
+ *
+ * Description Send requested audio policies to remote device.
+ *
+ * Returns bt_status_t
+ *
+ ******************************************************************************/
+static bt_status_t send_android_at(const RawAddress* bd_addr, const char* arg) {
+ btif_hf_client_cb_t* cb = btif_hf_client_get_cb_by_bda(*bd_addr);
+ if (cb == NULL || !is_connected(cb)) return BT_STATUS_FAIL;
+
+ CHECK_BTHF_CLIENT_SLC_CONNECTED(cb);
+
+ BTIF_TRACE_EVENT("%s: val1 %s", __func__, arg);
+ BTA_HfClientSendAT(cb->handle, BTA_HF_CLIENT_AT_CMD_ANDROID, 0, 0, arg);
+
+ return BT_STATUS_SUCCESS;
+}
+
static const bthf_client_interface_t bthfClientInterface = {
.size = sizeof(bthf_client_interface_t),
.init = init,
@@ -763,6 +784,7 @@
.request_last_voice_tag_number = request_last_voice_tag_number,
.cleanup = cleanup,
.send_at_cmd = send_at_cmd,
+ .send_android_at = send_android_at,
};
static void process_ind_evt(tBTA_HF_CLIENT_IND* ind) {
diff --git a/system/gd/common/init_flags.fbs b/system/gd/common/init_flags.fbs
index d1d8c12..b6dbe0a 100644
--- a/system/gd/common/init_flags.fbs
+++ b/system/gd/common/init_flags.fbs
@@ -14,6 +14,7 @@
asynchronously_start_l2cap_coc_is_enabled:bool (privacy:"Any");
btaa_hci_is_enabled:bool (privacy:"Any");
+ bta_dm_clear_conn_id_on_client_close_is_enabled:bool (privacy:"Any");
btm_dm_flush_discovery_queue_on_search_cancel_is_enabled:bool (privacy:"Any");
finite_att_timeout_is_enabled:bool (privacy:"Any");
gatt_robust_caching_client_is_enabled:bool (privacy:"Any");
diff --git a/system/gd/dumpsys/bundler/Android.bp b/system/gd/dumpsys/bundler/Android.bp
index 3754611..41f5549 100644
--- a/system/gd/dumpsys/bundler/Android.bp
+++ b/system/gd/dumpsys/bundler/Android.bp
@@ -69,9 +69,8 @@
],
}
-cc_test {
+cc_test_host {
name: "bluetooth_flatbuffer_bundler_test",
- host_supported: true,
srcs: [
":BluetoothFlatbufferBundlerTestSources",
],
diff --git a/system/gd/dumpsys/init_flags.cc b/system/gd/dumpsys/init_flags.cc
index 79e1d8d..08d3bf9 100644
--- a/system/gd/dumpsys/init_flags.cc
+++ b/system/gd/dumpsys/init_flags.cc
@@ -35,6 +35,8 @@
builder.add_asynchronously_start_l2cap_coc_is_enabled(initFlags::asynchronously_start_l2cap_coc_is_enabled());
builder.add_btaa_hci_is_enabled(initFlags::btaa_hci_is_enabled());
+ builder.add_bta_dm_clear_conn_id_on_client_close_is_enabled(
+ initFlags::bta_dm_clear_conn_id_on_client_close_is_enabled());
builder.add_btm_dm_flush_discovery_queue_on_search_cancel_is_enabled(
initFlags::btm_dm_flush_discovery_queue_on_search_cancel_is_enabled());
builder.add_finite_att_timeout_is_enabled(initFlags::finite_att_timeout_is_enabled());
diff --git a/system/gd/rust/common/src/init_flags.rs b/system/gd/rust/common/src/init_flags.rs
index 21e0c41..8e06df7 100644
--- a/system/gd/rust/common/src/init_flags.rs
+++ b/system/gd/rust/common/src/init_flags.rs
@@ -207,6 +207,7 @@
flags: {
asynchronously_start_l2cap_coc = true,
btaa_hci = true,
+ bta_dm_clear_conn_id_on_client_close = true,
btm_dm_flush_discovery_queue_on_search_cancel,
finite_att_timeout = true,
gatt_robust_caching_client = true,
diff --git a/system/gd/rust/shim/src/init_flags.rs b/system/gd/rust/shim/src/init_flags.rs
index 071d658..46c379f 100644
--- a/system/gd/rust/shim/src/init_flags.rs
+++ b/system/gd/rust/shim/src/init_flags.rs
@@ -6,6 +6,7 @@
fn asynchronously_start_l2cap_coc_is_enabled() -> bool;
fn btaa_hci_is_enabled() -> bool;
+ fn bta_dm_clear_conn_id_on_client_close_is_enabled() -> bool;
fn btm_dm_flush_discovery_queue_on_search_cancel_is_enabled() -> bool;
fn finite_att_timeout_is_enabled() -> bool;
fn gatt_robust_caching_client_is_enabled() -> bool;
diff --git a/system/include/hardware/bt_hf_client.h b/system/include/hardware/bt_hf_client.h
index 7263805..bf8b1bb 100644
--- a/system/include/hardware/bt_hf_client.h
+++ b/system/include/hardware/bt_hf_client.h
@@ -392,6 +392,9 @@
/** Send AT Command. */
bt_status_t (*send_at_cmd)(const RawAddress* bd_addr, int cmd, int val1,
int val2, const char* arg);
+
+ /** Send hfp audio policy to remote */
+ bt_status_t (*send_android_at)(const RawAddress* bd_addr, const char* arg);
} bthf_client_interface_t;
__END_DECLS
diff --git a/system/stack/sdp/sdp_discovery.cc b/system/stack/sdp/sdp_discovery.cc
index 22d6e7c..9988c54 100644
--- a/system/stack/sdp/sdp_discovery.cc
+++ b/system/stack/sdp/sdp_discovery.cc
@@ -337,7 +337,7 @@
uint8_t* p_end;
uint8_t type;
- if (p_ccb->p_db->raw_data) {
+ if (p_ccb->p_db && p_ccb->p_db->raw_data) {
cpy_len = p_ccb->p_db->raw_size - p_ccb->p_db->raw_used;
list_len = p_ccb->list_len;
p = &p_ccb->rsp_list[0];
diff --git a/system/test/mock/mock_bta_gatts_api.cc b/system/test/mock/mock_bta_gatts_api.cc
index 68380e4..ba86ae5 100644
--- a/system/test/mock/mock_bta_gatts_api.cc
+++ b/system/test/mock/mock_bta_gatts_api.cc
@@ -85,3 +85,4 @@
BTA_GATTS_AddServiceCb cb) {
mock_function_count_map[__func__]++;
}
+void BTA_GATTS_InitBonded(void) { mock_function_count_map[__func__]++; }
diff --git a/system/test/mock/mock_stack_btm_dev.cc b/system/test/mock/mock_stack_btm_dev.cc
index 8a65ab8..bfd3081 100644
--- a/system/test/mock/mock_stack_btm_dev.cc
+++ b/system/test/mock/mock_stack_btm_dev.cc
@@ -112,3 +112,9 @@
void btm_dev_consolidate_existing_connections(const RawAddress& bd_addr) {
mock_function_count_map[__func__]++;
}
+void BTM_SecDump(const std::string& label) {
+ mock_function_count_map[__func__]++;
+}
+void BTM_SecDumpDev(const RawAddress& bd_addr) {
+ mock_function_count_map[__func__]++;
+}