Loading android/app/src/com/android/bluetooth/btservice/AdapterService.java +67 −0 Original line number Diff line number Diff line Loading @@ -4062,6 +4062,73 @@ public class AdapterService extends Service { enforceBluetoothPrivilegedPermission(service); Utils.setForegroundUserId(userId); } @Override public void setPreferredAudioProfiles(BluetoothDevice device, Bundle modeToProfileBundle, AttributionSource source, SynchronousResultReceiver receiver) { try { receiver.send(setPreferredAudioProfiles(device, modeToProfileBundle, source)); } catch (RuntimeException e) { receiver.propagateException(e); } } private int setPreferredAudioProfiles(BluetoothDevice device, Bundle modeToProfileBundle, AttributionSource source) { AdapterService service = getService(); if (service == null) { return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED; } if (!callerIsSystemOrActiveOrManagedUser(service, TAG, "setPreferredAudioProfiles")) { return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED; } if (device == null) { throw new IllegalArgumentException("device cannot be null"); } if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { throw new IllegalArgumentException("device cannot have an invalid address"); } if (!Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION; } enforceBluetoothPrivilegedPermission(service); return service.mDatabaseManager.setPreferredAudioProfiles(device, modeToProfileBundle); } @Override public void getPreferredAudioProfiles(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver) { try { receiver.send(getPreferredAudioProfiles(device, source)); } catch (RuntimeException e) { receiver.propagateException(e); } } private Bundle getPreferredAudioProfiles(BluetoothDevice device, AttributionSource source) { AdapterService service = getService(); if (service == null) { return Bundle.EMPTY; } if (!callerIsSystemOrActiveOrManagedUser(service, TAG, "getPreferredAudioProfiles")) { throw new IllegalStateException("Caller is not the system or part of the " + "active/managed user"); } if (device == null) { throw new IllegalArgumentException("device cannot be null"); } if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { throw new IllegalArgumentException("device cannot have an invalid address"); } if (!Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { return Bundle.EMPTY; } enforceBluetoothPrivilegedPermission(service); return service.mDatabaseManager.getPreferredAudioProfiles(device); } } // ----API Methods-------- Loading android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java +87 −0 Original line number Diff line number Diff line Loading @@ -24,12 +24,14 @@ import android.bluetooth.BluetoothAudioPolicy; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.BluetoothStatusCodes; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; Loading Loading @@ -771,6 +773,91 @@ public class DatabaseManager { } } /** * Sets the preferred profile for the supplied audio modes. See * {@link BluetoothDevice#setPreferredAudioProfiles(Bundle)} for more details. * * @param device is the remote device for which we are setting the preferred audio profiles * @param modeToProfileBundle contains the preferred profile * @return */ public int setPreferredAudioProfiles(BluetoothDevice device, Bundle modeToProfileBundle) { synchronized (mMetadataCache) { if (device == null) { Log.e(TAG, "setPreferredAudioProfiles: device is null"); throw new IllegalArgumentException("setPreferredAudioProfiles: device is null"); } String address = device.getAddress(); if (!mMetadataCache.containsKey(address)) { return BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED; } // Updates preferred audio profiles for the device Metadata metadata = mMetadataCache.get(address); int outputProfile = modeToProfileBundle.getInt(BluetoothDevice.AUDIO_MODE_OUTPUT_ONLY); int duplexProfile = modeToProfileBundle.getInt(BluetoothDevice.AUDIO_MODE_DUPLEX); if (outputProfile != 0) { Log.i(TAG, "setPreferredAudioProfiles: Updating output only audio profile for " + "device: " + device + " to " + BluetoothProfile.getProfileName(outputProfile)); metadata.preferred_output_only_profile = outputProfile; } if (duplexProfile != 0) { Log.i(TAG, "setPreferredAudioProfiles: Updating duplex audio profile for device: " + device + " to " + BluetoothProfile.getProfileName(duplexProfile)); metadata.preferred_duplex_profile = duplexProfile; } updateDatabase(metadata); } return BluetoothStatusCodes.SUCCESS; } /** * Sets the preferred profile for the supplied audio modes. See * {@link BluetoothDevice#getPreferredAudioProfiles()} for more details. * * @param device is the device for which we want to get the preferred audio profiles * @return a Bundle containing the preferred audio profiles */ public Bundle getPreferredAudioProfiles(BluetoothDevice device) { synchronized (mMetadataCache) { if (device == null) { Log.e(TAG, "getPreferredAudioProfiles: device is null"); throw new IllegalArgumentException("getPreferredAudioProfiles: device is null"); } String address = device.getAddress(); if (!mMetadataCache.containsKey(address)) { return Bundle.EMPTY; } // Gets the preferred audio profiles for each audio mode Metadata metadata = mMetadataCache.get(address); int outputOnlyProfile = metadata.preferred_output_only_profile; int duplexProfile = metadata.preferred_duplex_profile; // Checks if the default values are present (aka no explicit preference) if (outputOnlyProfile == 0 && duplexProfile == 0) { return Bundle.EMPTY; } Bundle modeToProfileBundle = new Bundle(); if (outputOnlyProfile != 0) { modeToProfileBundle.putInt(BluetoothDevice.AUDIO_MODE_OUTPUT_ONLY, outputOnlyProfile); } if (duplexProfile != 0) { modeToProfileBundle.putInt(BluetoothDevice.AUDIO_MODE_DUPLEX, duplexProfile); } return modeToProfileBundle; } } /** * Get the {@link Looper} for the handler thread. This is used in testing and helper * objects Loading android/app/src/com/android/bluetooth/btservice/storage/Metadata.java +18 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,22 @@ class Metadata { @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; Loading @@ -64,6 +80,8 @@ class Metadata { last_active_time = MetadataDatabase.sCurrentConnectionNumber++; is_active_a2dp_device = true; audioPolicyMetadata = new AudioPolicyEntity(); preferred_output_only_profile = 0; preferred_duplex_profile = 0; } String getAddress() { Loading android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java +22 −1 Original line number Diff line number Diff line Loading @@ -33,7 +33,7 @@ import java.util.List; /** * MetadataDatabase is a Room database stores Bluetooth persistence data */ @Database(entities = {Metadata.class}, version = 115) @Database(entities = {Metadata.class}, version = 116) public abstract class MetadataDatabase extends RoomDatabase { /** * The metadata database file name Loading Loading @@ -526,4 +526,25 @@ public abstract class MetadataDatabase extends RoomDatabase { } } }; @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; } } } }; } android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java +31 −4 Original line number Diff line number Diff line Loading @@ -1158,7 +1158,7 @@ public final class DatabaseManagerTest { @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(); Loading Loading @@ -1204,7 +1204,7 @@ public final class DatabaseManagerTest { @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(); Loading @@ -1226,7 +1226,7 @@ public final class DatabaseManagerTest { @Test public void testDatabaseMigration_114_115() throws IOException { // Create a database with version 112 // Create a database with version 114 SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 114); // insert a device to the database ContentValues device = new ContentValues(); Loading @@ -1234,11 +1234,13 @@ public final class DatabaseManagerTest { device.put("migrated", false); assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device), CoreMatchers.not(-1)); // Migrate database from 112 to 113 // 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); Loading @@ -1250,6 +1252,31 @@ public final class DatabaseManagerTest { } } @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); } } /** * Helper function to check whether the database has the expected column */ Loading Loading
android/app/src/com/android/bluetooth/btservice/AdapterService.java +67 −0 Original line number Diff line number Diff line Loading @@ -4062,6 +4062,73 @@ public class AdapterService extends Service { enforceBluetoothPrivilegedPermission(service); Utils.setForegroundUserId(userId); } @Override public void setPreferredAudioProfiles(BluetoothDevice device, Bundle modeToProfileBundle, AttributionSource source, SynchronousResultReceiver receiver) { try { receiver.send(setPreferredAudioProfiles(device, modeToProfileBundle, source)); } catch (RuntimeException e) { receiver.propagateException(e); } } private int setPreferredAudioProfiles(BluetoothDevice device, Bundle modeToProfileBundle, AttributionSource source) { AdapterService service = getService(); if (service == null) { return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED; } if (!callerIsSystemOrActiveOrManagedUser(service, TAG, "setPreferredAudioProfiles")) { return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED; } if (device == null) { throw new IllegalArgumentException("device cannot be null"); } if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { throw new IllegalArgumentException("device cannot have an invalid address"); } if (!Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION; } enforceBluetoothPrivilegedPermission(service); return service.mDatabaseManager.setPreferredAudioProfiles(device, modeToProfileBundle); } @Override public void getPreferredAudioProfiles(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver) { try { receiver.send(getPreferredAudioProfiles(device, source)); } catch (RuntimeException e) { receiver.propagateException(e); } } private Bundle getPreferredAudioProfiles(BluetoothDevice device, AttributionSource source) { AdapterService service = getService(); if (service == null) { return Bundle.EMPTY; } if (!callerIsSystemOrActiveOrManagedUser(service, TAG, "getPreferredAudioProfiles")) { throw new IllegalStateException("Caller is not the system or part of the " + "active/managed user"); } if (device == null) { throw new IllegalArgumentException("device cannot be null"); } if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { throw new IllegalArgumentException("device cannot have an invalid address"); } if (!Utils.checkConnectPermissionForDataDelivery(service, source, TAG)) { return Bundle.EMPTY; } enforceBluetoothPrivilegedPermission(service); return service.mDatabaseManager.getPreferredAudioProfiles(device); } } // ----API Methods-------- Loading
android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java +87 −0 Original line number Diff line number Diff line Loading @@ -24,12 +24,14 @@ import android.bluetooth.BluetoothAudioPolicy; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.BluetoothStatusCodes; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; Loading Loading @@ -771,6 +773,91 @@ public class DatabaseManager { } } /** * Sets the preferred profile for the supplied audio modes. See * {@link BluetoothDevice#setPreferredAudioProfiles(Bundle)} for more details. * * @param device is the remote device for which we are setting the preferred audio profiles * @param modeToProfileBundle contains the preferred profile * @return */ public int setPreferredAudioProfiles(BluetoothDevice device, Bundle modeToProfileBundle) { synchronized (mMetadataCache) { if (device == null) { Log.e(TAG, "setPreferredAudioProfiles: device is null"); throw new IllegalArgumentException("setPreferredAudioProfiles: device is null"); } String address = device.getAddress(); if (!mMetadataCache.containsKey(address)) { return BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED; } // Updates preferred audio profiles for the device Metadata metadata = mMetadataCache.get(address); int outputProfile = modeToProfileBundle.getInt(BluetoothDevice.AUDIO_MODE_OUTPUT_ONLY); int duplexProfile = modeToProfileBundle.getInt(BluetoothDevice.AUDIO_MODE_DUPLEX); if (outputProfile != 0) { Log.i(TAG, "setPreferredAudioProfiles: Updating output only audio profile for " + "device: " + device + " to " + BluetoothProfile.getProfileName(outputProfile)); metadata.preferred_output_only_profile = outputProfile; } if (duplexProfile != 0) { Log.i(TAG, "setPreferredAudioProfiles: Updating duplex audio profile for device: " + device + " to " + BluetoothProfile.getProfileName(duplexProfile)); metadata.preferred_duplex_profile = duplexProfile; } updateDatabase(metadata); } return BluetoothStatusCodes.SUCCESS; } /** * Sets the preferred profile for the supplied audio modes. See * {@link BluetoothDevice#getPreferredAudioProfiles()} for more details. * * @param device is the device for which we want to get the preferred audio profiles * @return a Bundle containing the preferred audio profiles */ public Bundle getPreferredAudioProfiles(BluetoothDevice device) { synchronized (mMetadataCache) { if (device == null) { Log.e(TAG, "getPreferredAudioProfiles: device is null"); throw new IllegalArgumentException("getPreferredAudioProfiles: device is null"); } String address = device.getAddress(); if (!mMetadataCache.containsKey(address)) { return Bundle.EMPTY; } // Gets the preferred audio profiles for each audio mode Metadata metadata = mMetadataCache.get(address); int outputOnlyProfile = metadata.preferred_output_only_profile; int duplexProfile = metadata.preferred_duplex_profile; // Checks if the default values are present (aka no explicit preference) if (outputOnlyProfile == 0 && duplexProfile == 0) { return Bundle.EMPTY; } Bundle modeToProfileBundle = new Bundle(); if (outputOnlyProfile != 0) { modeToProfileBundle.putInt(BluetoothDevice.AUDIO_MODE_OUTPUT_ONLY, outputOnlyProfile); } if (duplexProfile != 0) { modeToProfileBundle.putInt(BluetoothDevice.AUDIO_MODE_DUPLEX, duplexProfile); } return modeToProfileBundle; } } /** * Get the {@link Looper} for the handler thread. This is used in testing and helper * objects Loading
android/app/src/com/android/bluetooth/btservice/storage/Metadata.java +18 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,22 @@ class Metadata { @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; Loading @@ -64,6 +80,8 @@ class Metadata { last_active_time = MetadataDatabase.sCurrentConnectionNumber++; is_active_a2dp_device = true; audioPolicyMetadata = new AudioPolicyEntity(); preferred_output_only_profile = 0; preferred_duplex_profile = 0; } String getAddress() { Loading
android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java +22 −1 Original line number Diff line number Diff line Loading @@ -33,7 +33,7 @@ import java.util.List; /** * MetadataDatabase is a Room database stores Bluetooth persistence data */ @Database(entities = {Metadata.class}, version = 115) @Database(entities = {Metadata.class}, version = 116) public abstract class MetadataDatabase extends RoomDatabase { /** * The metadata database file name Loading Loading @@ -526,4 +526,25 @@ public abstract class MetadataDatabase extends RoomDatabase { } } }; @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; } } } }; }
android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java +31 −4 Original line number Diff line number Diff line Loading @@ -1158,7 +1158,7 @@ public final class DatabaseManagerTest { @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(); Loading Loading @@ -1204,7 +1204,7 @@ public final class DatabaseManagerTest { @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(); Loading @@ -1226,7 +1226,7 @@ public final class DatabaseManagerTest { @Test public void testDatabaseMigration_114_115() throws IOException { // Create a database with version 112 // Create a database with version 114 SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 114); // insert a device to the database ContentValues device = new ContentValues(); Loading @@ -1234,11 +1234,13 @@ public final class DatabaseManagerTest { device.put("migrated", false); assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device), CoreMatchers.not(-1)); // Migrate database from 112 to 113 // 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); Loading @@ -1250,6 +1252,31 @@ public final class DatabaseManagerTest { } } @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); } } /** * Helper function to check whether the database has the expected column */ Loading