Merge "[Audiosharing] Set visibility by active device." into main
This commit is contained in:
@@ -25,10 +25,12 @@
|
||||
android:layout="@layout/settings_entity_header"
|
||||
android:selectable="false"
|
||||
settings:allowDividerBelow="true"
|
||||
settings:searchable="false" />
|
||||
settings:searchable="false"
|
||||
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController" />
|
||||
|
||||
<com.android.settingslib.widget.ActionButtonsPreference
|
||||
android:key="audio_stream_button"
|
||||
settings:allowDividerBelow="true" />
|
||||
settings:allowDividerBelow="true"
|
||||
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamButtonController" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
@@ -41,6 +41,8 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class AudioSharingUtils {
|
||||
private static final String TAG = "AudioSharingUtils";
|
||||
private static final boolean DEBUG = BluetoothUtils.D;
|
||||
@@ -237,7 +239,7 @@ public class AudioSharingUtils {
|
||||
* @return An Optional containing the active LE Audio device, or an empty Optional if not found.
|
||||
*/
|
||||
public static Optional<CachedBluetoothDevice> getActiveSinkOnAssistant(
|
||||
LocalBluetoothManager manager) {
|
||||
@Nullable LocalBluetoothManager manager) {
|
||||
if (manager == null) {
|
||||
Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
|
||||
return Optional.empty();
|
||||
|
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settingslib.widget.ActionButtonsPreference;
|
||||
|
||||
public class AudioStreamButtonController extends BasePreferenceController
|
||||
implements DefaultLifecycleObserver {
|
||||
private static final String KEY = "audio_stream_button";
|
||||
private @Nullable ActionButtonsPreference mPreference;
|
||||
private int mBroadcastId = -1;
|
||||
|
||||
public AudioStreamButtonController(Context context, String preferenceKey) {
|
||||
super(context, preferenceKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void displayPreference(PreferenceScreen screen) {
|
||||
mPreference = screen.findPreference(getPreferenceKey());
|
||||
if (mPreference != null) {
|
||||
mPreference.setButton1Enabled(true);
|
||||
// TODO(chelseahao): update this based on stream connection state
|
||||
mPreference
|
||||
.setButton1Text(R.string.bluetooth_device_context_disconnect)
|
||||
.setButton1Icon(R.drawable.ic_settings_close);
|
||||
}
|
||||
super.displayPreference(screen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailabilityStatus() {
|
||||
return AVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreferenceKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
/** Initialize with broadcast id */
|
||||
void init(int broadcastId) {
|
||||
mBroadcastId = broadcastId;
|
||||
}
|
||||
}
|
@@ -17,16 +17,28 @@
|
||||
package com.android.settings.connecteddevice.audiosharing.audiostreams;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
|
||||
public class AudioStreamDetailsFragment extends DashboardFragment {
|
||||
static final String BROADCAST_NAME_ARG = "broadcast_name";
|
||||
static final String BROADCAST_ID_ARG = "broadcast_id";
|
||||
private static final String TAG = "AudioStreamDetailsFragment";
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
Bundle arguments = getArguments();
|
||||
if (arguments != null) {
|
||||
use(AudioStreamHeaderController.class)
|
||||
.init(
|
||||
this,
|
||||
arguments.getString(BROADCAST_NAME_ARG),
|
||||
arguments.getInt(BROADCAST_ID_ARG));
|
||||
use(AudioStreamButtonController.class).init(arguments.getInt(BROADCAST_ID_ARG));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.widget.EntityHeaderController;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class AudioStreamHeaderController extends BasePreferenceController
|
||||
implements DefaultLifecycleObserver {
|
||||
private static final String KEY = "audio_stream_header";
|
||||
private @Nullable EntityHeaderController mHeaderController;
|
||||
private @Nullable DashboardFragment mFragment;
|
||||
private String mBroadcastName = "";
|
||||
private int mBroadcastId = -1;
|
||||
|
||||
public AudioStreamHeaderController(Context context, String preferenceKey) {
|
||||
super(context, preferenceKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void displayPreference(PreferenceScreen screen) {
|
||||
LayoutPreference headerPreference = screen.findPreference(KEY);
|
||||
if (headerPreference != null && mFragment != null) {
|
||||
mHeaderController =
|
||||
EntityHeaderController.newInstance(
|
||||
mFragment.getActivity(),
|
||||
mFragment,
|
||||
headerPreference.findViewById(R.id.entity_header));
|
||||
if (mBroadcastName != null) {
|
||||
mHeaderController.setLabel(mBroadcastName);
|
||||
}
|
||||
mHeaderController.setIcon(
|
||||
screen.getContext().getDrawable(R.drawable.ic_bt_audio_sharing));
|
||||
// TODO(chelseahao): update this based on stream connection state
|
||||
mHeaderController.setSummary("Listening now");
|
||||
mHeaderController.done(true);
|
||||
screen.addPreference(headerPreference);
|
||||
}
|
||||
super.displayPreference(screen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailabilityStatus() {
|
||||
return AVAILABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreferenceKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
/** Initialize with {@link AudioStreamDetailsFragment} and broadcast name and id */
|
||||
void init(
|
||||
AudioStreamDetailsFragment audioStreamDetailsFragment,
|
||||
String broadcastName,
|
||||
int broadcastId) {
|
||||
mFragment = audioStreamDetailsFragment;
|
||||
mBroadcastName = broadcastName;
|
||||
mBroadcastId = broadcastId;
|
||||
}
|
||||
}
|
@@ -80,8 +80,8 @@ class AudioStreamsHelper {
|
||||
});
|
||||
}
|
||||
|
||||
/** Removes all sources from LE broadcasts associated for all active sinks. */
|
||||
void removeSource() {
|
||||
/** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
|
||||
void removeSource(int broadcastId) {
|
||||
if (mLeBroadcastAssistant == null) {
|
||||
Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
|
||||
return;
|
||||
@@ -93,14 +93,17 @@ class AudioStreamsHelper {
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"removeSource(): remove all sources from sink : "
|
||||
"removeSource(): remove all sources with broadcast id :"
|
||||
+ broadcastId
|
||||
+ " from sink : "
|
||||
+ sink.getAddress());
|
||||
}
|
||||
var sources = mLeBroadcastAssistant.getAllSources(sink);
|
||||
if (!sources.isEmpty()) {
|
||||
mLeBroadcastAssistant.removeSource(
|
||||
sink, sources.get(0).getSourceId());
|
||||
}
|
||||
mLeBroadcastAssistant.getAllSources(sink).stream()
|
||||
.filter(state -> state.getBroadcastId() == broadcastId)
|
||||
.forEach(
|
||||
state ->
|
||||
mLeBroadcastAssistant.removeSource(
|
||||
sink, state.getSourceId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -121,6 +124,12 @@ class AudioStreamsHelper {
|
||||
return mLeBroadcastAssistant;
|
||||
}
|
||||
|
||||
static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
|
||||
return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
|
||||
&& state.getBigEncryptionState()
|
||||
== BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
|
||||
}
|
||||
|
||||
private static List<BluetoothDevice> getActiveSinksOnAssistant(
|
||||
@Nullable LocalBluetoothManager manager) {
|
||||
if (manager == null) {
|
||||
|
@@ -22,9 +22,9 @@ import android.app.AlertDialog;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -42,8 +42,11 @@ import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settingslib.bluetooth.BluetoothCallback;
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -57,11 +60,22 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
|
||||
implements DefaultLifecycleObserver {
|
||||
private static final String TAG = "AudioStreamsProgressCategoryController";
|
||||
private static final boolean DEBUG = BluetoothUtils.D;
|
||||
private final BluetoothCallback mBluetoothCallback =
|
||||
new BluetoothCallback() {
|
||||
@Override
|
||||
public void onActiveDeviceChanged(
|
||||
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
|
||||
if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
|
||||
mExecutor.execute(() -> init(activeDevice != null));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final Executor mExecutor;
|
||||
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
|
||||
private final AudioStreamsHelper mAudioStreamsHelper;
|
||||
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
|
||||
private final @Nullable LocalBluetoothManager mBluetoothManager;
|
||||
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
|
||||
new ConcurrentHashMap<>();
|
||||
private AudioStreamsProgressCategoryPreference mCategoryPreference;
|
||||
@@ -69,7 +83,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
|
||||
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
|
||||
super(context, preferenceKey);
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(mContext));
|
||||
mBluetoothManager = Utils.getLocalBtManager(mContext);
|
||||
mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
|
||||
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
|
||||
mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
|
||||
}
|
||||
@@ -87,48 +102,24 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
if (mLeBroadcastAssistant == null) {
|
||||
Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
|
||||
return;
|
||||
}
|
||||
mBroadcastIdToPreferenceMap.clear();
|
||||
if (mCategoryPreference != null) {
|
||||
mCategoryPreference.removeAll();
|
||||
if (mBluetoothManager != null) {
|
||||
mBluetoothManager.getEventManager().registerCallback(mBluetoothCallback);
|
||||
}
|
||||
mExecutor.execute(
|
||||
() -> {
|
||||
mLeBroadcastAssistant.registerServiceCallBack(
|
||||
mExecutor, mBroadcastAssistantCallback);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "scanAudioStreamsStart()");
|
||||
}
|
||||
mLeBroadcastAssistant.startSearchingForSources(emptyList());
|
||||
// Display currently connected streams
|
||||
var unused =
|
||||
ThreadUtils.postOnBackgroundThread(
|
||||
() ->
|
||||
mAudioStreamsHelper
|
||||
.getAllSources()
|
||||
.forEach(this::handleSourceConnected));
|
||||
boolean hasActive =
|
||||
AudioSharingUtils.getActiveSinkOnAssistant(mBluetoothManager)
|
||||
.isPresent();
|
||||
init(hasActive);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
if (mLeBroadcastAssistant == null) {
|
||||
Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
|
||||
return;
|
||||
if (mBluetoothManager != null) {
|
||||
mBluetoothManager.getEventManager().unregisterCallback(mBluetoothCallback);
|
||||
}
|
||||
mExecutor.execute(
|
||||
() -> {
|
||||
if (mLeBroadcastAssistant.isSearchInProgress()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "scanAudioStreamsStop()");
|
||||
}
|
||||
mLeBroadcastAssistant.stopSearchingForSources();
|
||||
}
|
||||
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
|
||||
});
|
||||
mExecutor.execute(this::stopScanning);
|
||||
}
|
||||
|
||||
void setScanning(boolean isScanning) {
|
||||
@@ -142,7 +133,10 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
|
||||
Preference.OnPreferenceClickListener addSourceOrShowDialog =
|
||||
preference -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "preferenceClicked(): attempt to join broadcast");
|
||||
Log.d(
|
||||
TAG,
|
||||
"preferenceClicked(): attempt to join broadcast id : "
|
||||
+ source.getBroadcastId());
|
||||
}
|
||||
if (source.isEncrypted()) {
|
||||
ThreadUtils.postOnMainThread(
|
||||
@@ -177,11 +171,13 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
|
||||
}
|
||||
});
|
||||
}
|
||||
mAudioStreamsHelper.removeSource();
|
||||
mAudioStreamsHelper.removeSource(broadcastId);
|
||||
}
|
||||
|
||||
void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
|
||||
// TODO(chelseahao): only continue when the state indicates a successful connection
|
||||
if (!AudioStreamsHelper.isConnected(state)) {
|
||||
return;
|
||||
}
|
||||
mBroadcastIdToPreferenceMap.compute(
|
||||
state.getBroadcastId(),
|
||||
(k, v) -> {
|
||||
@@ -194,7 +190,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
|
||||
ThreadUtils.postOnMainThread(
|
||||
() -> {
|
||||
preference.setIsConnected(
|
||||
true, p -> launchDetailFragment((AudioStreamPreference) p));
|
||||
true, p -> launchDetailFragment(state.getBroadcastId()));
|
||||
if (mCategoryPreference != null && !existed) {
|
||||
mCategoryPreference.addPreference(preference);
|
||||
}
|
||||
@@ -208,11 +204,73 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
|
||||
AudioSharingUtils.toastMessage(mContext, msg);
|
||||
}
|
||||
|
||||
private boolean launchDetailFragment(AudioStreamPreference preference) {
|
||||
private void init(boolean hasActive) {
|
||||
mBroadcastIdToPreferenceMap.clear();
|
||||
ThreadUtils.postOnMainThread(
|
||||
() -> {
|
||||
if (mCategoryPreference != null) {
|
||||
mCategoryPreference.removeAll();
|
||||
mCategoryPreference.setVisible(hasActive);
|
||||
}
|
||||
});
|
||||
if (hasActive) {
|
||||
startScanning();
|
||||
} else {
|
||||
stopScanning();
|
||||
}
|
||||
}
|
||||
|
||||
private void startScanning() {
|
||||
if (mLeBroadcastAssistant == null) {
|
||||
Log.w(TAG, "startScanning(): LeBroadcastAssistant is null!");
|
||||
return;
|
||||
}
|
||||
if (mLeBroadcastAssistant.isSearchInProgress()) {
|
||||
showToast("Failed to start scanning, please try again.");
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "startScanning()");
|
||||
}
|
||||
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
|
||||
mLeBroadcastAssistant.startSearchingForSources(emptyList());
|
||||
|
||||
// Display currently connected streams
|
||||
var unused =
|
||||
ThreadUtils.postOnBackgroundThread(
|
||||
() ->
|
||||
mAudioStreamsHelper
|
||||
.getAllSources()
|
||||
.forEach(this::handleSourceConnected));
|
||||
}
|
||||
|
||||
private void stopScanning() {
|
||||
if (mLeBroadcastAssistant == null) {
|
||||
Log.w(TAG, "stopScanning(): LeBroadcastAssistant is null!");
|
||||
return;
|
||||
}
|
||||
if (mLeBroadcastAssistant.isSearchInProgress()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "stopScanning()");
|
||||
}
|
||||
mLeBroadcastAssistant.stopSearchingForSources();
|
||||
}
|
||||
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
|
||||
}
|
||||
|
||||
private boolean launchDetailFragment(int broadcastId) {
|
||||
if (!mBroadcastIdToPreferenceMap.containsKey(broadcastId)) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"launchDetailFragment(): broadcastId not exist in BroadcastIdToPreferenceMap!");
|
||||
return false;
|
||||
}
|
||||
AudioStreamPreference preference = mBroadcastIdToPreferenceMap.get(broadcastId);
|
||||
|
||||
Bundle broadcast = new Bundle();
|
||||
broadcast.putString(
|
||||
Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO,
|
||||
(String) preference.getTitle());
|
||||
AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) preference.getTitle());
|
||||
broadcast.putInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG, broadcastId);
|
||||
|
||||
new SubSettingLauncher(mContext)
|
||||
.setTitleText("Audio stream details")
|
||||
@@ -240,8 +298,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
|
||||
(dialog, which) -> {
|
||||
var code =
|
||||
((EditText)
|
||||
layout.requireViewById(
|
||||
R.id.broadcast_edit_text))
|
||||
layout.requireViewById(
|
||||
R.id.broadcast_edit_text))
|
||||
.getText()
|
||||
.toString();
|
||||
mAudioStreamsHelper.addSource(
|
||||
|
Reference in New Issue
Block a user