blob: 79fb566023288e04003b46d6c025fc0c6b4f8702 [file] [log] [blame]
/*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastSubgroup;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProfile.ServiceListener;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
/**
* LocalBluetoothLeBroadcast provides an interface between the Settings app
* and the functionality of the local {@link BluetoothLeBroadcast}.
* Use the {@link BluetoothLeBroadcast.Callback} to get the result callback.
*/
public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
private static final String TAG = "LocalBluetoothLeBroadcast";
private static final boolean DEBUG = BluetoothUtils.D;
static final String NAME = "LE_AUDIO_BROADCAST";
private static final String UNDERLINE = "_";
private static final int DEFAULT_CODE_MAX = 9999;
private static final int DEFAULT_CODE_MIN = 1000;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
private static final int UNKNOWN_VALUE_PLACEHOLDER = -1;
private static final Uri[] SETTINGS_URIS = new Uri[]{
Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO),
Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE),
Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME),
};
private BluetoothLeBroadcast mService;
private BluetoothLeAudioContentMetadata mBluetoothLeAudioContentMetadata;
private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
private BluetoothLeAudioContentMetadata.Builder mBuilder;
private int mBroadcastId = UNKNOWN_VALUE_PLACEHOLDER;
private String mAppSourceName = "";
private String mNewAppSourceName = "";
private boolean mIsProfileReady;
private String mProgramInfo;
private byte[] mBroadcastCode;
private Executor mExecutor;
private ContentResolver mContentResolver;
private ContentObserver mSettingsObserver;
private final ServiceListener mServiceListener = new ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service connected");
}
if(!mIsProfileReady) {
mService = (BluetoothLeBroadcast) proxy;
mIsProfileReady = true;
registerServiceCallBack(mExecutor, mBroadcastCallback);
List<BluetoothLeBroadcastMetadata> metadata = getAllBroadcastMetadata();
if (!metadata.isEmpty()) {
updateBroadcastInfoFromBroadcastMetadata(metadata.get(0));
}
registerContentObserver();
}
}
@Override
public void onServiceDisconnected(int profile) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service disconnected");
}
if(mIsProfileReady) {
mIsProfileReady = false;
unregisterServiceCallBack(mBroadcastCallback);
unregisterContentObserver();
}
}
};
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@Override
public void onBroadcastStarted(int reason, int broadcastId) {
if (DEBUG) {
Log.d(TAG,
"onBroadcastStarted(), reason = " + reason + ", broadcastId = "
+ broadcastId);
}
setLatestBroadcastId(broadcastId);
setAppSourceName(mNewAppSourceName, /*updateContentResolver=*/ true);
}
@Override
public void onBroadcastStartFailed(int reason) {
if (DEBUG) {
Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
}
}
@Override
public void onBroadcastMetadataChanged(int broadcastId,
@NonNull BluetoothLeBroadcastMetadata metadata) {
if (DEBUG) {
Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId);
}
setLatestBluetoothLeBroadcastMetadata(metadata);
}
@Override
public void onBroadcastStopped(int reason, int broadcastId) {
if (DEBUG) {
Log.d(TAG,
"onBroadcastStopped(), reason = " + reason + ", broadcastId = "
+ broadcastId);
}
resetCacheInfo();
}
@Override
public void onBroadcastStopFailed(int reason) {
if (DEBUG) {
Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
}
}
@Override
public void onBroadcastUpdated(int reason, int broadcastId) {
if (DEBUG) {
Log.d(TAG,
"onBroadcastUpdated(), reason = " + reason + ", broadcastId = "
+ broadcastId);
}
setLatestBroadcastId(broadcastId);
setAppSourceName(mNewAppSourceName, /*updateContentResolver=*/ true);
}
@Override
public void onBroadcastUpdateFailed(int reason, int broadcastId) {
if (DEBUG) {
Log.d(TAG,
"onBroadcastUpdateFailed(), reason = " + reason + ", broadcastId = "
+ broadcastId);
}
}
@Override
public void onPlaybackStarted(int reason, int broadcastId) {
}
@Override
public void onPlaybackStopped(int reason, int broadcastId) {
}
};
private class BroadcastSettingsObserver extends ContentObserver {
BroadcastSettingsObserver(Handler h) {
super(h);
}
@Override
public void onChange(boolean selfChange) {
Log.d(TAG, "BroadcastSettingsObserver: onChange");
updateBroadcastInfoFromContentProvider();
}
}
LocalBluetoothLeBroadcast(Context context) {
mExecutor = Executors.newSingleThreadExecutor();
mBuilder = new BluetoothLeAudioContentMetadata.Builder();
mContentResolver = context.getContentResolver();
Handler handler = new Handler(Looper.getMainLooper());
mSettingsObserver = new BroadcastSettingsObserver(handler);
updateBroadcastInfoFromContentProvider();
// Before registering callback, the constructor should finish creating the all of variables.
BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST);
}
/**
* Start the LE Broadcast. If the system started the LE Broadcast, then the system calls the
* corresponding callback {@link BluetoothLeBroadcast.Callback}.
*/
public void startBroadcast(String appSourceName, String language) {
mNewAppSourceName = appSourceName;
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcast is null when starting the broadcast.");
return;
}
String programInfo = getProgramInfo();
if (DEBUG) {
Log.d(TAG,
"startBroadcast: language = " + language + " ,programInfo = " + programInfo);
}
buildContentMetadata(language, programInfo);
mService.startBroadcast(mBluetoothLeAudioContentMetadata, mBroadcastCode);
}
public String getProgramInfo() {
return mProgramInfo;
}
public void setProgramInfo(String programInfo) {
setProgramInfo(programInfo, /*updateContentResolver=*/ true);
}
private void setProgramInfo(String programInfo, boolean updateContentResolver) {
if (TextUtils.isEmpty(programInfo)) {
Log.d(TAG, "setProgramInfo: programInfo is null or empty");
return;
}
if (mProgramInfo != null && TextUtils.equals(mProgramInfo, programInfo)) {
Log.d(TAG, "setProgramInfo: programInfo is not changed");
return;
}
Log.d(TAG, "setProgramInfo: " + programInfo);
mProgramInfo = programInfo;
if (updateContentResolver) {
if (mContentResolver == null) {
Log.d(TAG, "mContentResolver is null");
return;
}
Settings.Secure.putString(mContentResolver,
Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, programInfo);
}
}
public byte[] getBroadcastCode() {
return mBroadcastCode;
}
public void setBroadcastCode(byte[] broadcastCode) {
setBroadcastCode(broadcastCode, /*updateContentResolver=*/ true);
}
private void setBroadcastCode(byte[] broadcastCode, boolean updateContentResolver) {
if (broadcastCode == null) {
Log.d(TAG, "setBroadcastCode: broadcastCode is null");
return;
}
if (mBroadcastCode != null && Arrays.equals(broadcastCode, mBroadcastCode)) {
Log.d(TAG, "setBroadcastCode: broadcastCode is not changed");
return;
}
mBroadcastCode = broadcastCode;
if (updateContentResolver) {
if (mContentResolver == null) {
Log.d(TAG, "mContentResolver is null");
return;
}
Settings.Secure.putString(mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE,
new String(broadcastCode, StandardCharsets.UTF_8));
}
}
private void setLatestBroadcastId(int broadcastId) {
Log.d(TAG, "setLatestBroadcastId: mBroadcastId is " + broadcastId);
mBroadcastId = broadcastId;
}
public int getLatestBroadcastId() {
return mBroadcastId;
}
private void setAppSourceName(String appSourceName, boolean updateContentResolver) {
if (TextUtils.isEmpty(appSourceName)) {
appSourceName = "";
}
if (mAppSourceName != null && TextUtils.equals(mAppSourceName, appSourceName)) {
Log.d(TAG, "setAppSourceName: appSourceName is not changed");
return;
}
mAppSourceName = appSourceName;
mNewAppSourceName = "";
if (updateContentResolver) {
if (mContentResolver == null) {
Log.d(TAG, "mContentResolver is null");
return;
}
Settings.Secure.putString(mContentResolver,
Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, mAppSourceName);
}
}
public String getAppSourceName() {
return mAppSourceName;
}
private void setLatestBluetoothLeBroadcastMetadata(
BluetoothLeBroadcastMetadata bluetoothLeBroadcastMetadata) {
if (bluetoothLeBroadcastMetadata != null
&& bluetoothLeBroadcastMetadata.getBroadcastId() == mBroadcastId) {
mBluetoothLeBroadcastMetadata = bluetoothLeBroadcastMetadata;
updateBroadcastInfoFromBroadcastMetadata(bluetoothLeBroadcastMetadata);
}
}
public BluetoothLeBroadcastMetadata getLatestBluetoothLeBroadcastMetadata() {
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcast is null");
return null;
}
if (mBluetoothLeBroadcastMetadata == null) {
final List<BluetoothLeBroadcastMetadata> metadataList =
mService.getAllBroadcastMetadata();
mBluetoothLeBroadcastMetadata = metadataList.stream()
.filter(i -> i.getBroadcastId() == mBroadcastId)
.findFirst()
.orElse(null);
}
return mBluetoothLeBroadcastMetadata;
}
private void updateBroadcastInfoFromContentProvider() {
if (mContentResolver == null) {
Log.d(TAG, "updateBroadcastInfoFromContentProvider: mContentResolver is null");
return;
}
String programInfo = Settings.Secure.getString(mContentResolver,
Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO);
if (programInfo == null) {
programInfo = getDefaultValueOfProgramInfo();
}
setProgramInfo(programInfo, /*updateContentResolver=*/ false);
String prefBroadcastCode = Settings.Secure.getString(mContentResolver,
Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE);
byte[] broadcastCode = (prefBroadcastCode == null) ? getDefaultValueOfBroadcastCode()
: prefBroadcastCode.getBytes(StandardCharsets.UTF_8);
setBroadcastCode(broadcastCode, /*updateContentResolver=*/ false);
String appSourceName = Settings.Secure.getString(mContentResolver,
Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME);
setAppSourceName(appSourceName, /*updateContentResolver=*/ false);
}
private void updateBroadcastInfoFromBroadcastMetadata(
BluetoothLeBroadcastMetadata bluetoothLeBroadcastMetadata) {
if (bluetoothLeBroadcastMetadata == null) {
Log.d(TAG, "The bluetoothLeBroadcastMetadata is null");
return;
}
setBroadcastCode(bluetoothLeBroadcastMetadata.getBroadcastCode());
setLatestBroadcastId(bluetoothLeBroadcastMetadata.getBroadcastId());
List<BluetoothLeBroadcastSubgroup> subgroup = bluetoothLeBroadcastMetadata.getSubgroups();
if (subgroup == null || subgroup.size() < 1) {
Log.d(TAG, "The subgroup is not valid value");
return;
}
BluetoothLeAudioContentMetadata contentMetadata = subgroup.get(0).getContentMetadata();
setProgramInfo(contentMetadata.getProgramInfo());
setAppSourceName(getAppSourceName(), /*updateContentResolver=*/ true);
}
/**
* Stop the latest LE Broadcast. If the system stopped the LE Broadcast, then the system
* calls the corresponding callback {@link BluetoothLeBroadcast.Callback}.
*/
public void stopLatestBroadcast() {
stopBroadcast(mBroadcastId);
}
/**
* Stop the LE Broadcast. If the system stopped the LE Broadcast, then the system calls the
* corresponding callback {@link BluetoothLeBroadcast.Callback}.
*/
public void stopBroadcast(int broadcastId) {
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcast is null when stopping the broadcast.");
return;
}
if (DEBUG) {
Log.d(TAG, "stopBroadcast()");
}
mService.stopBroadcast(broadcastId);
}
/**
* Update the LE Broadcast. If the system stopped the LE Broadcast, then the system calls the
* corresponding callback {@link BluetoothLeBroadcast.Callback}.
*/
public void updateBroadcast(String appSourceName, String language) {
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcast is null when updating the broadcast.");
return;
}
String programInfo = getProgramInfo();
if (DEBUG) {
Log.d(TAG,
"updateBroadcast: language = " + language + " ,programInfo = " + programInfo);
}
mNewAppSourceName = appSourceName;
mBluetoothLeAudioContentMetadata = mBuilder.setProgramInfo(programInfo).build();
mService.updateBroadcast(mBroadcastId, mBluetoothLeAudioContentMetadata);
}
public void registerServiceCallBack(@NonNull @CallbackExecutor Executor executor,
@NonNull BluetoothLeBroadcast.Callback callback) {
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcast is null.");
return;
}
mService.registerCallback(executor, callback);
}
public void unregisterServiceCallBack(@NonNull BluetoothLeBroadcast.Callback callback) {
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcast is null.");
return;
}
mService.unregisterCallback(callback);
}
private void buildContentMetadata(String language, String programInfo) {
mBluetoothLeAudioContentMetadata = mBuilder.setLanguage(language).setProgramInfo(
programInfo).build();
}
public LocalBluetoothLeBroadcastMetadata getLocalBluetoothLeBroadcastMetaData() {
final BluetoothLeBroadcastMetadata metadata = getLatestBluetoothLeBroadcastMetadata();
if (metadata == null) {
Log.d(TAG, "The BluetoothLeBroadcastMetadata is null.");
return null;
}
return new LocalBluetoothLeBroadcastMetadata(metadata);
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.LE_AUDIO_BROADCAST;
}
public boolean accessProfileEnabled() {
return false;
}
public boolean isAutoConnectable() {
return true;
}
/**
* Not supported since LE Audio Broadcasts do not establish a connection.
*/
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
// LE Audio Broadcasts are not connection-oriented.
return mService.getConnectionState(device);
}
/**
* Not supported since LE Audio Broadcasts do not establish a connection.
*/
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
// LE Audio Broadcasts are not connection-oriented.
return mService.getConnectedDevices();
}
public @NonNull
List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() {
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcast is null.");
return Collections.emptyList();
}
return mService.getAllBroadcastMetadata();
}
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return !mService.getAllBroadcastMetadata().isEmpty();
}
/**
* Service does not provide method to get/set policy.
*/
public int getConnectionPolicy(BluetoothDevice device) {
return CONNECTION_POLICY_FORBIDDEN;
}
/**
* Service does not provide "setEnabled" method. Please use {@link #startBroadcast},
* {@link #stopBroadcast()} or {@link #updateBroadcast(String, String)}
*/
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
return false;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.summary_empty;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
return BluetoothUtils.getConnectionStateSummary(state);
}
public int getDrawableResource(BluetoothClass btClass) {
return 0;
}
@RequiresApi(Build.VERSION_CODES.S)
protected void finalize() {
if (DEBUG) {
Log.d(TAG, "finalize()");
}
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
BluetoothProfile.LE_AUDIO_BROADCAST,
mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up LeAudio proxy", t);
}
}
}
private String getDefaultValueOfProgramInfo() {
//set the default value;
int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX);
return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix;
}
private byte[] getDefaultValueOfBroadcastCode() {
//set the default value;
return generateRandomPassword().getBytes(StandardCharsets.UTF_8);
}
private void resetCacheInfo() {
if (DEBUG) {
Log.d(TAG, "resetCacheInfo:");
}
setAppSourceName("", /*updateContentResolver=*/ true);
mBluetoothLeBroadcastMetadata = null;
mBroadcastId = UNKNOWN_VALUE_PLACEHOLDER;
}
private String generateRandomPassword() {
String randomUUID = UUID.randomUUID().toString();
//first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
return randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
}
private void registerContentObserver() {
if (mContentResolver == null) {
Log.d(TAG, "mContentResolver is null");
return;
}
for (Uri uri : SETTINGS_URIS) {
mContentResolver.registerContentObserver(uri, false, mSettingsObserver);
}
}
private void unregisterContentObserver() {
if (mContentResolver == null) {
Log.d(TAG, "mContentResolver is null");
return;
}
mContentResolver.unregisterContentObserver(mSettingsObserver);
}
}