diff options
author | chelseahao <chelseahao@google.com> | 2023-12-13 17:56:41 +0800 |
---|---|---|
committer | chelseahao <chelseahao@google.com> | 2023-12-13 19:10:12 +0800 |
commit | 7edc93ec2547699d482f533bcc179398a72dc322 (patch) | |
tree | 618d77786a1c04fc542aca877d5d9817d332ae9f | |
parent | 1bb099f872d5aa1bf5858953261bae9a230cfbff (diff) | |
download | Settings-7edc93ec2547699d482f533bcc179398a72dc322.tar.gz |
[Audiosharing] Set visibility by active device.
Also created header and button controller for detail page.
Bug: 305620450
Test: manual
Change-Id: I5e468a0fb9ce49ef0fd9a0b00b51084cfd416ce0
7 files changed, 289 insertions, 55 deletions
diff --git a/res/xml/audio_stream_details_fragment.xml b/res/xml/audio_stream_details_fragment.xml index 97274428013..2a84939732a 100644 --- a/res/xml/audio_stream_details_fragment.xml +++ b/res/xml/audio_stream_details_fragment.xml @@ -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> diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java index b5361f22057..3d4ef82a844 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java @@ -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(); diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java new file mode 100644 index 00000000000..bb729d67ec3 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java @@ -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; + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java index 1e6982959fc..e1dc228bfe1 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java @@ -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 diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java new file mode 100644 index 00000000000..89f24bccdbd --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java @@ -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; + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java index 5acbc1f7b93..198e8e5f335 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java @@ -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) { diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java index 45f0c2fda3e..3c005b294b5 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java @@ -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( |