From 8a3ebe25e0da2363786de61b0b0b2ed8da938f42 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Fri, 19 Jan 2024 15:43:17 +0800 Subject: [PATCH 1/3] [Audiosharing] Add button action in detail page. Bug: 308368124 Test: manual Change-Id: I44e631cb75af432965d2221e71676146ea1537b6 --- .../AudioStreamButtonController.java | 145 +++++++++++++++- .../AudioStreamHeaderController.java | 92 +++++++++- ...udioStreamsBroadcastAssistantCallback.java | 41 ----- .../AudioStreamsProgressCategoryCallback.java | 119 +++++++++++++ ...udioStreamsProgressCategoryController.java | 26 ++- .../audiostreams/AudioStreamsRepository.java | 160 ++++++++++++++++++ .../AudioStreamsScanQrCodeController.java | 5 - 7 files changed, 528 insertions(+), 60 deletions(-) create mode 100644 src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java create mode 100644 src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java index bb729d67ec3..47597cf370f 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java @@ -16,39 +16,170 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; +import android.util.Log; +import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.ActionButtonsPreference; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + public class AudioStreamButtonController extends BasePreferenceController implements DefaultLifecycleObserver { + private static final String TAG = "AudioStreamButtonController"; private static final String KEY = "audio_stream_button"; + private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new AudioStreamsBroadcastAssistantCallback() { + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoved(sink, sourceId, reason); + updateButton(); + } + + @Override + public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoveFailed(sink, sourceId, reason); + updateButton(); + } + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, + int sourceId, + BluetoothLeBroadcastReceiveState state) { + super.onReceiveStateChanged(sink, sourceId, state); + if (mAudioStreamsHelper.isConnected(state)) { + updateButton(); + } + } + + @Override + public void onSourceAddFailed( + BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) { + super.onSourceAddFailed(sink, source, reason); + updateButton(); + } + + @Override + public void onSourceLost(int broadcastId) { + super.onSourceLost(broadcastId); + updateButton(); + } + }; + + private final AudioStreamsRepository mAudioStreamsRepository = + AudioStreamsRepository.getInstance(); + private final Executor mExecutor; + private final AudioStreamsHelper mAudioStreamsHelper; + private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private @Nullable ActionButtonsPreference mPreference; private int mBroadcastId = -1; public AudioStreamButtonController(Context context, String preferenceKey) { super(context, preferenceKey); + mExecutor = Executors.newSingleThreadExecutor(); + mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context)); + mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "onStart(): LeBroadcastAssistant is null!"); + return; + } + mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "onStop(): LeBroadcastAssistant is null!"); + return; + } + mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); } @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); - } + updateButton(); super.displayPreference(screen); } + private void updateButton() { + if (mPreference != null) { + if (mAudioStreamsHelper.getAllConnectedSources().stream() + .map(BluetoothLeBroadcastReceiveState::getBroadcastId) + .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId)) { + ThreadUtils.postOnMainThread( + () -> { + if (mPreference != null) { + mPreference.setButton1Enabled(true); + mPreference + .setButton1Text( + R.string.bluetooth_device_context_disconnect) + .setButton1Icon(R.drawable.ic_settings_close) + .setButton1OnClickListener( + unused -> { + if (mPreference != null) { + mPreference.setButton1Enabled(false); + } + mAudioStreamsHelper.removeSource(mBroadcastId); + }); + } + }); + } else { + View.OnClickListener clickToRejoin = + unused -> + ThreadUtils.postOnBackgroundThread( + () -> { + var metadata = + mAudioStreamsRepository.getSavedMetadata( + mContext, mBroadcastId); + if (metadata != null) { + mAudioStreamsHelper.addSource(metadata); + ThreadUtils.postOnMainThread( + () -> { + if (mPreference != null) { + mPreference.setButton1Enabled( + false); + } + }); + } + }); + ThreadUtils.postOnMainThread( + () -> { + if (mPreference != null) { + mPreference.setButton1Enabled(true); + mPreference + .setButton1Text(R.string.bluetooth_device_context_connect) + .setButton1Icon(R.drawable.ic_add_24dp) + .setButton1OnClickListener(clickToRejoin); + } + }); + } + } else { + Log.w(TAG, "updateButton(): preference is null!"); + } + } + @Override public int getAvailabilityStatus() { return AVAILABLE; diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java index 89f24bccdbd..3524543bfc5 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java @@ -16,22 +16,64 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.EntityHeaderController; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.LayoutPreference; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + import javax.annotation.Nullable; public class AudioStreamHeaderController extends BasePreferenceController implements DefaultLifecycleObserver { + private static final String TAG = "AudioStreamHeaderController"; private static final String KEY = "audio_stream_header"; + private final Executor mExecutor; + private final AudioStreamsHelper mAudioStreamsHelper; + @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; + private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new AudioStreamsBroadcastAssistantCallback() { + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoved(sink, sourceId, reason); + updateSummary(); + } + + @Override + public void onSourceLost(int broadcastId) { + super.onSourceLost(broadcastId); + updateSummary(); + } + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, + int sourceId, + BluetoothLeBroadcastReceiveState state) { + super.onReceiveStateChanged(sink, sourceId, state); + if (mAudioStreamsHelper.isConnected(state)) { + updateSummary(); + } + } + }; + private @Nullable EntityHeaderController mHeaderController; private @Nullable DashboardFragment mFragment; private String mBroadcastName = ""; @@ -39,6 +81,27 @@ public class AudioStreamHeaderController extends BasePreferenceController public AudioStreamHeaderController(Context context, String preferenceKey) { super(context, preferenceKey); + mExecutor = Executors.newSingleThreadExecutor(); + mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context)); + mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "onStart(): LeBroadcastAssistant is null!"); + return; + } + mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "onStop(): LeBroadcastAssistant is null!"); + return; + } + mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); } @Override @@ -55,14 +118,37 @@ public class AudioStreamHeaderController extends BasePreferenceController } 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); + updateSummary(); } super.displayPreference(screen); } + private void updateSummary() { + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + var latestSummary = + mAudioStreamsHelper.getAllConnectedSources().stream() + .map( + BluetoothLeBroadcastReceiveState + ::getBroadcastId) + .anyMatch( + connectedBroadcastId -> + connectedBroadcastId + == mBroadcastId) + ? "Listening now" + : ""; + ThreadUtils.postOnMainThread( + () -> { + if (mHeaderController != null) { + mHeaderController.setSummary(latestSummary); + mHeaderController.done(true); + } + }); + }); + } + @Override public int getAvailabilityStatus() { return AVAILABLE; diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java index 84e753c5526..9fb5b21fed9 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java @@ -24,21 +24,12 @@ import android.util.Log; import com.android.settingslib.bluetooth.BluetoothUtils; -import java.util.Locale; - public class AudioStreamsBroadcastAssistantCallback implements BluetoothLeBroadcastAssistant.Callback { private static final String TAG = "AudioStreamsBroadcastAssistantCallback"; private static final boolean DEBUG = BluetoothUtils.D; - private final AudioStreamsProgressCategoryController mCategoryController; - - public AudioStreamsBroadcastAssistantCallback( - AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) { - mCategoryController = audioStreamsProgressCategoryController; - } - @Override public void onReceiveStateChanged( BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { @@ -52,45 +43,30 @@ public class AudioStreamsBroadcastAssistantCallback + " state: " + state); } - mCategoryController.handleSourceConnected(state); } @Override public void onSearchStartFailed(int reason) { Log.w(TAG, "onSearchStartFailed() reason : " + reason); - mCategoryController.showToast( - String.format(Locale.US, "Failed to start scanning, reason %d", reason)); } @Override public void onSearchStarted(int reason) { - if (mCategoryController == null) { - Log.w(TAG, "onSearchStarted() : mCategoryController is null!"); - return; - } if (DEBUG) { Log.d(TAG, "onSearchStarted() reason : " + reason); } - mCategoryController.setScanning(true); } @Override public void onSearchStopFailed(int reason) { Log.w(TAG, "onSearchStopFailed() reason : " + reason); - mCategoryController.showToast( - String.format(Locale.US, "Failed to stop scanning, reason %d", reason)); } @Override public void onSearchStopped(int reason) { - if (mCategoryController == null) { - Log.w(TAG, "onSearchStopped() : mCategoryController is null!"); - return; - } if (DEBUG) { Log.d(TAG, "onSearchStopped() reason : " + reason); } - mCategoryController.setScanning(false); } @Override @@ -106,8 +82,6 @@ public class AudioStreamsBroadcastAssistantCallback + " reason: " + reason); } - mCategoryController.showToast( - String.format(Locale.US, "Failed to join broadcast, reason %d", reason)); } @Override @@ -126,14 +100,9 @@ public class AudioStreamsBroadcastAssistantCallback @Override public void onSourceFound(BluetoothLeBroadcastMetadata source) { - if (mCategoryController == null) { - Log.w(TAG, "onSourceFound() : mCategoryController is null!"); - return; - } if (DEBUG) { Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId()); } - mCategoryController.handleSourceFound(source); } @Override @@ -141,7 +110,6 @@ public class AudioStreamsBroadcastAssistantCallback if (DEBUG) { Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId); } - mCategoryController.handleSourceLost(broadcastId); } @Override @@ -153,12 +121,6 @@ public class AudioStreamsBroadcastAssistantCallback @Override public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason); - mCategoryController.showToast( - String.format( - Locale.US, - "Failed to remove source %d for sink %s", - sourceId, - sink.getAddress())); } @Override @@ -166,8 +128,5 @@ public class AudioStreamsBroadcastAssistantCallback if (DEBUG) { Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason); } - mCategoryController.showToast( - String.format( - Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress())); } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java new file mode 100644 index 00000000000..15a06030264 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 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.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.util.Log; + +import java.util.Locale; + +public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback { + private static final String TAG = "AudioStreamsProgressCategoryCallback"; + + private final AudioStreamsProgressCategoryController mCategoryController; + + public AudioStreamsProgressCategoryCallback( + AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) { + mCategoryController = audioStreamsProgressCategoryController; + } + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { + super.onReceiveStateChanged(sink, sourceId, state); + mCategoryController.handleSourceConnected(state); + } + + @Override + public void onSearchStartFailed(int reason) { + super.onSearchStartFailed(reason); + mCategoryController.showToast( + String.format(Locale.US, "Failed to start scanning, reason %d", reason)); + } + + @Override + public void onSearchStarted(int reason) { + super.onSearchStarted(reason); + if (mCategoryController == null) { + Log.w(TAG, "onSearchStarted() : mCategoryController is null!"); + return; + } + mCategoryController.setScanning(true); + } + + @Override + public void onSearchStopFailed(int reason) { + super.onSearchStopFailed(reason); + mCategoryController.showToast( + String.format(Locale.US, "Failed to stop scanning, reason %d", reason)); + } + + @Override + public void onSearchStopped(int reason) { + super.onSearchStopped(reason); + if (mCategoryController == null) { + Log.w(TAG, "onSearchStopped() : mCategoryController is null!"); + return; + } + mCategoryController.setScanning(false); + } + + @Override + public void onSourceAddFailed( + BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) { + super.onSourceAddFailed(sink, source, reason); + mCategoryController.showToast( + String.format(Locale.US, "Failed to join broadcast, reason %d", reason)); + } + + @Override + public void onSourceFound(BluetoothLeBroadcastMetadata source) { + super.onSourceFound(source); + if (mCategoryController == null) { + Log.w(TAG, "onSourceFound() : mCategoryController is null!"); + return; + } + mCategoryController.handleSourceFound(source); + } + + @Override + public void onSourceLost(int broadcastId) { + super.onSourceLost(broadcastId); + mCategoryController.handleSourceLost(broadcastId); + } + + @Override + public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoveFailed(sink, sourceId, reason); + mCategoryController.showToast( + String.format( + Locale.US, + "Failed to remove source %d for sink %s", + sourceId, + sink.getAddress())); + } + + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoved(sink, sourceId, reason); + mCategoryController.showToast( + String.format( + Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress())); + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java index cb9975da9aa..b3b074335aa 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java @@ -74,6 +74,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro } }; + private final AudioStreamsRepository mAudioStreamsRepository = + AudioStreamsRepository.getInstance(); + enum AudioStreamState { // When mTimedSourceFromQrCode is present and this source has not been synced. WAIT_FOR_SYNC, @@ -86,7 +89,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro } private final Executor mExecutor; - private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback; + private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback; private final AudioStreamsHelper mAudioStreamsHelper; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private final @Nullable LocalBluetoothManager mBluetoothManager; @@ -102,7 +105,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro mBluetoothManager = Utils.getLocalBtManager(mContext); mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager); mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); - mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this); + mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this); } @Override @@ -170,6 +173,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro source, (AudioStreamPreference) preference)); } else { mAudioStreamsHelper.addSource(source); + mAudioStreamsRepository.cacheMetadata(source); ((AudioStreamPreference) preference) .setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD); updatePreferenceConnectionState( @@ -202,6 +206,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro return v; } mAudioStreamsHelper.addSource(pendingSource); + mAudioStreamsRepository.cacheMetadata(pendingSource); mTimedSourceFromQrCode.consumed(); v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD); updatePreferenceConnectionState( @@ -236,6 +241,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro var fromState = v.getAudioStreamState(); if (fromState == AudioStreamState.SYNCED) { mAudioStreamsHelper.addSource(metadataFromQrCode); + mAudioStreamsRepository.cacheMetadata(metadataFromQrCode); mTimedSourceFromQrCode.consumed(); v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD); updatePreferenceConnectionState( @@ -302,6 +308,16 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected)); return v; }); + // Saved connected metadata for user to re-join this broadcast later. + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + var cached = + mAudioStreamsRepository.getCachedMetadata(broadcastIdConnected); + if (cached != null) { + mAudioStreamsRepository.saveMetadata(mContext, cached); + } + }); } private static String getPreferenceSummary(AudioStreamState state) { @@ -457,11 +473,13 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro R.id.broadcast_edit_text)) .getText() .toString(); - mAudioStreamsHelper.addSource( + var metadata = new BluetoothLeBroadcastMetadata.Builder(source) .setBroadcastCode( code.getBytes(StandardCharsets.UTF_8)) - .build()); + .build(); + mAudioStreamsHelper.addSource(metadata); + mAudioStreamsRepository.cacheMetadata(metadata); preference.setAudioStreamState( AudioStreamState.WAIT_FOR_SOURCE_ADD); updatePreferenceConnectionState( diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java new file mode 100644 index 00000000000..65245acf177 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 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.bluetooth.BluetoothLeBroadcastMetadata; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +/** Manages the caching and storage of Bluetooth audio stream metadata. */ +public class AudioStreamsRepository { + + private static final String TAG = "AudioStreamsRepository"; + private static final boolean DEBUG = BluetoothUtils.D; + + private static final String PREF_KEY = "bluetooth_audio_stream_pref"; + private static final String METADATA_KEY = "bluetooth_audio_stream_metadata"; + + @Nullable + private static AudioStreamsRepository sInstance = null; + + private AudioStreamsRepository() {} + + /** + * Gets the single instance of AudioStreamsRepository. + * + * @return The AudioStreamsRepository instance. + */ + public static synchronized AudioStreamsRepository getInstance() { + if (sInstance == null) { + sInstance = new AudioStreamsRepository(); + } + return sInstance; + } + + private final ConcurrentHashMap + mBroadcastIdToMetadataCacheMap = new ConcurrentHashMap<>(); + + /** + * Caches BluetoothLeBroadcastMetadata in a local cache. + * + * @param metadata The BluetoothLeBroadcastMetadata to be cached. + */ + void cacheMetadata(BluetoothLeBroadcastMetadata metadata) { + if (DEBUG) { + Log.d( + TAG, + "cacheMetadata(): broadcastId " + + metadata.getBroadcastId() + + " saved in local cache."); + } + mBroadcastIdToMetadataCacheMap.put(metadata.getBroadcastId(), metadata); + } + + /** + * Gets cached BluetoothLeBroadcastMetadata by broadcastId. + * + * @param broadcastId The broadcastId to look up in the cache. + * @return The cached BluetoothLeBroadcastMetadata or null if not found. + */ + @Nullable + BluetoothLeBroadcastMetadata getCachedMetadata(int broadcastId) { + var metadata = mBroadcastIdToMetadataCacheMap.get(broadcastId); + if (metadata == null) { + Log.w( + TAG, + "getCachedMetadata(): broadcastId not found in" + + " mBroadcastIdToMetadataCacheMap."); + return null; + } + return metadata; + } + + /** + * Saves metadata to SharedPreferences asynchronously. + * + * @param context The context. + * @param metadata The BluetoothLeBroadcastMetadata to be saved. + */ + void saveMetadata(Context context, BluetoothLeBroadcastMetadata metadata) { + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + SharedPreferences sharedPref = + context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE); + if (sharedPref != null) { + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString( + METADATA_KEY, + BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString( + metadata)); + editor.apply(); + if (DEBUG) { + Log.d( + TAG, + "saveMetadata(): broadcastId " + + metadata.getBroadcastId() + + " metadata saved in storage."); + } + } + }); + } + + /** + * Gets saved metadata from SharedPreferences. + * + * @param context The context. + * @param broadcastId The broadcastId to retrieve metadata for. + * @return The saved BluetoothLeBroadcastMetadata or null if not found. + */ + @Nullable + BluetoothLeBroadcastMetadata getSavedMetadata(Context context, int broadcastId) { + SharedPreferences sharedPref = context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE); + if (sharedPref != null) { + String savedMetadataStr = sharedPref.getString(METADATA_KEY, null); + if (savedMetadataStr == null) { + Log.w(TAG, "getSavedMetadata(): savedMetadataStr is null"); + return null; + } + var savedMetadata = + BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata( + savedMetadataStr); + if (savedMetadata == null || savedMetadata.getBroadcastId() != broadcastId) { + Log.w(TAG, "getSavedMetadata(): savedMetadata doesn't match broadcast Id."); + return null; + } + if (DEBUG) { + Log.d( + TAG, + "getSavedMetadata(): broadcastId " + + savedMetadata.getBroadcastId() + + " metadata found in storage."); + } + return savedMetadata; + } + return null; + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java index 549e7258347..e006cecf67b 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java @@ -16,7 +16,6 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; -import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.Intent; @@ -124,10 +123,6 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController }); } - void addSource(BluetoothLeBroadcastMetadata source) { - mAudioStreamsHelper.addSource(source); - } - private void updateVisibility() { ThreadUtils.postOnBackgroundThread( () -> { From 8b5da73e4d66d92a04c8728dd4dd9b6a2b3346e1 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 23 Jan 2024 19:38:59 +0800 Subject: [PATCH 2/3] [Audiosharing] Some UI tweaks (e.g, sort by RSSI) Bug: 308368124 Test: manual Change-Id: Ie066077f6ef47a57b9fb1c85bc7200498dcae093 --- res/layout/qrcode_scanner_fragment.xml | 25 ++-------- res/xml/bluetooth_audio_streams.xml | 28 +++++++---- res/xml/bluetooth_audio_streams_dialog.xml | 45 ++++++++++------- res/xml/bluetooth_audio_streams_qr_code.xml | 9 +++- .../AudioStreamConfirmDialog.java | 2 +- .../audiostreams/AudioStreamPreference.java | 32 ++++++++++-- ...udioStreamsProgressCategoryController.java | 16 ++++-- ...udioStreamsProgressCategoryPreference.java | 37 ++++++++++++++ .../AudioStreamsQrCodeFragment.java | 50 +++++++++++++------ .../qrcode/QrCodeScanModeFragment.java | 1 + 10 files changed, 175 insertions(+), 70 deletions(-) diff --git a/res/layout/qrcode_scanner_fragment.xml b/res/layout/qrcode_scanner_fragment.xml index e6d1c32bfe4..d402dc36ee2 100644 --- a/res/layout/qrcode_scanner_fragment.xml +++ b/res/layout/qrcode_scanner_fragment.xml @@ -17,7 +17,6 @@ @@ -26,36 +25,22 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="3" - android:layout_marginBottom="35dp"> + android:layout_marginBottom="55dp"> - - - - + android:layout_marginTop="20dp"/> diff --git a/res/xml/bluetooth_audio_streams.xml b/res/xml/bluetooth_audio_streams.xml index 95ee710e2dd..e7e708e484a 100644 --- a/res/xml/bluetooth_audio_streams.xml +++ b/res/xml/bluetooth_audio_streams.xml @@ -18,23 +18,31 @@ + android:title="Find an audio stream"> - + + android:title="Audio streams nearby" + settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController"> + + + + \ No newline at end of file diff --git a/res/xml/bluetooth_audio_streams_dialog.xml b/res/xml/bluetooth_audio_streams_dialog.xml index 502e55a1305..024e53763b9 100644 --- a/res/xml/bluetooth_audio_streams_dialog.xml +++ b/res/xml/bluetooth_audio_streams_dialog.xml @@ -16,6 +16,7 @@ --> @@ -23,70 +24,78 @@ android:id="@+id/dialog_bg" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingStart="25dp" + android:paddingEnd="25dp" android:orientation="vertical"> - + android:layout_marginBottom="@dimen/broadcast_dialog_margin">