Merge changes I99011feb,Ie066077f,I44e631cb into main

* changes:
  [Audiosharing] Handle source remove plus small refactor.
  [Audiosharing] Some UI tweaks (e.g, sort by RSSI)
  [Audiosharing] Add button action in detail page.
This commit is contained in:
Chelsea Hao
2024-01-26 09:48:42 +00:00
committed by Android (Google) Code Review
16 changed files with 931 additions and 288 deletions

View File

@@ -17,7 +17,6 @@
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
@@ -26,36 +25,22 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="3" android:layout_weight="3"
android:layout_marginBottom="35dp"> android:layout_marginBottom="55dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="40dp"
android:paddingEnd="40dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:orientation="vertical">
<ImageView
android:src="@drawable/ic_qr_code_scanner"
android:tint="?androidprv:attr/materialColorPrimaryContainer"
android:layout_width="@dimen/qrcode_icon_size"
android:layout_height="@dimen/qrcode_icon_size"
android:contentDescription="@null"/>
<TextView <TextView
style="@style/QrCodeScanner" style="@style/QrCodeScanner"
android:textSize="24sp" android:text="Scan an audio stream QR code to listen with the active LE device"
android:text="@string/bluetooth_find_broadcast_button_scan"
android:gravity="center" android:gravity="center"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="19dp"/> android:layout_marginTop="20dp"/>
<TextView
style="@style/QrCodeScanner"
android:text="@string/bt_le_audio_scan_qr_code_scanner"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -18,23 +18,31 @@
<PreferenceScreen <PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto" xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/audio_streams_title"> android:title="Find an audio stream">
<Preference <com.android.settingslib.widget.TopIntroPreference
android:key="audio_streams_scan_qr_code" android:key="audio_streams_top_intro"
android:title="@string/bluetooth_find_broadcast_button_scan" android:title="Listen to a device that's sharing audio or to a nearby Auracast broadcast"
android:icon="@drawable/ic_add_24dp" settings:searchable="false"/>
android:summary="@string/audio_streams_qr_code_summary"
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController" />
<Preference <Preference
android:key="audio_streams_active_device" android:key="audio_streams_active_device"
android:title="Listen with" android:title="Your audio device"
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsActiveDeviceController" /> settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsActiveDeviceController" />
<com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference <com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference
android:key="audio_streams_nearby_category" android:key="audio_streams_nearby_category"
android:title="@string/audio_streams_pref_title" android:title="Audio streams nearby"
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController" /> settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController">
<Preference
android:key="audio_streams_scan_qr_code"
android:title="Scan a QR code"
android:icon="@drawable/ic_add_24dp"
android:summary="Start listening by scanning a stream's QR code"
android:order="0"
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController" />
</com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference>
</PreferenceScreen> </PreferenceScreen>

View File

@@ -16,6 +16,7 @@
--> -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -23,70 +24,78 @@
android:id="@+id/dialog_bg" android:id="@+id/dialog_bg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingStart="25dp"
android:paddingEnd="25dp"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/broadcast_dialog_margin" android:layout_marginBottom="25dp"
android:orientation="vertical"> android:orientation="vertical">
<ImageView <ImageView
android:id="@+id/dialog_icon" android:id="@+id/dialog_icon"
android:layout_width="36dp" android:layout_width="30dp"
android:layout_height="36dp" android:layout_height="30dp"
android:layout_marginTop="@dimen/broadcast_dialog_icon_margin_top" android:layout_marginTop="24dp"
android:layout_marginBottom="@dimen/broadcast_dialog_title_img_margin_top"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_bt_audio_sharing"/> android:src="@drawable/ic_bt_audio_sharing"/>
<TextView <TextView
style="@style/BroadcastDialogTitleStyle"
android:id="@+id/dialog_title" android:id="@+id/dialog_title"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:gravity="center" android:gravity="center"
android:layout_gravity="center"/> android:layout_gravity="center"/>
<TextView <TextView
style="@style/BroadcastDialogBodyStyle"
android:id="@+id/dialog_subtitle" android:id="@+id/dialog_subtitle"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
android:textStyle="bold"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:gravity="center" android:gravity="center"
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone"/> android:visibility="gone"/>
<TextView <TextView
style="@style/BroadcastDialogBodyStyle"
android:id="@+id/dialog_subtitle_2" android:id="@+id/dialog_subtitle_2"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:gravity="center" android:gravity="center"
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/broadcast_dialog_margin" android:layout_marginBottom="@dimen/broadcast_dialog_margin">
android:orientation="horizontal">
<Button <Button
android:id="@+id/left_button" android:id="@+id/left_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="16dp" android:visibility="gone"
android:layout_weight="1" app:layout_constraintBottom_toBottomOf="parent"
android:visibility="invisible"/> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
style="@style/BroadcastActionButton"/>
<Button <Button
android:id="@+id/right_button" android:id="@+id/right_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:visibility="gone"
android:layout_marginRight="16dp" app:layout_constraintBottom_toBottomOf="parent"
android:visibility="invisible"/> app:layout_constraintEnd_toEndOf="parent"
</LinearLayout> app:layout_constraintTop_toTopOf="parent"
style="@style/BroadcastActionButton"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>

View File

@@ -36,7 +36,7 @@
android:gravity="start" android:gravity="start"
android:textSize="15sp" android:textSize="15sp"
android:textColor="?android:attr/textColorPrimary" 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."/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -50,6 +50,13 @@
android:layout_width="@dimen/qrcode_size" android:layout_width="@dimen/qrcode_size"
android:layout_height="@dimen/qrcode_size" android:layout_height="@dimen/qrcode_size"
android:src="@android:color/transparent"/> android:src="@android:color/transparent"/>
<TextView
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="15sp"
android:textColor="?android:attr/textColorPrimary"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -16,39 +16,170 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; 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.content.Context;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController; 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 com.android.settingslib.widget.ActionButtonsPreference;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AudioStreamButtonController extends BasePreferenceController public class AudioStreamButtonController extends BasePreferenceController
implements DefaultLifecycleObserver { implements DefaultLifecycleObserver {
private static final String TAG = "AudioStreamButtonController";
private static final String KEY = "audio_stream_button"; 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 @Nullable ActionButtonsPreference mPreference;
private int mBroadcastId = -1; private int mBroadcastId = -1;
public AudioStreamButtonController(Context context, String preferenceKey) { public AudioStreamButtonController(Context context, String preferenceKey) {
super(context, 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 @Override
public final void displayPreference(PreferenceScreen screen) { public final void displayPreference(PreferenceScreen screen) {
mPreference = screen.findPreference(getPreferenceKey()); mPreference = screen.findPreference(getPreferenceKey());
if (mPreference != null) { updateButton();
mPreference.setButton1Enabled(true);
// TODO(chelseahao): update this based on stream connection state
mPreference
.setButton1Text(R.string.bluetooth_device_context_disconnect)
.setButton1Icon(R.drawable.ic_settings_close);
}
super.displayPreference(screen); 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 @Override
public int getAvailabilityStatus() { public int getAvailabilityStatus() {
return AVAILABLE; return AVAILABLE;

View File

@@ -104,7 +104,7 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
private Dialog getErrorDialog() { private Dialog getErrorDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mActivity) return new AudioStreamsDialogFragment.DialogBuilder(mActivity)
.setTitle("Can't listen to audio stream") .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") .setRightButtonText("Close")
.setRightButtonOnClickListener( .setRightButtonOnClickListener(
unused -> { unused -> {

View File

@@ -16,22 +16,64 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; 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.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController; import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.EntityHeaderController; 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 com.android.settingslib.widget.LayoutPreference;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public class AudioStreamHeaderController extends BasePreferenceController public class AudioStreamHeaderController extends BasePreferenceController
implements DefaultLifecycleObserver { implements DefaultLifecycleObserver {
private static final String TAG = "AudioStreamHeaderController";
private static final String KEY = "audio_stream_header"; 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 EntityHeaderController mHeaderController;
private @Nullable DashboardFragment mFragment; private @Nullable DashboardFragment mFragment;
private String mBroadcastName = ""; private String mBroadcastName = "";
@@ -39,6 +81,27 @@ public class AudioStreamHeaderController extends BasePreferenceController
public AudioStreamHeaderController(Context context, String preferenceKey) { public AudioStreamHeaderController(Context context, String preferenceKey) {
super(context, 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 @Override
@@ -55,14 +118,37 @@ public class AudioStreamHeaderController extends BasePreferenceController
} }
mHeaderController.setIcon( mHeaderController.setIcon(
screen.getContext().getDrawable(R.drawable.ic_bt_audio_sharing)); 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); screen.addPreference(headerPreference);
updateSummary();
} }
super.displayPreference(screen); 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 @Override
public int getAvailabilityStatus() { public int getAvailabilityStatus() {
return AVAILABLE; return AVAILABLE;

View File

@@ -21,8 +21,10 @@ import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R; import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference; import com.android.settingslib.widget.TwoTargetPreference;
@@ -56,7 +58,6 @@ class AudioStreamPreference extends TwoTargetPreference {
} }
mIsConnected = isConnected; mIsConnected = isConnected;
setSummary(summary); setSummary(summary);
setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener); setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged(); notifyChanged();
} }
@@ -70,6 +71,23 @@ class AudioStreamPreference extends TwoTargetPreference {
mAudioStream.setState(state); 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() { AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
return mAudioStream.getState(); return mAudioStream.getState();
} }
@@ -84,25 +102,31 @@ class AudioStreamPreference extends TwoTargetPreference {
return R.layout.preference_widget_lock; 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( static AudioStreamPreference fromMetadata(
Context context, Context context, BluetoothLeBroadcastMetadata source) {
BluetoothLeBroadcastMetadata source,
AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null); AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source)); preference.setTitle(getBroadcastName(source));
preference.setAudioStream(new AudioStream(source.getBroadcastId(), streamState)); preference.setAudioStream(new AudioStream(source));
return preference; return preference;
} }
static AudioStreamPreference fromReceiveState( static AudioStreamPreference fromReceiveState(
Context context, Context context, BluetoothLeBroadcastReceiveState receiveState) {
BluetoothLeBroadcastReceiveState receiveState,
AudioStreamsProgressCategoryController.AudioStreamState streamState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null); AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(receiveState)); preference.setTitle(getBroadcastName(receiveState));
preference.setAudioStream( preference.setAudioStream(new AudioStream(receiveState));
new AudioStream(
receiveState.getSourceId(), receiveState.getBroadcastId(), streamState));
return preference; return preference;
} }
@@ -127,41 +151,45 @@ class AudioStreamPreference extends TwoTargetPreference {
} }
private static final class AudioStream { private static final class AudioStream {
private int mSourceId; private static final int UNAVAILABLE = -1;
private int mBroadcastId; @Nullable private BluetoothLeBroadcastMetadata mMetadata;
private AudioStreamsProgressCategoryController.AudioStreamState mState; @Nullable private BluetoothLeBroadcastReceiveState mReceiveState;
private AudioStreamsProgressCategoryController.AudioStreamState mState =
AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
private AudioStream( private AudioStream(BluetoothLeBroadcastMetadata metadata) {
int broadcastId, AudioStreamsProgressCategoryController.AudioStreamState state) { mMetadata = metadata;
mBroadcastId = broadcastId;
mState = state;
} }
private AudioStream( private AudioStream(BluetoothLeBroadcastReceiveState receiveState) {
int sourceId, mReceiveState = receiveState;
int broadcastId,
AudioStreamsProgressCategoryController.AudioStreamState state) {
mSourceId = sourceId;
mBroadcastId = broadcastId;
mState = state;
} }
// TODO(chelseahao): use this to handleSourceRemoved
private int getSourceId() {
return mSourceId;
}
// TODO(chelseahao): use this to handleSourceRemoved
private int getBroadcastId() { 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() { private AudioStreamsProgressCategoryController.AudioStreamState getState() {
return mState; return mState;
} }
@Nullable
private BluetoothLeBroadcastMetadata getMetadata() {
return mMetadata;
}
private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) { private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
mState = state; mState = state;
} }
private void setMetadata(BluetoothLeBroadcastMetadata metadata) {
mMetadata = metadata;
}
} }
} }

View File

@@ -24,21 +24,12 @@ import android.util.Log;
import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.BluetoothUtils;
import java.util.Locale;
public class AudioStreamsBroadcastAssistantCallback public class AudioStreamsBroadcastAssistantCallback
implements BluetoothLeBroadcastAssistant.Callback { implements BluetoothLeBroadcastAssistant.Callback {
private static final String TAG = "AudioStreamsBroadcastAssistantCallback"; private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
private static final boolean DEBUG = BluetoothUtils.D; private static final boolean DEBUG = BluetoothUtils.D;
private final AudioStreamsProgressCategoryController mCategoryController;
public AudioStreamsBroadcastAssistantCallback(
AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
mCategoryController = audioStreamsProgressCategoryController;
}
@Override @Override
public void onReceiveStateChanged( public void onReceiveStateChanged(
BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
@@ -52,45 +43,30 @@ public class AudioStreamsBroadcastAssistantCallback
+ " state: " + " state: "
+ state); + state);
} }
mCategoryController.handleSourceConnected(state);
} }
@Override @Override
public void onSearchStartFailed(int reason) { public void onSearchStartFailed(int reason) {
Log.w(TAG, "onSearchStartFailed() reason : " + reason); Log.w(TAG, "onSearchStartFailed() reason : " + reason);
mCategoryController.showToast(
String.format(Locale.US, "Failed to start scanning, reason %d", reason));
} }
@Override @Override
public void onSearchStarted(int reason) { public void onSearchStarted(int reason) {
if (mCategoryController == null) {
Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
return;
}
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onSearchStarted() reason : " + reason); Log.d(TAG, "onSearchStarted() reason : " + reason);
} }
mCategoryController.setScanning(true);
} }
@Override @Override
public void onSearchStopFailed(int reason) { public void onSearchStopFailed(int reason) {
Log.w(TAG, "onSearchStopFailed() reason : " + reason); Log.w(TAG, "onSearchStopFailed() reason : " + reason);
mCategoryController.showToast(
String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
} }
@Override @Override
public void onSearchStopped(int reason) { public void onSearchStopped(int reason) {
if (mCategoryController == null) {
Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
return;
}
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onSearchStopped() reason : " + reason); Log.d(TAG, "onSearchStopped() reason : " + reason);
} }
mCategoryController.setScanning(false);
} }
@Override @Override
@@ -106,8 +82,6 @@ public class AudioStreamsBroadcastAssistantCallback
+ " reason: " + " reason: "
+ reason); + reason);
} }
mCategoryController.showToast(
String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
} }
@Override @Override
@@ -126,14 +100,9 @@ public class AudioStreamsBroadcastAssistantCallback
@Override @Override
public void onSourceFound(BluetoothLeBroadcastMetadata source) { public void onSourceFound(BluetoothLeBroadcastMetadata source) {
if (mCategoryController == null) {
Log.w(TAG, "onSourceFound() : mCategoryController is null!");
return;
}
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId()); Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
} }
mCategoryController.handleSourceFound(source);
} }
@Override @Override
@@ -141,7 +110,6 @@ public class AudioStreamsBroadcastAssistantCallback
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId); Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
} }
mCategoryController.handleSourceLost(broadcastId);
} }
@Override @Override
@@ -153,12 +121,6 @@ public class AudioStreamsBroadcastAssistantCallback
@Override @Override
public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + 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 @Override
@@ -166,8 +128,5 @@ public class AudioStreamsBroadcastAssistantCallback
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason); Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
} }
mCategoryController.showToast(
String.format(
Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress()));
} }
} }

View File

@@ -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();
}
}

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; 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 static java.util.Collections.emptyList;
import android.app.AlertDialog; import android.app.AlertDialog;
@@ -43,8 +45,10 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.bluetooth.Utils; import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils; 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.BasePreferenceController;
import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -53,6 +57,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; 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 { enum AudioStreamState {
UNKNOWN,
// When mTimedSourceFromQrCode is present and this source has not been synced. // When mTimedSourceFromQrCode is present and this source has not been synced.
WAIT_FOR_SYNC, WAIT_FOR_SYNC,
// When source has been synced but not added to any sink. // When source has been synced but not added to any sink.
SYNCED, SYNCED,
// When addSource is called for this source and waiting for response. // 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 is added to active sink.
SOURCE_ADDED, SOURCE_ADDED,
} }
private final Comparator<AudioStreamPreference> mComparator =
Comparator.<AudioStreamPreference, Boolean>comparing(
p ->
p.getAudioStreamState()
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_ADDED)
.thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
.reversed();
private final Executor mExecutor; private final Executor mExecutor;
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback; private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper; private final AudioStreamsHelper mAudioStreamsHelper;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private final @Nullable LocalBluetoothManager mBluetoothManager; private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap = private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private TimedSourceFromQrCode mTimedSourceFromQrCode; private @Nullable TimedSourceFromQrCode mTimedSourceFromQrCode;
private AudioStreamsProgressCategoryPreference mCategoryPreference; private AudioStreamsProgressCategoryPreference mCategoryPreference;
private AudioStreamsDashboardFragment mFragment; private AudioStreamsDashboardFragment mFragment;
@@ -102,7 +159,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mBluetoothManager = Utils.getLocalBtManager(mContext); mBluetoothManager = Utils.getLocalBtManager(mContext);
mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager); mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this); mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
} }
@Override @Override
@@ -155,41 +212,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
} }
void handleSourceFound(BluetoothLeBroadcastMetadata source) { 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(); var broadcastIdFound = source.getBroadcastId();
mBroadcastIdToPreferenceMap.compute( mBroadcastIdToPreferenceMap.compute(
broadcastIdFound, broadcastIdFound,
(k, v) -> { (k, v) -> {
if (v == null) { if (v == null) {
return addNewPreference( // No existing preference for this source founded, add one and set initial
source, AudioStreamState.SYNCED, addSourceOrShowDialog); // state to SYNCED.
return addNewPreference(source, AudioStreamState.SYNCED);
} }
var fromState = v.getAudioStreamState(); var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.WAIT_FOR_SYNC) { if (fromState == AudioStreamState.WAIT_FOR_SYNC
&& mTimedSourceFromQrCode != null) {
var pendingSource = mTimedSourceFromQrCode.get(); var pendingSource = mTimedSourceFromQrCode.get();
if (pendingSource == null) { if (pendingSource == null) {
Log.w( Log.w(
@@ -198,15 +232,20 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
+ fromState + fromState
+ " for broadcastId : " + " for broadcastId : "
+ broadcastIdFound); + broadcastIdFound);
v.setAudioStreamState(AudioStreamState.SYNCED); v.setAudioStreamMetadata(source);
moveToState(v, AudioStreamState.SYNCED);
return v; return v;
} }
mAudioStreamsHelper.addSource(pendingSource); // A preference with source founded is existed from a QR code scan. As the
mTimedSourceFromQrCode.consumed(); // source is now synced, we update the preference with pendingSource from QR
v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD); // code scan and add source with it (since it has the password).
updatePreferenceConnectionState( v.setAudioStreamMetadata(pendingSource);
v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null); moveToState(v, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else { } 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) { if (fromState != AudioStreamState.SOURCE_ADDED) {
Log.w( Log.w(
TAG, TAG,
@@ -229,18 +268,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
metadataFromQrCode.getBroadcastId(), metadataFromQrCode.getBroadcastId(),
(k, v) -> { (k, v) -> {
if (v == null) { if (v == null) {
mTimedSourceFromQrCode.waitForConsume(); // No existing preference for this source from the QR code scan, add one and
return addNewPreference( // set initial state to WAIT_FOR_SYNC.
metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC, null); return addNewPreference(metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC);
} }
var fromState = v.getAudioStreamState(); var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.SYNCED) { if (fromState == AudioStreamState.SYNCED) {
mAudioStreamsHelper.addSource(metadataFromQrCode); // A preference with source from the QR code is existed because it has been
mTimedSourceFromQrCode.consumed(); // founded during scanning, now we have the password, we can add source.
v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD); v.setAudioStreamMetadata(metadataFromQrCode);
updatePreferenceConnectionState( moveToState(v, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
} else { } else {
v.setAudioStreamMetadata(metadataFromQrCode);
Log.w( Log.w(
TAG, TAG,
"handleSourceFromQrCode(): unexpected state : " "handleSourceFromQrCode(): unexpected state : "
@@ -265,54 +304,71 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper.removeSource(broadcastId); 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) { void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
if (!mAudioStreamsHelper.isConnected(receiveState)) { if (!mAudioStreamsHelper.isConnected(receiveState)) {
return; return;
} }
var sourceAddedState = AudioStreamState.SOURCE_ADDED;
var broadcastIdConnected = receiveState.getBroadcastId(); var broadcastIdConnected = receiveState.getBroadcastId();
mBroadcastIdToPreferenceMap.compute( mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected, broadcastIdConnected,
(k, v) -> { (k, v) -> {
if (v == null) { if (v == null) {
return addNewPreference( // No existing preference for this source even if it's already connected,
receiveState, // add one and set initial state to SOURCE_ADDED. This could happen because
sourceAddedState, // we retrieves the connected source during onStart() from
p -> launchDetailFragment(broadcastIdConnected)); // AudioStreamsHelper#getAllConnectedSources() even before the source is
// founded by scanning.
return addNewPreference(receiveState, AudioStreamState.SOURCE_ADDED);
} }
var fromState = v.getAudioStreamState(); var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.WAIT_FOR_SOURCE_ADD if (fromState == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE
|| fromState == AudioStreamState.SYNCED || fromState == AudioStreamState.SYNCED
|| fromState == AudioStreamState.WAIT_FOR_SYNC) { || fromState == AudioStreamState.WAIT_FOR_SYNC
if (mTimedSourceFromQrCode != null) { || fromState == AudioStreamState.SOURCE_ADDED) {
mTimedSourceFromQrCode.consumed(); // Expected state, do nothing
}
} else { } else {
if (fromState != AudioStreamState.SOURCE_ADDED) { Log.w(
Log.w( TAG,
TAG, "handleSourceConnected(): unexpected state : "
"handleSourceConnected(): unexpected state : " + fromState
+ fromState + " for broadcastId : "
+ " for broadcastId : " + broadcastIdConnected);
+ broadcastIdConnected);
}
} }
v.setAudioStreamState(sourceAddedState); moveToState(v, AudioStreamState.SOURCE_ADDED);
updatePreferenceConnectionState(
v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected));
return v; 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) { void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg); AudioSharingUtils.toastMessage(mContext, msg);
} }
@@ -322,7 +378,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
ThreadUtils.postOnMainThread( ThreadUtils.postOnMainThread(
() -> { () -> {
if (mCategoryPreference != null) { if (mCategoryPreference != null) {
mCategoryPreference.removeAll(); mCategoryPreference.removeAudioStreamPreferences();
mCategoryPreference.setVisible(hasActive); mCategoryPreference.setVisible(hasActive);
} }
}); });
@@ -348,7 +404,6 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
Log.d(TAG, "startScanning()"); Log.d(TAG, "startScanning()");
} }
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mLeBroadcastAssistant.startSearchingForSources(emptyList());
// Handle QR code scan and display currently connected streams // Handle QR code scan and display currently connected streams
var unused = var unused =
@@ -358,6 +413,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper mAudioStreamsHelper
.getAllConnectedSources() .getAllConnectedSources()
.forEach(this::handleSourceConnected); .forEach(this::handleSourceConnected);
mLeBroadcastAssistant.startSearchingForSources(emptyList());
}); });
} }
@@ -374,68 +430,93 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
} }
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
if (mTimedSourceFromQrCode != null) { if (mTimedSourceFromQrCode != null) {
mTimedSourceFromQrCode.consumed(); mTimedSourceFromQrCode.cleanup();
mTimedSourceFromQrCode = null;
} }
} }
private AudioStreamPreference addNewPreference( private AudioStreamPreference addNewPreference(
BluetoothLeBroadcastReceiveState receiveState, BluetoothLeBroadcastReceiveState receiveState, AudioStreamState state) {
AudioStreamState state, var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState);
Preference.OnPreferenceClickListener onClickListener) { moveToState(preference, state);
var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState, state);
updatePreferenceConnectionState(preference, state, onClickListener);
return preference; return preference;
} }
private AudioStreamPreference addNewPreference( private AudioStreamPreference addNewPreference(
BluetoothLeBroadcastMetadata metadata, BluetoothLeBroadcastMetadata metadata, AudioStreamState state) {
AudioStreamState state, var preference = AudioStreamPreference.fromMetadata(mContext, metadata);
Preference.OnPreferenceClickListener onClickListener) { moveToState(preference, state);
var preference = AudioStreamPreference.fromMetadata(mContext, metadata, state);
updatePreferenceConnectionState(preference, state, onClickListener);
return preference; return preference;
} }
private void updatePreferenceConnectionState( private void moveToState(AudioStreamPreference preference, AudioStreamState state) {
AudioStreamPreference preference, if (preference.getAudioStreamState() == state) {
AudioStreamState state, return;
Preference.OnPreferenceClickListener onClickListener) { }
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( ThreadUtils.postOnMainThread(
() -> { () -> {
preference.setIsConnected( preference.setIsConnected(
state == AudioStreamState.SOURCE_ADDED, state == AudioStreamState.SOURCE_ADDED, summary, listener);
getPreferenceSummary(state),
onClickListener);
if (mCategoryPreference != null) { 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( private void launchPasswordDialog(
BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) { BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) {
View layout = View layout =
@@ -457,15 +538,16 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
R.id.broadcast_edit_text)) R.id.broadcast_edit_text))
.getText() .getText()
.toString(); .toString();
mAudioStreamsHelper.addSource( var metadata =
new BluetoothLeBroadcastMetadata.Builder(source) new BluetoothLeBroadcastMetadata.Builder(source)
.setBroadcastCode( .setBroadcastCode(
code.getBytes(StandardCharsets.UTF_8)) code.getBytes(StandardCharsets.UTF_8))
.build()); .build();
preference.setAudioStreamState( // Update the metadata after user entered the password
AudioStreamState.WAIT_FOR_SOURCE_ADD); preference.setAudioStreamMetadata(metadata);
updatePreferenceConnectionState( moveToState(
preference, AudioStreamState.WAIT_FOR_SOURCE_ADD, null); preference,
AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
}) })
.create(); .create();
alertDialog.show(); alertDialog.show();
@@ -474,16 +556,17 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private AudioStreamsDialogFragment.DialogBuilder getNoLeDeviceDialog() { private AudioStreamsDialogFragment.DialogBuilder getNoLeDeviceDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mContext) return new AudioStreamsDialogFragment.DialogBuilder(mContext)
.setTitle("Connect compatible headphones") .setTitle("Connect compatible headphones")
.setSubTitle1( .setSubTitle2(
"To listen to an audio stream, first connect headphones that support LE" "To listen to an audio stream, first connect headphones that support LE"
+ " Audio to this device. Learn more") + " Audio to this device. Learn more")
.setLeftButtonText("Close") .setLeftButtonText("Close")
.setLeftButtonOnClickListener(AlertDialog::dismiss) .setLeftButtonOnClickListener(AlertDialog::dismiss)
.setRightButtonText("Connect a device") .setRightButtonText("Connect a device")
.setRightButtonOnClickListener( .setRightButtonOnClickListener(
unused -> dialog -> {
mContext.startActivity( mContext.startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
new Intent(Settings.ACTION_BLUETOOTH_SETTINGS))); dialog.dismiss();
});
} }
private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableDialog( private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableDialog(
@@ -495,8 +578,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
.setLeftButtonText("Close") .setLeftButtonText("Close")
.setLeftButtonOnClickListener(AlertDialog::dismiss) .setLeftButtonOnClickListener(AlertDialog::dismiss)
.setRightButtonText("Retry") .setRightButtonText("Retry")
// TODO(chelseahao): Add retry action .setRightButtonOnClickListener(
.setRightButtonOnClickListener(AlertDialog::dismiss); 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 { private class TimedSourceFromQrCode {
@@ -529,11 +622,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mTimer.start(); mTimer.start();
} }
private void consumed() { private void cleanup() {
mTimer.cancel(); mTimer.cancel();
mSourceFromQrCode = null; mSourceFromQrCode = null;
} }
private void consumed(int broadcastId) {
if (mSourceFromQrCode == null || broadcastId != mSourceFromQrCode.getBroadcastId()) {
return;
}
cleanup();
}
private BluetoothLeBroadcastMetadata get() { private BluetoothLeBroadcastMetadata get() {
return mSourceFromQrCode; return mSourceFromQrCode;
} }

View File

@@ -19,9 +19,15 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import androidx.annotation.NonNull;
import com.android.settings.ProgressCategory; import com.android.settings.ProgressCategory;
import com.android.settings.R; import com.android.settings.R;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class AudioStreamsProgressCategoryPreference extends ProgressCategory { public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
public AudioStreamsProgressCategoryPreference(Context context) { public AudioStreamsProgressCategoryPreference(Context context) {
@@ -46,6 +52,37 @@ public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
init(); init();
} }
void addAudioStreamPreference(
@NonNull AudioStreamPreference preference,
Comparator<AudioStreamPreference> comparator) {
super.addPreference(preference);
List<AudioStreamPreference> 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<AudioStreamPreference> streams = getAllAudioStreamPreferences();
for (var toRemove : streams) {
removePreference(toRemove);
}
}
private List<AudioStreamPreference> getAllAudioStreamPreferences() {
List<AudioStreamPreference> 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() { private void init() {
setEmptyTextRes(R.string.audio_streams_empty); setEmptyTextRes(R.string.audio_streams_empty);
} }

View File

@@ -24,6 +24,9 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.bluetooth.Utils; import com.android.settings.bluetooth.Utils;
@@ -34,6 +37,7 @@ import com.android.settingslib.qrcode.QrCodeGenerator;
import com.google.zxing.WriterException; import com.google.zxing.WriterException;
import java.nio.charset.StandardCharsets;
import java.util.Optional; import java.util.Optional;
public class AudioStreamsQrCodeFragment extends InstrumentedFragment { public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
@@ -49,30 +53,47 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
public final View onCreateView( public final View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false); View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
getQrCodeBitmap()
.ifPresent( BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
bm ->
if (broadcastMetadata != null) {
getQrCodeBitmap(broadcastMetadata)
.ifPresent(
bm -> {
((ImageView) view.requireViewById(R.id.qrcode_view)) ((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; return view;
} }
private Optional<Bitmap> getQrCodeBitmap() { private Optional<Bitmap> getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata) {
String broadcastMetadata = getBroadcastMetadataQrCode(); if (metadata == null) {
if (broadcastMetadata.isEmpty()) {
Log.d(TAG, "onCreateView: broadcastMetadata is empty!"); Log.d(TAG, "onCreateView: broadcastMetadata is empty!");
return Optional.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 { try {
int qrcodeSize = getContext().getResources().getDimensionPixelSize(R.dimen.qrcode_size); 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); return Optional.of(bitmap);
} catch (WriterException e) { } catch (WriterException e) {
Log.d( Log.d(
TAG, TAG,
"onCreateView: broadcastMetadata " "onCreateView: broadcastMetadata "
+ broadcastMetadata + metadata
+ " qrCode generation exception " + " qrCode generation exception "
+ e); + e);
} }
@@ -80,23 +101,24 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
return Optional.empty(); return Optional.empty();
} }
private String getBroadcastMetadataQrCode() { @Nullable
private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
LocalBluetoothLeBroadcast localBluetoothLeBroadcast = LocalBluetoothLeBroadcast localBluetoothLeBroadcast =
Utils.getLocalBtManager(getActivity()) Utils.getLocalBtManager(getActivity())
.getProfileManager() .getProfileManager()
.getLeAudioBroadcastProfile(); .getLeAudioBroadcastProfile();
if (localBluetoothLeBroadcast == null) { if (localBluetoothLeBroadcast == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: localBluetoothLeBroadcast is null!"); Log.d(TAG, "getBroadcastMetadataQrCode: localBluetoothLeBroadcast is null!");
return ""; return null;
} }
BluetoothLeBroadcastMetadata metadata = BluetoothLeBroadcastMetadata metadata =
localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata(); localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata();
if (metadata == null) { if (metadata == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!"); Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!");
return ""; return null;
} }
return BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata); return metadata;
} }
} }

View File

@@ -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<Integer, BluetoothLeBroadcastMetadata>
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;
}
}

View File

@@ -16,7 +16,6 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -58,14 +57,12 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController
}; };
private final LocalBluetoothManager mLocalBtManager; private final LocalBluetoothManager mLocalBtManager;
private final AudioStreamsHelper mAudioStreamsHelper;
private AudioStreamsDashboardFragment mFragment; private AudioStreamsDashboardFragment mFragment;
private Preference mPreference; private Preference mPreference;
public AudioStreamsScanQrCodeController(Context context, String preferenceKey) { public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
super(context, preferenceKey); super(context, preferenceKey);
mLocalBtManager = Utils.getLocalBtManager(mContext); mLocalBtManager = Utils.getLocalBtManager(mContext);
mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
} }
public void setFragment(AudioStreamsDashboardFragment fragment) { public void setFragment(AudioStreamsDashboardFragment fragment) {
@@ -124,10 +121,6 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController
}); });
} }
void addSource(BluetoothLeBroadcastMetadata source) {
mAudioStreamsHelper.addSource(source);
}
private void updateVisibility() { private void updateVisibility() {
ThreadUtils.postOnBackgroundThread( ThreadUtils.postOnBackgroundThread(
() -> { () -> {

View File

@@ -229,6 +229,7 @@ public class QrCodeScanModeFragment extends InstrumentedFragment
} }
mErrorMessage.setVisibility(View.INVISIBLE); mErrorMessage.setVisibility(View.INVISIBLE);
mTextureView.setVisibility(View.INVISIBLE);
triggerVibrationForQrCodeRecognition(getContext()); triggerVibrationForQrCodeRecognition(getContext());