diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml index 681c7689516..d5e08bbc7e4 100644 --- a/res/xml/bluetooth_audio_sharing.xml +++ b/res/xml/bluetooth_audio_sharing.xml @@ -19,18 +19,23 @@ xmlns:settings="http://schemas.android.com/apk/res-auto" android:title="@string/audio_sharing_title"> + + + android:key="calls_and_alarms" + android:summary="" + android:title="@string/calls_and_alarms_device_title" + settings:controller="com.android.settings.connecteddevice.audiosharing.CallsAndAlarmsPreferenceController" /> + android:title="Stream name" + settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController" /> + android:icon="@drawable/ic_chevron_right_24dp" + android:key="audio_streams_settings" + android:title="@string/audio_streams_pref_title" /> \ No newline at end of file diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdater.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdater.java index d3908c2f650..59393ad952c 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdater.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdater.java @@ -16,7 +16,6 @@ package com.android.settings.connecteddevice.audiosharing; -import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import android.util.Log; @@ -27,11 +26,8 @@ import com.android.settings.bluetooth.BluetoothDeviceUpdater; import com.android.settings.bluetooth.Utils; import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; -import java.util.List; - public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater implements Preference.OnPreferenceClickListener { @@ -55,7 +51,8 @@ public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) { // If device is LE audio device and has a broadcast source, // it would show in audio sharing devices group. - if (cachedDevice.isConnectedLeAudioDevice() && hasBroadcastSource(cachedDevice)) { + if (cachedDevice.isConnectedLeAudioDevice() + && AudioSharingUtils.hasBroadcastSource(cachedDevice, mLocalBluetoothManager)) { isFilterMatched = true; } } @@ -76,24 +73,6 @@ public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater return device.setActive(); } - private boolean hasBroadcastSource(CachedBluetoothDevice cachedDevice) { - LocalBluetoothLeBroadcastAssistant assistant = - mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); - if (assistant == null) { - return false; - } - List sourceList = - assistant.getAllSources(cachedDevice.getDevice()); - if (!sourceList.isEmpty()) return true; - // Return true if member device is in broadcast. - for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { - List list = - assistant.getAllSources(device.getDevice()); - if (!list.isEmpty()) return true; - } - return false; - } - @Override protected String getPreferenceKey() { return PREF_KEY; diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java index 40207bec448..cf5881b3dfe 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java @@ -31,6 +31,7 @@ public class AudioSharingDashboardFragment extends DashboardFragment SettingsMainSwitchBar mMainSwitchBar; private AudioSharingSwitchBarController mSwitchBarController; + private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController; private CallsAndAlarmsPreferenceController mCallsAndAlarmsPreferenceController; private AudioSharingNamePreferenceController mAudioSharingNamePreferenceController; @@ -66,6 +67,9 @@ public class AudioSharingDashboardFragment extends DashboardFragment @Override public void onAttach(Context context) { super.onAttach(context); + mAudioSharingDeviceVolumeGroupController = + use(AudioSharingDeviceVolumeGroupController.class); + mAudioSharingDeviceVolumeGroupController.init(this); mCallsAndAlarmsPreferenceController = use(CallsAndAlarmsPreferenceController.class); mCallsAndAlarmsPreferenceController.init(this); mAudioSharingNamePreferenceController = use(AudioSharingNamePreferenceController.class); @@ -91,6 +95,7 @@ public class AudioSharingDashboardFragment extends DashboardFragment } private void updateVisibilityForAttachedPreferences(boolean isVisible) { + mAudioSharingDeviceVolumeGroupController.updateVisibility(isVisible); mCallsAndAlarmsPreferenceController.updateVisibility(isVisible); mAudioSharingNamePreferenceController.updateVisibility(isVisible); } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdater.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdater.java new file mode 100644 index 00000000000..0668f50db74 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdater.java @@ -0,0 +1,139 @@ +/* + * 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; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.util.Log; +import android.widget.SeekBar; + +import androidx.preference.Preference; + +import com.android.settings.bluetooth.BluetoothDevicePreference; +import com.android.settings.bluetooth.BluetoothDeviceUpdater; +import com.android.settings.bluetooth.Utils; +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +public class AudioSharingDeviceVolumeControlUpdater extends BluetoothDeviceUpdater + implements Preference.OnPreferenceClickListener { + + private static final String TAG = "AudioSharingDeviceVolumeControlUpdater"; + + private static final String PREF_KEY = "audio_sharing_volume_control"; + + private final LocalBluetoothManager mLocalBtManager; + + public AudioSharingDeviceVolumeControlUpdater( + Context context, + DevicePreferenceCallback devicePreferenceCallback, + int metricsCategory) { + super(context, devicePreferenceCallback, metricsCategory); + mLocalBtManager = Utils.getLocalBluetoothManager(context); + } + + @Override + public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) { + boolean isFilterMatched = false; + if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) { + // If device is LE audio device and has a broadcast source, + // it would show in audio sharing devices group. + if (cachedDevice.isConnectedLeAudioDevice() + && AudioSharingUtils.hasBroadcastSource(cachedDevice, mLocalBtManager)) { + isFilterMatched = true; + } + } + Log.d( + TAG, + "isFilterMatched() device : " + + cachedDevice.getName() + + ", isFilterMatched : " + + isFilterMatched); + return isFilterMatched; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + return true; + } + + @Override + protected void addPreference(CachedBluetoothDevice cachedDevice) { + if (cachedDevice == null) return; + final BluetoothDevice device = cachedDevice.getDevice(); + if (!mPreferenceMap.containsKey(device)) { + SeekBar.OnSeekBarChangeListener listener = + new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged( + SeekBar seekBar, int progress, boolean fromUser) {} + + @Override + public void onStartTrackingTouch(SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (mLocalBtManager != null + && mLocalBtManager.getProfileManager().getVolumeControlProfile() + != null) { + mLocalBtManager + .getProfileManager() + .getVolumeControlProfile() + .setDeviceVolume( + cachedDevice.getDevice(), + seekBar.getProgress(), + /* isGroupOp= */ true); + } + } + }; + AudioSharingDeviceVolumePreference vPreference = + new AudioSharingDeviceVolumePreference(mPrefContext, cachedDevice, listener); + vPreference.setKey(getPreferenceKey()); + vPreference.setIcon(com.android.settingslib.R.drawable.ic_bt_untethered_earbuds); + vPreference.setTitle(cachedDevice.getName()); + mPreferenceMap.put(device, vPreference); + mDevicePreferenceCallback.onDeviceAdded(vPreference); + } + } + + @Override + protected String getPreferenceKey() { + return PREF_KEY; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected void update(CachedBluetoothDevice cachedBluetoothDevice) { + super.update(cachedBluetoothDevice); + Log.d(TAG, "Map : " + mPreferenceMap); + } + + @Override + protected void addPreference( + CachedBluetoothDevice cachedDevice, @BluetoothDevicePreference.SortType int type) {} + + @Override + protected void launchDeviceDetails(Preference preference) {} + + @Override + public void refreshPreference() {} +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupController.java new file mode 100644 index 00000000000..90054d4b3f9 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupController.java @@ -0,0 +1,278 @@ +/* + * 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; + +import android.annotation.IntRange; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothVolumeControl; +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.settings.bluetooth.BluetoothDeviceUpdater; +import com.android.settings.bluetooth.Utils; +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.VolumeControlProfile; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePreferenceController + implements DefaultLifecycleObserver, DevicePreferenceCallback { + + private static final String TAG = "AudioSharingDeviceVolumeGroupController"; + private static final String KEY = "audio_sharing_device_volume_group"; + + private final LocalBluetoothManager mLocalBtManager; + private final LocalBluetoothLeBroadcastAssistant mAssistant; + private final Executor mExecutor; + private VolumeControlProfile mVolumeControl; + private BluetoothDeviceUpdater mBluetoothDeviceUpdater; + private FragmentManager mFragmentManager; + private PreferenceGroup mPreferenceGroup; + private Map mCallbackMap = + new HashMap(); + + private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new BluetoothLeBroadcastAssistant.Callback() { + @Override + public void onSearchStarted(int reason) {} + + @Override + public void onSearchStartFailed(int reason) {} + + @Override + public void onSearchStopped(int reason) {} + + @Override + public void onSearchStopFailed(int reason) {} + + @Override + public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} + + @Override + public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) { + Log.d( + TAG, + "onSourceAdded(), sink = " + + sink + + ", sourceId = " + + sourceId + + ", reason = " + + reason); + mBluetoothDeviceUpdater.forceUpdate(); + } + + @Override + public void onSourceAddFailed( + @NonNull BluetoothDevice sink, + @NonNull BluetoothLeBroadcastMetadata source, + int reason) { + Log.d( + TAG, + "onSourceAddFailed(), sink = " + + sink + + ", source = " + + source + + ", reason = " + + reason); + } + + @Override + public void onSourceModified( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceModifyFailed( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceRemoved( + @NonNull BluetoothDevice sink, int sourceId, int reason) { + Log.d( + TAG, + "onSourceRemoved(), sink = " + + sink + + ", sourceId = " + + sourceId + + ", reason = " + + reason); + mBluetoothDeviceUpdater.forceUpdate(); + } + + @Override + public void onSourceRemoveFailed( + @NonNull BluetoothDevice sink, int sourceId, int reason) { + Log.d( + TAG, + "onSourceRemoveFailed(), sink = " + + sink + + ", sourceId = " + + sourceId + + ", reason = " + + reason); + } + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, + int sourceId, + BluetoothLeBroadcastReceiveState state) {} + }; + + public AudioSharingDeviceVolumeGroupController(Context context) { + super(context, KEY); + mLocalBtManager = Utils.getLocalBtManager(mContext); + mAssistant = mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + mExecutor = Executors.newSingleThreadExecutor(); + if (mLocalBtManager != null) { + mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile(); + } + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + if (mAssistant == null) { + Log.d(TAG, "onStart() Broadcast or assistant is not supported on this device"); + return; + } + if (mBluetoothDeviceUpdater == null) { + Log.d(TAG, "onStart() Bluetooth device updater is not initialized"); + return; + } + mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); + mBluetoothDeviceUpdater.registerCallback(); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + if (mAssistant == null) { + Log.d(TAG, "onStop() Broadcast or assistant is not supported on this device"); + return; + } + if (mBluetoothDeviceUpdater == null) { + Log.d(TAG, "onStop() Bluetooth device updater is not initialized"); + return; + } + mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); + mBluetoothDeviceUpdater.unregisterCallback(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + + mPreferenceGroup = screen.findPreference(KEY); + mPreferenceGroup.setVisible(false); + + if (isAvailable() && mBluetoothDeviceUpdater != null) { + mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); + mBluetoothDeviceUpdater.forceUpdate(); + } + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void onDeviceAdded(Preference preference) { + if (mPreferenceGroup.getPreferenceCount() == 0) { + mPreferenceGroup.setVisible(true); + } + mPreferenceGroup.addPreference(preference); + if (mVolumeControl != null && preference instanceof AudioSharingDeviceVolumePreference) { + BluetoothVolumeControl.Callback callback = + buildVcCallback((AudioSharingDeviceVolumePreference) preference); + mCallbackMap.put(preference, callback); + mVolumeControl.registerCallback(mExecutor, callback); + } + } + + @Override + public void onDeviceRemoved(Preference preference) { + mPreferenceGroup.removePreference(preference); + if (mPreferenceGroup.getPreferenceCount() == 0) { + mPreferenceGroup.setVisible(false); + } + if (mVolumeControl != null && mCallbackMap.containsKey(preference)) { + mVolumeControl.unregisterCallback(mCallbackMap.get(preference)); + mCallbackMap.remove(preference); + } + } + + @Override + public void updateVisibility(boolean isVisible) { + super.updateVisibility(isVisible); + if (mPreferenceGroup != null) { + mPreferenceGroup.setVisible(mPreferenceGroup.getPreferenceCount() > 0); + } + } + + /** + * Initialize the controller. + * + * @param fragment The fragment to provide the context and metrics category for {@link + * AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs. + */ + public void init(DashboardFragment fragment) { + mBluetoothDeviceUpdater = + new AudioSharingDeviceVolumeControlUpdater( + fragment.getContext(), + AudioSharingDeviceVolumeGroupController.this, + fragment.getMetricsCategory()); + } + + private BluetoothVolumeControl.Callback buildVcCallback( + AudioSharingDeviceVolumePreference preference) { + return new BluetoothVolumeControl.Callback() { + @Override + public void onVolumeOffsetChanged(BluetoothDevice device, int volumeOffset) {} + + @Override + public void onDeviceVolumeChanged( + @android.annotation.NonNull BluetoothDevice device, + @IntRange(from = 0, to = 255) int volume) { + Log.d(TAG, "onDeviceVolumeChanged changed " + device.getAnonymizedAddress()); + CachedBluetoothDevice cachedDevice = + mLocalBtManager.getCachedDeviceManager().findDevice(device); + if (cachedDevice == null) return; + if (preference.getCachedDevice() != null + && preference.getCachedDevice().getGroupId() == cachedDevice.getGroupId()) { + preference.setProgress(volume); + } + } + }; + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreference.java new file mode 100644 index 00000000000..3808b600af8 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreference.java @@ -0,0 +1,73 @@ +/* + * 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; + +import android.content.Context; +import android.widget.SeekBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settings.bluetooth.Utils; +import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +public class AudioSharingDeviceVolumePreference extends SeekBarPreference { + private static final String TAG = "AudioSharingDeviceVolumePreference"; + public static final int MIN_VOLUME = 0; + public static final int MAX_VOLUME = 255; + + protected SeekBar mSeekBar; + private final LocalBluetoothManager mLocalBtManager; + private final CachedBluetoothDevice mCachedDevice; + private final SeekBar.OnSeekBarChangeListener mListener; + + public AudioSharingDeviceVolumePreference( + Context context, + @NonNull CachedBluetoothDevice device, + SeekBar.OnSeekBarChangeListener listener) { + super(context); + setLayoutResource(R.layout.preference_volume_slider); + mLocalBtManager = Utils.getLocalBtManager(context); + mCachedDevice = device; + mListener = listener; + } + + @Nullable + public CachedBluetoothDevice getCachedDevice() { + return mCachedDevice; + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); + mSeekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); + mSeekBar.setMax(MAX_VOLUME); + mSeekBar.setMin(MIN_VOLUME); + mSeekBar.setOnSeekBarChangeListener(mListener); + } + + /** Set the progress bar to target progress */ + public void setProgress(int progress) { + if (mSeekBar != null) { + mSeekBar.setProgress(progress); + } + } +}