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">
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ style="@style/BroadcastActionButton"/>
-
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ style="@style/BroadcastActionButton"/>
+
\ No newline at end of file
diff --git a/res/xml/bluetooth_audio_streams_qr_code.xml b/res/xml/bluetooth_audio_streams_qr_code.xml
index c750963dbf0..50b14295d5e 100644
--- a/res/xml/bluetooth_audio_streams_qr_code.xml
+++ b/res/xml/bluetooth_audio_streams_qr_code.xml
@@ -36,7 +36,7 @@
android:gravity="start"
android:textSize="15sp"
android:textColor="?android:attr/textColorPrimary"
- android:text="Scan this QR code with another device connected to LE audio headphones to start sharing audio"/>
+ android:text="To listen to this audio stream, other people can connect compatible headphones to their Android device. They can then scan this QR code."/>
+
+
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/AudioStreamConfirmDialog.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
index 5981c9e1876..131c8f6baaf 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
@@ -104,7 +104,7 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
private Dialog getErrorDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mActivity)
.setTitle("Can't listen to audio stream")
- .setSubTitle1("Can't play this audio stream. Learn more")
+ .setSubTitle2("Can't play this audio stream. Learn more")
.setRightButtonText("Close")
.setRightButtonOnClickListener(
unused -> {
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/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index 678f9524a37..c2e11786110 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -21,8 +21,10 @@ import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.util.AttributeSet;
+import android.view.View;
import androidx.annotation.Nullable;
+import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;
@@ -56,7 +58,6 @@ class AudioStreamPreference extends TwoTargetPreference {
}
mIsConnected = isConnected;
setSummary(summary);
- setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged();
}
@@ -70,6 +71,23 @@ class AudioStreamPreference extends TwoTargetPreference {
mAudioStream.setState(state);
}
+ void setAudioStreamMetadata(BluetoothLeBroadcastMetadata metadata) {
+ mAudioStream.setMetadata(metadata);
+ }
+
+ int getAudioStreamBroadcastId() {
+ return mAudioStream.getBroadcastId();
+ }
+
+ int getAudioStreamRssi() {
+ return mAudioStream.getRssi();
+ }
+
+ @Nullable
+ BluetoothLeBroadcastMetadata getAudioStreamMetadata() {
+ return mAudioStream.getMetadata();
+ }
+
AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
return mAudioStream.getState();
}
@@ -84,25 +102,31 @@ class AudioStreamPreference extends TwoTargetPreference {
return R.layout.preference_widget_lock;
}
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ View divider =
+ holder.findViewById(
+ com.android.settingslib.widget.preference.twotarget.R.id
+ .two_target_divider);
+ if (divider != null) {
+ divider.setVisibility(View.GONE);
+ }
+ }
+
static AudioStreamPreference fromMetadata(
- Context context,
- BluetoothLeBroadcastMetadata source,
- AudioStreamsProgressCategoryController.AudioStreamState streamState) {
+ Context context, BluetoothLeBroadcastMetadata source) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source));
- preference.setAudioStream(new AudioStream(source.getBroadcastId(), streamState));
+ preference.setAudioStream(new AudioStream(source));
return preference;
}
static AudioStreamPreference fromReceiveState(
- Context context,
- BluetoothLeBroadcastReceiveState receiveState,
- AudioStreamsProgressCategoryController.AudioStreamState streamState) {
+ Context context, BluetoothLeBroadcastReceiveState receiveState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(receiveState));
- preference.setAudioStream(
- new AudioStream(
- receiveState.getSourceId(), receiveState.getBroadcastId(), streamState));
+ preference.setAudioStream(new AudioStream(receiveState));
return preference;
}
@@ -127,41 +151,45 @@ class AudioStreamPreference extends TwoTargetPreference {
}
private static final class AudioStream {
- private int mSourceId;
- private int mBroadcastId;
- private AudioStreamsProgressCategoryController.AudioStreamState mState;
+ private static final int UNAVAILABLE = -1;
+ @Nullable private BluetoothLeBroadcastMetadata mMetadata;
+ @Nullable private BluetoothLeBroadcastReceiveState mReceiveState;
+ private AudioStreamsProgressCategoryController.AudioStreamState mState =
+ AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
- private AudioStream(
- int broadcastId, AudioStreamsProgressCategoryController.AudioStreamState state) {
- mBroadcastId = broadcastId;
- mState = state;
+ private AudioStream(BluetoothLeBroadcastMetadata metadata) {
+ mMetadata = metadata;
}
- private AudioStream(
- int sourceId,
- int broadcastId,
- AudioStreamsProgressCategoryController.AudioStreamState state) {
- mSourceId = sourceId;
- mBroadcastId = broadcastId;
- mState = state;
+ private AudioStream(BluetoothLeBroadcastReceiveState receiveState) {
+ mReceiveState = receiveState;
}
- // TODO(chelseahao): use this to handleSourceRemoved
- private int getSourceId() {
- return mSourceId;
- }
-
- // TODO(chelseahao): use this to handleSourceRemoved
private int getBroadcastId() {
- return mBroadcastId;
+ return mMetadata != null
+ ? mMetadata.getBroadcastId()
+ : mReceiveState != null ? mReceiveState.getBroadcastId() : UNAVAILABLE;
+ }
+
+ private int getRssi() {
+ return mMetadata != null ? mMetadata.getRssi() : Integer.MAX_VALUE;
}
private AudioStreamsProgressCategoryController.AudioStreamState getState() {
return mState;
}
+ @Nullable
+ private BluetoothLeBroadcastMetadata getMetadata() {
+ return mMetadata;
+ }
+
private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
mState = state;
}
+
+ private void setMetadata(BluetoothLeBroadcastMetadata metadata) {
+ mMetadata = metadata;
+ }
}
}
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..34ffc911269
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
@@ -0,0 +1,117 @@
+/*
+ * 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.handleSourceRemoved();
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index cb9975da9aa..c6f342a421f 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
+
import static java.util.Collections.emptyList;
import android.app.AlertDialog;
@@ -43,8 +45,10 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -53,6 +57,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.nio.charset.StandardCharsets;
+import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -74,25 +79,77 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
};
+ private final Preference.OnPreferenceClickListener mAddSourceOrShowDialog =
+ preference -> {
+ var p = (AudioStreamPreference) preference;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "preferenceClicked(): attempt to join broadcast id : "
+ + p.getAudioStreamBroadcastId());
+ }
+ var source = p.getAudioStreamMetadata();
+ if (source != null) {
+ if (source.isEncrypted()) {
+ ThreadUtils.postOnMainThread(() -> launchPasswordDialog(source, p));
+ } else {
+ moveToState(p, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
+ }
+ }
+ return true;
+ };
+
+ private final Preference.OnPreferenceClickListener mLaunchDetailFragment =
+ preference -> {
+ var p = (AudioStreamPreference) preference;
+ Bundle broadcast = new Bundle();
+ broadcast.putString(
+ AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle());
+ broadcast.putInt(
+ AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
+
+ new SubSettingLauncher(mContext)
+ .setTitleText("Audio stream details")
+ .setDestination(AudioStreamDetailsFragment.class.getName())
+ // TODO(chelseahao): Add logging enum
+ .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
+ .setArguments(broadcast)
+ .launch();
+ return true;
+ };
+
+ private final AudioStreamsRepository mAudioStreamsRepository =
+ AudioStreamsRepository.getInstance();
+
enum AudioStreamState {
+ UNKNOWN,
// When mTimedSourceFromQrCode is present and this source has not been synced.
WAIT_FOR_SYNC,
// When source has been synced but not added to any sink.
SYNCED,
// When addSource is called for this source and waiting for response.
- WAIT_FOR_SOURCE_ADD,
+ ADD_SOURCE_WAIT_FOR_RESPONSE,
// Source is added to active sink.
SOURCE_ADDED,
}
+ private final Comparator mComparator =
+ Comparator.comparing(
+ p ->
+ p.getAudioStreamState()
+ == AudioStreamsProgressCategoryController
+ .AudioStreamState.SOURCE_ADDED)
+ .thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
+ .reversed();
+
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;
private final ConcurrentHashMap mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
- private TimedSourceFromQrCode mTimedSourceFromQrCode;
+ private @Nullable TimedSourceFromQrCode mTimedSourceFromQrCode;
private AudioStreamsProgressCategoryPreference mCategoryPreference;
private AudioStreamsDashboardFragment mFragment;
@@ -102,7 +159,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
@@ -155,41 +212,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
void handleSourceFound(BluetoothLeBroadcastMetadata source) {
- Preference.OnPreferenceClickListener addSourceOrShowDialog =
- preference -> {
- if (DEBUG) {
- Log.d(
- TAG,
- "preferenceClicked(): attempt to join broadcast id : "
- + source.getBroadcastId());
- }
- if (source.isEncrypted()) {
- ThreadUtils.postOnMainThread(
- () ->
- launchPasswordDialog(
- source, (AudioStreamPreference) preference));
- } else {
- mAudioStreamsHelper.addSource(source);
- ((AudioStreamPreference) preference)
- .setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- (AudioStreamPreference) preference,
- AudioStreamState.WAIT_FOR_SOURCE_ADD,
- null);
- }
- return true;
- };
-
var broadcastIdFound = source.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
broadcastIdFound,
(k, v) -> {
if (v == null) {
- return addNewPreference(
- source, AudioStreamState.SYNCED, addSourceOrShowDialog);
+ // No existing preference for this source founded, add one and set initial
+ // state to SYNCED.
+ return addNewPreference(source, AudioStreamState.SYNCED);
}
var fromState = v.getAudioStreamState();
- if (fromState == AudioStreamState.WAIT_FOR_SYNC) {
+ if (fromState == AudioStreamState.WAIT_FOR_SYNC
+ && mTimedSourceFromQrCode != null) {
var pendingSource = mTimedSourceFromQrCode.get();
if (pendingSource == null) {
Log.w(
@@ -198,15 +232,20 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
+ fromState
+ " for broadcastId : "
+ broadcastIdFound);
- v.setAudioStreamState(AudioStreamState.SYNCED);
+ v.setAudioStreamMetadata(source);
+ moveToState(v, AudioStreamState.SYNCED);
return v;
}
- mAudioStreamsHelper.addSource(pendingSource);
- mTimedSourceFromQrCode.consumed();
- v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ // A preference with source founded is existed from a QR code scan. As the
+ // source is now synced, we update the preference with pendingSource from QR
+ // code scan and add source with it (since it has the password).
+ v.setAudioStreamMetadata(pendingSource);
+ moveToState(v, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else {
+ // A preference with source founded existed either because it's already
+ // connected (SOURCE_ADDED), or other unexpected reason. We update the
+ // preference with this source and won't change it's state.
+ v.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED) {
Log.w(
TAG,
@@ -229,18 +268,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
metadataFromQrCode.getBroadcastId(),
(k, v) -> {
if (v == null) {
- mTimedSourceFromQrCode.waitForConsume();
- return addNewPreference(
- metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC, null);
+ // No existing preference for this source from the QR code scan, add one and
+ // set initial state to WAIT_FOR_SYNC.
+ return addNewPreference(metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC);
}
var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.SYNCED) {
- mAudioStreamsHelper.addSource(metadataFromQrCode);
- mTimedSourceFromQrCode.consumed();
- v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ // A preference with source from the QR code is existed because it has been
+ // founded during scanning, now we have the password, we can add source.
+ v.setAudioStreamMetadata(metadataFromQrCode);
+ moveToState(v, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else {
+ v.setAudioStreamMetadata(metadataFromQrCode);
Log.w(
TAG,
"handleSourceFromQrCode(): unexpected state : "
@@ -265,54 +304,71 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper.removeSource(broadcastId);
}
+ void handleSourceRemoved() {
+ for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
+ var preference = entry.getValue();
+
+ // Look for preference has SOURCE_ADDED state, re-check if they are still connected. If
+ // not, means the source is removed from the sink, we move back the preference to SYNCED
+ // state.
+ if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
+ && mAudioStreamsHelper.getAllConnectedSources().stream()
+ .noneMatch(
+ connected ->
+ connected.getBroadcastId()
+ == preference.getAudioStreamBroadcastId())) {
+
+ ThreadUtils.postOnMainThread(
+ () -> {
+ var metadata = preference.getAudioStreamMetadata();
+
+ if (metadata != null) {
+ moveToState(preference, AudioStreamState.SYNCED);
+ } else {
+ handleSourceLost(preference.getAudioStreamBroadcastId());
+ }
+ });
+
+ return;
+ }
+ }
+ }
+
void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
if (!mAudioStreamsHelper.isConnected(receiveState)) {
return;
}
- var sourceAddedState = AudioStreamState.SOURCE_ADDED;
var broadcastIdConnected = receiveState.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected,
(k, v) -> {
if (v == null) {
- return addNewPreference(
- receiveState,
- sourceAddedState,
- p -> launchDetailFragment(broadcastIdConnected));
+ // No existing preference for this source even if it's already connected,
+ // add one and set initial state to SOURCE_ADDED. This could happen because
+ // we retrieves the connected source during onStart() from
+ // AudioStreamsHelper#getAllConnectedSources() even before the source is
+ // founded by scanning.
+ return addNewPreference(receiveState, AudioStreamState.SOURCE_ADDED);
}
var fromState = v.getAudioStreamState();
- if (fromState == AudioStreamState.WAIT_FOR_SOURCE_ADD
+ if (fromState == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE
|| fromState == AudioStreamState.SYNCED
- || fromState == AudioStreamState.WAIT_FOR_SYNC) {
- if (mTimedSourceFromQrCode != null) {
- mTimedSourceFromQrCode.consumed();
- }
+ || fromState == AudioStreamState.WAIT_FOR_SYNC
+ || fromState == AudioStreamState.SOURCE_ADDED) {
+ // Expected state, do nothing
} else {
- if (fromState != AudioStreamState.SOURCE_ADDED) {
- Log.w(
- TAG,
- "handleSourceConnected(): unexpected state : "
- + fromState
- + " for broadcastId : "
- + broadcastIdConnected);
- }
+ Log.w(
+ TAG,
+ "handleSourceConnected(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdConnected);
}
- v.setAudioStreamState(sourceAddedState);
- updatePreferenceConnectionState(
- v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected));
+ moveToState(v, AudioStreamState.SOURCE_ADDED);
return v;
});
}
- private static String getPreferenceSummary(AudioStreamState state) {
- return switch (state) {
- case WAIT_FOR_SYNC -> "Scanning...";
- case WAIT_FOR_SOURCE_ADD -> "Connecting...";
- case SOURCE_ADDED -> "Listening now";
- default -> "";
- };
- }
-
void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg);
}
@@ -322,7 +378,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
ThreadUtils.postOnMainThread(
() -> {
if (mCategoryPreference != null) {
- mCategoryPreference.removeAll();
+ mCategoryPreference.removeAudioStreamPreferences();
mCategoryPreference.setVisible(hasActive);
}
});
@@ -348,7 +404,6 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
Log.d(TAG, "startScanning()");
}
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
- mLeBroadcastAssistant.startSearchingForSources(emptyList());
// Handle QR code scan and display currently connected streams
var unused =
@@ -358,6 +413,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper
.getAllConnectedSources()
.forEach(this::handleSourceConnected);
+ mLeBroadcastAssistant.startSearchingForSources(emptyList());
});
}
@@ -374,68 +430,93 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
if (mTimedSourceFromQrCode != null) {
- mTimedSourceFromQrCode.consumed();
+ mTimedSourceFromQrCode.cleanup();
+ mTimedSourceFromQrCode = null;
}
}
private AudioStreamPreference addNewPreference(
- BluetoothLeBroadcastReceiveState receiveState,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
- var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState, state);
- updatePreferenceConnectionState(preference, state, onClickListener);
+ BluetoothLeBroadcastReceiveState receiveState, AudioStreamState state) {
+ var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState);
+ moveToState(preference, state);
return preference;
}
private AudioStreamPreference addNewPreference(
- BluetoothLeBroadcastMetadata metadata,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
- var preference = AudioStreamPreference.fromMetadata(mContext, metadata, state);
- updatePreferenceConnectionState(preference, state, onClickListener);
+ BluetoothLeBroadcastMetadata metadata, AudioStreamState state) {
+ var preference = AudioStreamPreference.fromMetadata(mContext, metadata);
+ moveToState(preference, state);
return preference;
}
- private void updatePreferenceConnectionState(
- AudioStreamPreference preference,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
+ private void moveToState(AudioStreamPreference preference, AudioStreamState state) {
+ if (preference.getAudioStreamState() == state) {
+ return;
+ }
+ preference.setAudioStreamState(state);
+
+ // Perform action according to the new state
+ if (state == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed(preference.getAudioStreamBroadcastId());
+ }
+ var metadata = preference.getAudioStreamMetadata();
+ if (metadata != null) {
+ mAudioStreamsHelper.addSource(metadata);
+ // Cache the metadata that used for add source, if source is added successfully, we
+ // will save it persistently.
+ mAudioStreamsRepository.cacheMetadata(metadata);
+ }
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed(preference.getAudioStreamBroadcastId());
+ }
+ // Saved connected metadata for user to re-join this broadcast later.
+ var cached =
+ mAudioStreamsRepository.getCachedMetadata(
+ preference.getAudioStreamBroadcastId());
+ if (cached != null) {
+ mAudioStreamsRepository.saveMetadata(mContext, cached);
+ }
+ } else if (state == AudioStreamState.WAIT_FOR_SYNC) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.waitForConsume();
+ }
+ }
+
+ // Get preference click listener according to the new state
+ Preference.OnPreferenceClickListener listener;
+ if (state == AudioStreamState.SYNCED) {
+ listener = mAddSourceOrShowDialog;
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ listener = mLaunchDetailFragment;
+ } else {
+ listener = null;
+ }
+
+ // Get preference summary according to the new state
+ String summary;
+ if (state == AudioStreamState.WAIT_FOR_SYNC) {
+ summary = "Scanning...";
+ } else if (state == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE) {
+ summary = "Connecting...";
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ summary = "Listening now";
+ } else {
+ summary = "";
+ }
+
+ // Update UI
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(
- state == AudioStreamState.SOURCE_ADDED,
- getPreferenceSummary(state),
- onClickListener);
+ state == AudioStreamState.SOURCE_ADDED, summary, listener);
if (mCategoryPreference != null) {
- mCategoryPreference.addPreference(preference);
+ mCategoryPreference.addAudioStreamPreference(preference, mComparator);
}
});
}
- 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(
- AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) preference.getTitle());
- broadcast.putInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG, broadcastId);
-
- new SubSettingLauncher(mContext)
- .setTitleText("Audio stream details")
- .setDestination(AudioStreamDetailsFragment.class.getName())
- // TODO(chelseahao): Add logging enum
- .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
- .setArguments(broadcast)
- .launch();
- return true;
- }
-
private void launchPasswordDialog(
BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) {
View layout =
@@ -457,15 +538,16 @@ 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());
- preference.setAudioStreamState(
- AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- preference, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ .build();
+ // Update the metadata after user entered the password
+ preference.setAudioStreamMetadata(metadata);
+ moveToState(
+ preference,
+ AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
})
.create();
alertDialog.show();
@@ -474,16 +556,17 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private AudioStreamsDialogFragment.DialogBuilder getNoLeDeviceDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mContext)
.setTitle("Connect compatible headphones")
- .setSubTitle1(
+ .setSubTitle2(
"To listen to an audio stream, first connect headphones that support LE"
+ " Audio to this device. Learn more")
.setLeftButtonText("Close")
.setLeftButtonOnClickListener(AlertDialog::dismiss)
.setRightButtonText("Connect a device")
.setRightButtonOnClickListener(
- unused ->
- mContext.startActivity(
- new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)));
+ dialog -> {
+ mContext.startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
+ dialog.dismiss();
+ });
}
private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableDialog(
@@ -495,8 +578,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
.setLeftButtonText("Close")
.setLeftButtonOnClickListener(AlertDialog::dismiss)
.setRightButtonText("Retry")
- // TODO(chelseahao): Add retry action
- .setRightButtonOnClickListener(AlertDialog::dismiss);
+ .setRightButtonOnClickListener(
+ dialog -> {
+ if (mFragment != null) {
+ Intent intent = new Intent(mContext, QrCodeScanModeActivity.class);
+ intent.setAction(
+ BluetoothBroadcastUtils
+ .ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER);
+ mFragment.startActivityForResult(
+ intent, REQUEST_SCAN_BT_BROADCAST_QR_CODE);
+ dialog.dismiss();
+ }
+ });
}
private class TimedSourceFromQrCode {
@@ -529,11 +622,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mTimer.start();
}
- private void consumed() {
+ private void cleanup() {
mTimer.cancel();
mSourceFromQrCode = null;
}
+ private void consumed(int broadcastId) {
+ if (mSourceFromQrCode == null || broadcastId != mSourceFromQrCode.getBroadcastId()) {
+ return;
+ }
+ cleanup();
+ }
+
private BluetoothLeBroadcastMetadata get() {
return mSourceFromQrCode;
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
index d2599000ba3..33adc312178 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
@@ -19,9 +19,15 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import android.util.AttributeSet;
+import androidx.annotation.NonNull;
+
import com.android.settings.ProgressCategory;
import com.android.settings.R;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
public AudioStreamsProgressCategoryPreference(Context context) {
@@ -46,6 +52,37 @@ public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
init();
}
+ void addAudioStreamPreference(
+ @NonNull AudioStreamPreference preference,
+ Comparator comparator) {
+ super.addPreference(preference);
+
+ List preferences = getAllAudioStreamPreferences();
+ preferences.sort(comparator);
+ for (int i = 0; i < preferences.size(); i++) {
+ // setOrder to i + 1, since the order 0 preference should always be the
+ // "audio_streams_scan_qr_code"
+ preferences.get(i).setOrder(i + 1);
+ }
+ }
+
+ void removeAudioStreamPreferences() {
+ List streams = getAllAudioStreamPreferences();
+ for (var toRemove : streams) {
+ removePreference(toRemove);
+ }
+ }
+
+ private List getAllAudioStreamPreferences() {
+ List streams = new ArrayList<>();
+ for (int i = 0; i < getPreferenceCount(); i++) {
+ if (getPreference(i) instanceof AudioStreamPreference) {
+ streams.add((AudioStreamPreference) getPreference(i));
+ }
+ }
+ return streams;
+ }
+
private void init() {
setEmptyTextRes(R.string.audio_streams_empty);
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
index 42b38ee2466..2366e70fb33 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
@@ -24,6 +24,9 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
@@ -34,6 +37,7 @@ import com.android.settingslib.qrcode.QrCodeGenerator;
import com.google.zxing.WriterException;
+import java.nio.charset.StandardCharsets;
import java.util.Optional;
public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
@@ -49,30 +53,47 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
public final View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
- getQrCodeBitmap()
- .ifPresent(
- bm ->
+
+ BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
+
+ if (broadcastMetadata != null) {
+ getQrCodeBitmap(broadcastMetadata)
+ .ifPresent(
+ bm -> {
((ImageView) view.requireViewById(R.id.qrcode_view))
- .setImageBitmap(bm));
+ .setImageBitmap(bm);
+ ((TextView) view.requireViewById(R.id.password))
+ .setText(
+ "Password: "
+ + new String(
+ broadcastMetadata
+ .getBroadcastCode(),
+ StandardCharsets.UTF_8));
+ });
+ }
return view;
}
- private Optional getQrCodeBitmap() {
- String broadcastMetadata = getBroadcastMetadataQrCode();
- if (broadcastMetadata.isEmpty()) {
+ private Optional getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata) {
+ if (metadata == null) {
Log.d(TAG, "onCreateView: broadcastMetadata is empty!");
return Optional.empty();
}
-
+ String metadataStr = BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
+ if (metadataStr.isEmpty()) {
+ Log.d(TAG, "onCreateView: metadataStr is empty!");
+ return Optional.empty();
+ }
+ Log.d("chelsea", metadataStr);
try {
int qrcodeSize = getContext().getResources().getDimensionPixelSize(R.dimen.qrcode_size);
- Bitmap bitmap = QrCodeGenerator.encodeQrCode(broadcastMetadata, qrcodeSize);
+ Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize);
return Optional.of(bitmap);
} catch (WriterException e) {
Log.d(
TAG,
"onCreateView: broadcastMetadata "
- + broadcastMetadata
+ + metadata
+ " qrCode generation exception "
+ e);
}
@@ -80,23 +101,24 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
return Optional.empty();
}
- private String getBroadcastMetadataQrCode() {
+ @Nullable
+ private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
LocalBluetoothLeBroadcast localBluetoothLeBroadcast =
Utils.getLocalBtManager(getActivity())
.getProfileManager()
.getLeAudioBroadcastProfile();
if (localBluetoothLeBroadcast == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: localBluetoothLeBroadcast is null!");
- return "";
+ return null;
}
BluetoothLeBroadcastMetadata metadata =
localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata();
if (metadata == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!");
- return "";
+ return null;
}
- return BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
+ return metadata;
}
}
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..24e1ca3e349 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;
@@ -58,14 +57,12 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController
};
private final LocalBluetoothManager mLocalBtManager;
- private final AudioStreamsHelper mAudioStreamsHelper;
private AudioStreamsDashboardFragment mFragment;
private Preference mPreference;
public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
super(context, preferenceKey);
mLocalBtManager = Utils.getLocalBtManager(mContext);
- mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
}
public void setFragment(AudioStreamsDashboardFragment fragment) {
@@ -124,10 +121,6 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController
});
}
- void addSource(BluetoothLeBroadcastMetadata source) {
- mAudioStreamsHelper.addSource(source);
- }
-
private void updateVisibility() {
ThreadUtils.postOnBackgroundThread(
() -> {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
index 2b52039768e..378128d069e 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
@@ -229,6 +229,7 @@ public class QrCodeScanModeFragment extends InstrumentedFragment
}
mErrorMessage.setVisibility(View.INVISIBLE);
+ mTextureView.setVisibility(View.INVISIBLE);
triggerVibrationForQrCodeRecognition(getContext());