Snap for 11367679 from 8eb26342de to 24Q2-release
Change-Id: I5087de78039d530b2116d20cdcdf404ac774b4bb
This commit is contained in:
@@ -17,7 +17,6 @@
|
||||
|
||||
<LinearLayout
|
||||
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_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
@@ -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">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="40dp"
|
||||
android:paddingEnd="40dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center"
|
||||
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
|
||||
style="@style/QrCodeScanner"
|
||||
android:textSize="24sp"
|
||||
android:text="@string/bluetooth_find_broadcast_button_scan"
|
||||
android:text="Scan an audio stream QR code to listen with the active LE device"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="19dp"/>
|
||||
|
||||
<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"/>
|
||||
android:layout_marginTop="20dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -233,6 +233,26 @@
|
||||
|
||||
<!-- Bluetooth Settings -->
|
||||
|
||||
<!-- Bluetooth developer settings: Bluetooth LE Audio modes -->
|
||||
<string-array name="bluetooth_leaudio_mode">
|
||||
<!-- Do not translate. -->
|
||||
<item>Disabled</item>
|
||||
<!-- Do not translate. -->
|
||||
<item>Unicast</item>
|
||||
<!-- Do not translate. -->
|
||||
<item>Unicast and Broadcast</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Values for Bluetooth LE Audio mode -->
|
||||
<string-array name="bluetooth_leaudio_mode_values" translatable="false">
|
||||
<!-- Do not translate. -->
|
||||
<item>disabled</item>
|
||||
<!-- Do not translate. -->
|
||||
<item>unicast</item>
|
||||
<!-- Do not translate. -->
|
||||
<item>broadcast</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Bluetooth developer settings: Titles for maximum number of connected audio devices -->
|
||||
<string-array name="bluetooth_max_connected_audio_devices">
|
||||
<item>Use System Default: <xliff:g id="default_bluetooth_max_connected_audio_devices">%1$d</xliff:g></item>
|
||||
|
||||
@@ -249,7 +249,8 @@
|
||||
<string name="bluetooth_disable_leaudio">Disable Bluetooth LE audio</string>
|
||||
<!-- Summary of toggle for disabling Bluetooth LE audio [CHAR LIMIT=none]-->
|
||||
<string name="bluetooth_disable_leaudio_summary">Disables Bluetooth LE audio feature if the device supports LE audio hardware capabilities.</string>
|
||||
|
||||
<!-- Setting toggle title for switch Bluetooth LE Audio mode. [CHAR LIMIT=40] -->
|
||||
<string name="bluetooth_leaudio_mode">Bluetooth LE Audio mode</string>
|
||||
<!-- Setting toggle title for enabling Bluetooth LE Audio toggle in Device Details. [CHAR LIMIT=40] -->
|
||||
<string name="bluetooth_show_leaudio_device_details">Show LE audio toggle in Device Details</string>
|
||||
|
||||
@@ -9623,10 +9624,6 @@
|
||||
<string name="permit_voice_activation_apps">Allow voice activation</string>
|
||||
<!-- Description for a setting which controls whether an app can be voice activated [CHAR LIMIT=NONE] -->
|
||||
<string name ="allow_voice_activation_apps_description">Voice activation turns-on approved apps, hands-free, using voice command. Built-in adaptive sensing ensures data stays private only to you.\n\n<a href="">More about protected adaptive sensing</a></string>
|
||||
<!-- Label for a setting which controls whether an app can receive sandboxed detection training data [CHAR LIMIT=NONE] -->
|
||||
<string name = "permit_receive_sandboxed_detection_training_data">Improve voice activation</string>
|
||||
<!-- Description for a setting which controls whether an app can receive sandboxed detection training data [CHAR LIMIT=NONE] -->
|
||||
<string name= "receive_sandboxed_detection_training_data_description">This device uses private intelligence to improve the voice activation model. Apps can receive summarized updates that are aggregated across many users to maintain privacy while improving the model for everyone.\n\n<a href="">More about private intelligence</a></string>
|
||||
|
||||
<!-- Manage full screen intent permission title [CHAR LIMIT=40] -->
|
||||
<string name="full_screen_intent_title">Full screen notifications</string>
|
||||
|
||||
@@ -45,9 +45,15 @@
|
||||
|
||||
<com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
|
||||
android:key="audio_sharing_stream_name"
|
||||
android:summary="********"
|
||||
android:title="Stream name"
|
||||
android:title="Name"
|
||||
settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController" />
|
||||
|
||||
<com.android.settings.widget.ValidatedEditTextPreference
|
||||
android:key="audio_sharing_stream_password"
|
||||
android:summary="********"
|
||||
android:title="Password"
|
||||
settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPasswordPreferenceController" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="audio_sharing_stream_compatibility"
|
||||
android:title="Improve compatibility"
|
||||
|
||||
@@ -18,23 +18,31 @@
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:settings="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/audio_streams_title">
|
||||
android:title="Find an audio stream">
|
||||
|
||||
<Preference
|
||||
android:key="audio_streams_scan_qr_code"
|
||||
android:title="@string/bluetooth_find_broadcast_button_scan"
|
||||
android:icon="@drawable/ic_add_24dp"
|
||||
android:summary="@string/audio_streams_qr_code_summary"
|
||||
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController" />
|
||||
<com.android.settingslib.widget.TopIntroPreference
|
||||
android:key="audio_streams_top_intro"
|
||||
android:title="Listen to a device that's sharing audio or to a nearby Auracast broadcast"
|
||||
settings:searchable="false"/>
|
||||
|
||||
<Preference
|
||||
android:key="audio_streams_active_device"
|
||||
android:title="Listen with"
|
||||
android:title="Your audio device"
|
||||
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsActiveDeviceController" />
|
||||
|
||||
<com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference
|
||||
android:key="audio_streams_nearby_category"
|
||||
android:title="@string/audio_streams_pref_title"
|
||||
settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController" />
|
||||
android:title="Audio streams nearby"
|
||||
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>
|
||||
@@ -16,6 +16,7 @@
|
||||
-->
|
||||
|
||||
<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_height="match_parent">
|
||||
|
||||
@@ -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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/broadcast_dialog_margin"
|
||||
android:layout_marginBottom="25dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/dialog_icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="@dimen/broadcast_dialog_icon_margin_top"
|
||||
android:layout_marginBottom="@dimen/broadcast_dialog_title_img_margin_top"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_bt_audio_sharing"/>
|
||||
|
||||
<TextView
|
||||
style="@style/BroadcastDialogTitleStyle"
|
||||
android:id="@+id/dialog_title"
|
||||
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<TextView
|
||||
style="@style/BroadcastDialogBodyStyle"
|
||||
android:id="@+id/dialog_subtitle"
|
||||
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
style="@style/BroadcastDialogBodyStyle"
|
||||
android:id="@+id/dialog_subtitle_2"
|
||||
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/broadcast_dialog_margin"
|
||||
android:orientation="horizontal">
|
||||
android:layout_marginBottom="@dimen/broadcast_dialog_margin">
|
||||
<Button
|
||||
android:id="@+id/left_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_weight="1"
|
||||
android:visibility="invisible"/>
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
style="@style/BroadcastActionButton"/>
|
||||
<Button
|
||||
android:id="@+id/right_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginRight="16dp"
|
||||
android:visibility="invisible"/>
|
||||
</LinearLayout>
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
style="@style/BroadcastActionButton"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -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."/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -50,6 +50,13 @@
|
||||
android:layout_width="@dimen/qrcode_size"
|
||||
android:layout_height="@dimen/qrcode_size"
|
||||
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>
|
||||
|
||||
@@ -373,6 +373,13 @@
|
||||
android:title="@string/bluetooth_disable_leaudio"
|
||||
android:summary="@string/bluetooth_disable_leaudio_summary" />
|
||||
|
||||
<ListPreference
|
||||
android:key="bluetooth_leaudio_mode"
|
||||
android:title="@string/bluetooth_leaudio_mode"
|
||||
android:summary="@string/summary_placeholder"
|
||||
android:entries="@array/bluetooth_leaudio_mode"
|
||||
android:entryValues="@array/bluetooth_leaudio_mode_values"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="bluetooth_show_leaudio_device_details"
|
||||
android:title="@string/bluetooth_show_leaudio_device_details"/>
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
settings:keywords="@string/keywords_more_mobile_networks"
|
||||
settings:userRestriction="no_config_mobile_networks"
|
||||
settings:isPreferenceVisible="@bool/config_show_sim_info"
|
||||
settings:allowDividerAbove="true"
|
||||
settings:useAdminDisabledSummary="true"
|
||||
settings:searchable="@bool/config_show_sim_info"/>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
@@ -656,7 +657,11 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
|
||||
CombinedProviderInfo.createSettingsActivityIntent(
|
||||
mContext, packageName, settingsActivity, getUser());
|
||||
if (settingsIntent != null) {
|
||||
mContext.startActivity(settingsIntent);
|
||||
try {
|
||||
mContext.startActivity(settingsIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(TAG, "Failed to open settings activity", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.settings.applications.credentials;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.credentials.CredentialManager;
|
||||
@@ -26,6 +27,7 @@ import android.provider.Settings;
|
||||
import android.service.autofill.AutofillService;
|
||||
import android.service.autofill.AutofillServiceInfo;
|
||||
import android.view.autofill.AutofillManager;
|
||||
import android.util.Slog;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -132,7 +134,11 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon
|
||||
new PrimaryProviderPreference.Delegate() {
|
||||
public void onOpenButtonClicked() {
|
||||
if (settingsActivityIntent != null) {
|
||||
startActivity(settingsActivityIntent);
|
||||
try {
|
||||
startActivity(settingsActivityIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Slog.e(TAG, "Failed to open settings activity", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
@@ -30,6 +32,7 @@ import com.android.settings.widget.ValidatedEditTextPreference;
|
||||
|
||||
public class AudioSharingNamePreference extends ValidatedEditTextPreference {
|
||||
private static final String TAG = "AudioSharingNamePreference";
|
||||
private boolean mShowQrCodeIcon = false;
|
||||
|
||||
public AudioSharingNamePreference(
|
||||
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
@@ -58,17 +61,50 @@ public class AudioSharingNamePreference extends ValidatedEditTextPreference {
|
||||
setWidgetLayoutResource(R.layout.preference_widget_qrcode);
|
||||
}
|
||||
|
||||
void setShowQrCodeIcon(boolean show) {
|
||||
mShowQrCodeIcon = show;
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
final ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
|
||||
|
||||
ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
|
||||
View divider =
|
||||
holder.findViewById(
|
||||
com.android.settingslib.widget.preference.twotarget.R.id
|
||||
.two_target_divider);
|
||||
|
||||
if (shareButton != null && divider != null) {
|
||||
if (mShowQrCodeIcon) {
|
||||
configureVisibleStateForQrCodeIcon(shareButton, divider);
|
||||
} else {
|
||||
configureInvisibleStateForQrCodeIcon(shareButton, divider);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "onBindViewHolder() : shareButton or divider is null!");
|
||||
}
|
||||
}
|
||||
|
||||
private void configureVisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
|
||||
divider.setVisibility(View.VISIBLE);
|
||||
shareButton.setVisibility(View.VISIBLE);
|
||||
shareButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
|
||||
shareButton.setOnClickListener(
|
||||
unused ->
|
||||
new SubSettingLauncher(getContext())
|
||||
.setTitleText("Audio sharing QR code")
|
||||
.setDestination(AudioStreamsQrCodeFragment.class.getName())
|
||||
.setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
|
||||
.launch());
|
||||
shareButton.setOnClickListener(unused -> launchAudioSharingQrCodeFragment());
|
||||
}
|
||||
|
||||
private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
|
||||
divider.setVisibility(View.INVISIBLE);
|
||||
shareButton.setVisibility(View.INVISIBLE);
|
||||
shareButton.setOnClickListener(null);
|
||||
}
|
||||
|
||||
private void launchAudioSharingQrCodeFragment() {
|
||||
new SubSettingLauncher(getContext())
|
||||
.setTitleText("Audio sharing QR code")
|
||||
.setDestination(AudioStreamsQrCodeFragment.class.getName())
|
||||
.setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
|
||||
.launch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,25 +16,128 @@
|
||||
|
||||
package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
|
||||
|
||||
import android.bluetooth.BluetoothLeBroadcast;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settings.widget.ValidatedEditTextPreference;
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class AudioSharingNamePreferenceController extends BasePreferenceController
|
||||
implements ValidatedEditTextPreference.Validator, Preference.OnPreferenceChangeListener {
|
||||
implements ValidatedEditTextPreference.Validator,
|
||||
Preference.OnPreferenceChangeListener,
|
||||
DefaultLifecycleObserver {
|
||||
|
||||
private static final String TAG = "AudioSharingNamePreferenceController";
|
||||
|
||||
private static final boolean DEBUG = BluetoothUtils.D;
|
||||
private static final String PREF_KEY = "audio_sharing_stream_name";
|
||||
|
||||
private AudioSharingNameTextValidator mAudioSharingNameTextValidator;
|
||||
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
new BluetoothLeBroadcast.Callback() {
|
||||
@Override
|
||||
public void onBroadcastMetadataChanged(
|
||||
int broadcastId, BluetoothLeBroadcastMetadata metadata) {
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onBroadcastMetadataChanged() broadcastId : "
|
||||
+ broadcastId
|
||||
+ " metadata: "
|
||||
+ metadata);
|
||||
}
|
||||
updateQrCodeIcon(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBroadcastStartFailed(int reason) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastStarted(int reason, int broadcastId) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastStopFailed(int reason) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastStopped(int reason, int broadcastId) {
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onBroadcastStopped() reason : "
|
||||
+ reason
|
||||
+ " broadcastId: "
|
||||
+ broadcastId);
|
||||
}
|
||||
updateQrCodeIcon(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBroadcastUpdateFailed(int reason, int broadcastId) {
|
||||
Log.w(TAG, "onBroadcastUpdateFailed() reason : " + reason);
|
||||
// Do nothing if update failed.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBroadcastUpdated(int reason, int broadcastId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBroadcastUpdated() reason : " + reason);
|
||||
}
|
||||
updateBroadcastName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStarted(int reason, int broadcastId) {}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStopped(int reason, int broadcastId) {}
|
||||
};
|
||||
|
||||
@Nullable private final LocalBluetoothManager mLocalBtManager;
|
||||
@Nullable private final LocalBluetoothLeBroadcast mBroadcast;
|
||||
private final Executor mExecutor;
|
||||
private final AudioSharingNameTextValidator mAudioSharingNameTextValidator;
|
||||
@Nullable private AudioSharingNamePreference mPreference;
|
||||
|
||||
public AudioSharingNamePreferenceController(Context context, String preferenceKey) {
|
||||
super(context, preferenceKey);
|
||||
mLocalBtManager = Utils.getLocalBluetoothManager(context);
|
||||
mBroadcast =
|
||||
(mLocalBtManager != null)
|
||||
? mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile()
|
||||
: null;
|
||||
mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
if (mBroadcast != null) {
|
||||
mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
if (mBroadcast != null) {
|
||||
mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -42,6 +145,17 @@ public class AudioSharingNamePreferenceController extends BasePreferenceControll
|
||||
return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayPreference(PreferenceScreen screen) {
|
||||
super.displayPreference(screen);
|
||||
mPreference = screen.findPreference(getPreferenceKey());
|
||||
if (mPreference != null) {
|
||||
mPreference.setValidator(this);
|
||||
updateBroadcastName();
|
||||
updateQrCodeIcon(isBroadcasting(mLocalBtManager));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreferenceKey() {
|
||||
return PREF_KEY;
|
||||
@@ -49,10 +163,59 @@ public class AudioSharingNamePreferenceController extends BasePreferenceControll
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
// TODO: update broadcast when name is changed.
|
||||
if (mPreference != null
|
||||
&& mPreference.getSummary() != null
|
||||
&& ((String) newValue).contentEquals(mPreference.getSummary())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var unused =
|
||||
ThreadUtils.postOnBackgroundThread(
|
||||
() -> {
|
||||
if (mBroadcast != null) {
|
||||
mBroadcast.setProgramInfo((String) newValue);
|
||||
if (isBroadcasting(mLocalBtManager)) {
|
||||
// Update broadcast, UI update will be handled after callback
|
||||
mBroadcast.updateBroadcast();
|
||||
} else {
|
||||
// Directly update UI if no ongoing broadcast
|
||||
updateBroadcastName();
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateBroadcastName() {
|
||||
if (mPreference != null) {
|
||||
var unused =
|
||||
ThreadUtils.postOnBackgroundThread(
|
||||
() -> {
|
||||
if (mBroadcast != null) {
|
||||
String name = mBroadcast.getProgramInfo();
|
||||
ThreadUtils.postOnMainThread(
|
||||
() -> {
|
||||
if (mPreference != null) {
|
||||
mPreference.setText(name);
|
||||
mPreference.setSummary(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateQrCodeIcon(boolean show) {
|
||||
if (mPreference != null) {
|
||||
ThreadUtils.postOnMainThread(
|
||||
() -> {
|
||||
if (mPreference != null) {
|
||||
mPreference.setShowQrCodeIcon(show);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTextValid(String value) {
|
||||
return mAudioSharingNameTextValidator.isTextValid(value);
|
||||
|
||||
@@ -18,10 +18,27 @@ package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import com.android.settings.widget.ValidatedEditTextPreference;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Validator for Audio Sharing Name, which should be a UTF-8 encoded string containing a minimum of
|
||||
* 4 characters and a maximum of 32 human-readable characters.
|
||||
*/
|
||||
public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
|
||||
private static final int MIN_LENGTH = 4;
|
||||
private static final int MAX_LENGTH = 32;
|
||||
|
||||
@Override
|
||||
public boolean isTextValid(String value) {
|
||||
// TODO: Add validate rule if applicable.
|
||||
return true;
|
||||
if (value == null || value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
return isValidUTF8(value);
|
||||
}
|
||||
|
||||
private static boolean isValidUTF8(String value) {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
|
||||
return value.equals(reconstructedString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import android.bluetooth.BluetoothLeBroadcast;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settings.widget.ValidatedEditTextPreference;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class AudioSharingPasswordPreferenceController extends BasePreferenceController
|
||||
implements ValidatedEditTextPreference.Validator,
|
||||
Preference.OnPreferenceChangeListener,
|
||||
DefaultLifecycleObserver {
|
||||
private static final String PREF_KEY = "audio_sharing_stream_password";
|
||||
|
||||
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
new BluetoothLeBroadcast.Callback() {
|
||||
@Override
|
||||
public void onBroadcastMetadataChanged(
|
||||
int broadcastId, BluetoothLeBroadcastMetadata metadata) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastStartFailed(int reason) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastStarted(int reason, int broadcastId) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastStopFailed(int reason) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastStopped(int reason, int broadcastId) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
|
||||
|
||||
@Override
|
||||
public void onBroadcastUpdated(int reason, int broadcastId) {}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStarted(int reason, int broadcastId) {}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStopped(int reason, int broadcastId) {}
|
||||
};
|
||||
@Nullable private final LocalBluetoothLeBroadcast mBroadcast;
|
||||
private final Executor mExecutor;
|
||||
private final AudioSharingPasswordValidator mAudioSharingPasswordValidator;
|
||||
@Nullable private ValidatedEditTextPreference mPreference;
|
||||
|
||||
public AudioSharingPasswordPreferenceController(Context context, String preferenceKey) {
|
||||
super(context, preferenceKey);
|
||||
mBroadcast =
|
||||
Utils.getLocalBtManager(context).getProfileManager().getLeAudioBroadcastProfile();
|
||||
mAudioSharingPasswordValidator = new AudioSharingPasswordValidator();
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
if (mBroadcast != null) {
|
||||
mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
if (mBroadcast != null) {
|
||||
mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailabilityStatus() {
|
||||
return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayPreference(PreferenceScreen screen) {
|
||||
super.displayPreference(screen);
|
||||
mPreference = screen.findPreference(getPreferenceKey());
|
||||
if (mPreference != null) {
|
||||
mPreference.setValidator(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreferenceKey() {
|
||||
return PREF_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
// TODO(chelseahao): implement
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTextValid(String value) {
|
||||
return mAudioSharingPasswordValidator.isTextValid(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import com.android.settings.widget.ValidatedEditTextPreference;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Validator for Audio Sharing Password, which should be a UTF-8 string that has at least 4 octets
|
||||
* and should not exceed 16 octets.
|
||||
*/
|
||||
public class AudioSharingPasswordValidator implements ValidatedEditTextPreference.Validator {
|
||||
private static final int MIN_OCTETS = 4;
|
||||
private static final int MAX_OCTETS = 16;
|
||||
|
||||
@Override
|
||||
public boolean isTextValid(String value) {
|
||||
if (value == null
|
||||
|| getOctetsCount(value) < MIN_OCTETS
|
||||
|| getOctetsCount(value) > MAX_OCTETS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isValidUTF8(value);
|
||||
}
|
||||
|
||||
private static int getOctetsCount(String value) {
|
||||
return value.getBytes(StandardCharsets.UTF_8).length;
|
||||
}
|
||||
|
||||
private static boolean isValidUTF8(String value) {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
|
||||
return value.equals(reconstructedString);
|
||||
}
|
||||
}
|
||||
@@ -336,7 +336,7 @@ public class AudioSharingUtils {
|
||||
}
|
||||
|
||||
/** Returns if the broadcast is on-going. */
|
||||
public static boolean isBroadcasting(LocalBluetoothManager manager) {
|
||||
public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
|
||||
if (manager == null) return false;
|
||||
LocalBluetoothLeBroadcast broadcast =
|
||||
manager.getProfileManager().getLeAudioBroadcastProfile();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<AudioStreamPreference> mComparator =
|
||||
Comparator.<AudioStreamPreference, Boolean>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<Integer, AudioStreamPreference> 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;
|
||||
}
|
||||
|
||||
@@ -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<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() {
|
||||
setEmptyTextRes(R.string.audio_streams_empty);
|
||||
}
|
||||
|
||||
@@ -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<Bitmap> getQrCodeBitmap() {
|
||||
String broadcastMetadata = getBroadcastMetadataQrCode();
|
||||
if (broadcastMetadata.isEmpty()) {
|
||||
private Optional<Bitmap> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
() -> {
|
||||
|
||||
@@ -229,6 +229,7 @@ public class QrCodeScanModeFragment extends InstrumentedFragment
|
||||
}
|
||||
|
||||
mErrorMessage.setVisibility(View.INVISIBLE);
|
||||
mTextureView.setVisibility(View.INVISIBLE);
|
||||
|
||||
triggerVibrationForQrCodeRecognition(getContext());
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 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.development;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.os.SystemProperties;
|
||||
import android.sysprop.BluetoothProperties;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.PreferenceControllerMixin;
|
||||
import com.android.settingslib.development.DeveloperOptionsPreferenceController;
|
||||
|
||||
|
||||
/**
|
||||
* Preference controller to control Bluetooth LE audio mode
|
||||
*/
|
||||
public class BluetoothLeAudioModePreferenceController
|
||||
extends DeveloperOptionsPreferenceController
|
||||
implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
|
||||
|
||||
private static final String PREFERENCE_KEY = "bluetooth_leaudio_mode";
|
||||
|
||||
static final String LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY =
|
||||
"persist.bluetooth.leaudio_dynamic_switcher.mode";
|
||||
|
||||
@Nullable private final DevelopmentSettingsDashboardFragment mFragment;
|
||||
|
||||
private final String[] mListValues;
|
||||
private final String[] mListSummaries;
|
||||
@VisibleForTesting
|
||||
@Nullable String mNewMode;
|
||||
@VisibleForTesting
|
||||
BluetoothAdapter mBluetoothAdapter;
|
||||
|
||||
boolean mChanged = false;
|
||||
|
||||
public BluetoothLeAudioModePreferenceController(@NonNull Context context,
|
||||
@Nullable DevelopmentSettingsDashboardFragment fragment) {
|
||||
super(context);
|
||||
mFragment = fragment;
|
||||
mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
|
||||
|
||||
mListValues = context.getResources().getStringArray(R.array.bluetooth_leaudio_mode_values);
|
||||
mListSummaries = context.getResources().getStringArray(R.array.bluetooth_leaudio_mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull public String getPreferenceKey() {
|
||||
return PREFERENCE_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
|
||||
if (mFragment == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BluetoothRebootDialog.show(mFragment);
|
||||
mChanged = true;
|
||||
mNewMode = newValue.toString();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(@NonNull Preference preference) {
|
||||
if (mBluetoothAdapter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mBluetoothAdapter.isLeAudioBroadcastSourceSupported()
|
||||
== BluetoothStatusCodes.FEATURE_SUPPORTED) {
|
||||
SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "broadcast");
|
||||
} else if (mBluetoothAdapter.isLeAudioSupported()
|
||||
== BluetoothStatusCodes.FEATURE_SUPPORTED) {
|
||||
SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "unicast");
|
||||
} else {
|
||||
SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "disabled");
|
||||
}
|
||||
|
||||
final String currentValue = SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY);
|
||||
int index = 0;
|
||||
for (int i = 0; i < mListValues.length; i++) {
|
||||
if (TextUtils.equals(currentValue, mListValues[i])) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final ListPreference listPreference = (ListPreference) preference;
|
||||
listPreference.setValue(mListValues[index]);
|
||||
listPreference.setSummary(mListSummaries[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the RebootDialog confirm is clicked.
|
||||
*/
|
||||
public void onRebootDialogConfirmed() {
|
||||
if (!mChanged) {
|
||||
return;
|
||||
}
|
||||
SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mNewMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the RebootDialog cancel is clicked.
|
||||
*/
|
||||
public void onRebootDialogCanceled() {
|
||||
mChanged = false;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.os.SystemProperties;
|
||||
import android.sysprop.BluetoothProperties;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.Preference;
|
||||
@@ -64,6 +65,12 @@ public class BluetoothLeAudioPreferenceController
|
||||
return PREFERENCE_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return BluetoothProperties.isProfileBapUnicastClientEnabled().orElse(false)
|
||||
&& !BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
BluetoothRebootDialog.show(mFragment);
|
||||
|
||||
@@ -454,6 +454,11 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
|
||||
getDevelopmentOptionsController(
|
||||
BluetoothLeAudioPreferenceController.class);
|
||||
leAudioFeatureController.onRebootDialogConfirmed();
|
||||
|
||||
final BluetoothLeAudioModePreferenceController leAudioModeController =
|
||||
getDevelopmentOptionsController(
|
||||
BluetoothLeAudioModePreferenceController.class);
|
||||
leAudioModeController.onRebootDialogConfirmed();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -471,6 +476,11 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
|
||||
getDevelopmentOptionsController(
|
||||
BluetoothLeAudioPreferenceController.class);
|
||||
leAudioFeatureController.onRebootDialogCanceled();
|
||||
|
||||
final BluetoothLeAudioModePreferenceController leAudioModeController =
|
||||
getDevelopmentOptionsController(
|
||||
BluetoothLeAudioModePreferenceController.class);
|
||||
leAudioModeController.onRebootDialogCanceled();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -670,6 +680,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
|
||||
controllers.add(new BluetoothAvrcpVersionPreferenceController(context));
|
||||
controllers.add(new BluetoothMapVersionPreferenceController(context));
|
||||
controllers.add(new BluetoothLeAudioPreferenceController(context, fragment));
|
||||
controllers.add(new BluetoothLeAudioModePreferenceController(context, fragment));
|
||||
controllers.add(new BluetoothLeAudioDeviceDetailsPreferenceController(context));
|
||||
controllers.add(new BluetoothLeAudioAllowListPreferenceController(context, fragment));
|
||||
controllers.add(new BluetoothA2dpHwOffloadPreferenceController(context, fragment));
|
||||
|
||||
@@ -41,6 +41,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.internal.accessibility.common.ShortcutConstants;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.accessibility.AccessibilityGestureNavigationTutorial;
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
@@ -353,7 +354,7 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment i
|
||||
private boolean isAnyServiceSupportAccessibilityButton() {
|
||||
final AccessibilityManager ams = getContext().getSystemService(AccessibilityManager.class);
|
||||
final List<String> targets = ams.getAccessibilityShortcutTargets(
|
||||
AccessibilityManager.ACCESSIBILITY_BUTTON);
|
||||
ShortcutConstants.UserShortcutType.SOFTWARE);
|
||||
return !targets.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,11 @@ import androidx.preference.Preference
|
||||
import com.android.settings.R
|
||||
import com.android.settings.SettingsPreferenceFragment
|
||||
import com.android.settings.dashboard.DashboardFragment
|
||||
import com.android.settings.flags.Flags
|
||||
import com.android.settings.network.telephony.MobileNetworkUtils
|
||||
import com.android.settings.search.BaseSearchIndexProvider
|
||||
import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
|
||||
import com.android.settings.spa.network.NetworkCellularGroupProvider
|
||||
import com.android.settingslib.search.SearchIndexable
|
||||
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
|
||||
import com.android.settingslib.spaprivileged.framework.common.userManager
|
||||
@@ -40,6 +43,15 @@ class MobileNetworkListFragment : DashboardFragment() {
|
||||
collectAirplaneModeAndFinishIfOn()
|
||||
}
|
||||
|
||||
override fun onCreate(icicle: Bundle?) {
|
||||
super.onCreate(icicle)
|
||||
|
||||
if (Flags.isDualSimOnboardingEnabled()) {
|
||||
context?.startSpaActivity(NetworkCellularGroupProvider.name);
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Disable the animation of the preference list
|
||||
|
||||
@@ -32,7 +32,19 @@ class SubscriptionInfoListViewModel(application: Application) : AndroidViewModel
|
||||
application.getSystemService(SubscriptionManager::class.java)!!
|
||||
private val scope = viewModelScope + Dispatchers.Default
|
||||
|
||||
/**
|
||||
* Getting the active Subscription list
|
||||
*/
|
||||
//ToDo: renaming the function name
|
||||
val subscriptionInfoListFlow = application.subscriptionsChangedFlow().map {
|
||||
SubscriptionUtil.getActiveSubscriptions(subscriptionManager)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList())
|
||||
|
||||
/**
|
||||
* Getting the Selectable SubscriptionInfo List from the SubscriptionManager's
|
||||
* getAvailableSubscriptionInfoList
|
||||
*/
|
||||
val selectableSubscriptionInfoListFlow = application.subscriptionsChangedFlow().map {
|
||||
SubscriptionUtil.getSelectableSubscriptionInfoList(application)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList())
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import com.android.settings.spa.development.UsageStatsPageProvider
|
||||
import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider
|
||||
import com.android.settings.spa.home.HomePageProvider
|
||||
import com.android.settings.spa.network.NetworkAndInternetPageProvider
|
||||
import com.android.settings.spa.network.NetworkCellularGroupProvider
|
||||
import com.android.settings.spa.network.SimOnboardingPageProvider
|
||||
import com.android.settings.spa.notification.AppListNotificationsPageProvider
|
||||
import com.android.settings.spa.notification.NotificationMainPageProvider
|
||||
@@ -118,6 +119,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) {
|
||||
ApnEditPageProvider,
|
||||
SimOnboardingPageProvider,
|
||||
BatteryOptimizationModeAppListPageProvider,
|
||||
NetworkCellularGroupProvider,
|
||||
)
|
||||
|
||||
override val logger = if (FeatureFlagUtils.isEnabled(
|
||||
|
||||
@@ -20,24 +20,12 @@ import android.Manifest
|
||||
import android.app.AppOpsManager
|
||||
import android.app.settings.SettingsEnums
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.android.settings.R
|
||||
import com.android.settings.overlay.FeatureFactory
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreference
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
import com.android.settingslib.spaprivileged.model.app.AppOpsController
|
||||
import com.android.settingslib.spaprivileged.model.app.PackageManagers.hasGrantPermission
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
|
||||
import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* This class builds an App List under voice activation apps and the individual page which
|
||||
@@ -56,97 +44,15 @@ class VoiceActivationAppsListModel(context: Context) : AppOpPermissionListModel(
|
||||
override val appOp = AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
|
||||
override val permission = Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO
|
||||
override val setModeByUid = true
|
||||
private var receiveDetectionTrainingDataOpController:AppOpsController? = null
|
||||
|
||||
override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
|
||||
super.setAllowed(record, newAllowed)
|
||||
if (!newAllowed && receiveDetectionTrainingDataOpController != null) {
|
||||
receiveDetectionTrainingDataOpController!!.setAllowed(false)
|
||||
}
|
||||
logPermissionChange(newAllowed)
|
||||
}
|
||||
|
||||
override fun isChangeable(record: AppOpPermissionRecord): Boolean =
|
||||
super.isChangeable(record) && record.app.hasGrantPermission(permission)
|
||||
|
||||
@Composable
|
||||
override fun InfoPageAdditionalContent(
|
||||
record: AppOpPermissionRecord,
|
||||
isAllowed: () -> Boolean?,
|
||||
) {
|
||||
SwitchPreference(createReceiveDetectionTrainingDataOpSwitchModel(record, isAllowed))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun createReceiveDetectionTrainingDataOpSwitchModel(
|
||||
record: AppOpPermissionRecord,
|
||||
isReceiveSandBoxTriggerAudioOpAllowed: () -> Boolean?
|
||||
): ReceiveDetectionTrainingDataOpSwitchModel {
|
||||
val context = LocalContext.current
|
||||
receiveDetectionTrainingDataOpController = remember {
|
||||
AppOpsController(
|
||||
context = context,
|
||||
app = record.app,
|
||||
op = AppOpsManager.OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
|
||||
)
|
||||
}
|
||||
val isReceiveDetectionTrainingDataOpAllowed = isReceiveDetectionTrainingDataOpAllowed(record, receiveDetectionTrainingDataOpController!!)
|
||||
|
||||
return remember(record) {
|
||||
ReceiveDetectionTrainingDataOpSwitchModel(
|
||||
context,
|
||||
record,
|
||||
isReceiveSandBoxTriggerAudioOpAllowed,
|
||||
receiveDetectionTrainingDataOpController!!,
|
||||
isReceiveDetectionTrainingDataOpAllowed,
|
||||
)
|
||||
}.also { model -> LaunchedEffect(model, Dispatchers.Default) { model.initState() } }
|
||||
}
|
||||
|
||||
private inner class ReceiveDetectionTrainingDataOpSwitchModel(
|
||||
context: Context,
|
||||
private val record: AppOpPermissionRecord,
|
||||
isReceiveSandBoxTriggerAudioOpAllowed: () -> Boolean?,
|
||||
receiveDetectionTrainingDataOpController: AppOpsController,
|
||||
isReceiveDetectionTrainingDataOpAllowed: () -> Boolean?,
|
||||
) : SwitchPreferenceModel {
|
||||
private var appChangeable by mutableStateOf(true)
|
||||
|
||||
override val title: String = context.getString(R.string.permit_receive_sandboxed_detection_training_data)
|
||||
override val summary: () -> String = { context.getString(R.string.receive_sandboxed_detection_training_data_description) }
|
||||
override val checked = { isReceiveDetectionTrainingDataOpAllowed() == true && isReceiveSandBoxTriggerAudioOpAllowed() == true }
|
||||
override val changeable = { appChangeable && isReceiveSandBoxTriggerAudioOpAllowed() == true }
|
||||
|
||||
fun initState() {
|
||||
appChangeable = isChangeable(record)
|
||||
}
|
||||
|
||||
override val onCheckedChange: (Boolean) -> Unit = { newChecked ->
|
||||
receiveDetectionTrainingDataOpController.setAllowed(newChecked)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun isReceiveDetectionTrainingDataOpAllowed(
|
||||
record: AppOpPermissionRecord,
|
||||
controller: AppOpsController
|
||||
): () -> Boolean? {
|
||||
if (record.hasRequestBroaderPermission) {
|
||||
// Broader permission trumps the specific permission.
|
||||
return { true }
|
||||
}
|
||||
|
||||
val mode = controller.mode.observeAsState()
|
||||
return {
|
||||
when (mode.value) {
|
||||
null -> null
|
||||
AppOpsManager.MODE_ALLOWED -> true
|
||||
AppOpsManager.MODE_DEFAULT -> record.app.hasGrantPermission(
|
||||
Manifest.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logPermissionChange(newAllowed: Boolean) {
|
||||
val category = when {
|
||||
newAllowed -> SettingsEnums.APP_SPECIAL_PERMISSION_RECEIVE_SANDBOX_TRIGGER_AUDIO_ALLOW
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
* 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.spa.network
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.UserManager
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.TelephonyManager
|
||||
import android.telephony.euicc.EuiccManager
|
||||
import android.util.Log
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Message
|
||||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material.icons.outlined.DataUsage
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableIntState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import com.android.settings.R
|
||||
import com.android.settings.network.SubscriptionInfoListViewModel
|
||||
import com.android.settings.network.SubscriptionUtil
|
||||
import com.android.settings.network.telephony.MobileNetworkUtils
|
||||
import com.android.settings.wifi.WifiPickerTrackerHelper
|
||||
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
|
||||
import com.android.settingslib.spa.framework.common.SettingsPageProvider
|
||||
import com.android.settingslib.spa.framework.common.createSettingsPage
|
||||
import com.android.settingslib.spa.framework.compose.navigator
|
||||
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
|
||||
import com.android.settingslib.spa.widget.preference.ListPreferenceOption
|
||||
import com.android.settingslib.spa.widget.preference.Preference
|
||||
import com.android.settingslib.spa.widget.preference.PreferenceModel
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreference
|
||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||
import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
|
||||
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
|
||||
import com.android.settingslib.spa.widget.ui.Category
|
||||
import com.android.settingslib.spa.widget.ui.SettingsIcon
|
||||
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
|
||||
|
||||
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
|
||||
import com.android.settingslib.spaprivileged.template.preference.RestrictedPreference
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Showing the sim onboarding which is the process flow of sim switching on.
|
||||
*/
|
||||
object NetworkCellularGroupProvider : SettingsPageProvider {
|
||||
override val name = "NetworkCellularGroupProvider"
|
||||
|
||||
private lateinit var subscriptionViewModel: SubscriptionInfoListViewModel
|
||||
private val owner = createSettingsPage()
|
||||
|
||||
var selectableSubscriptionInfoList: List<SubscriptionInfo> = listOf()
|
||||
var defaultVoiceSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
var defaultSmsSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
var defaultDataSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
var nonDds: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
|
||||
fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
|
||||
.setUiLayoutFn {
|
||||
// never using
|
||||
Preference(object : PreferenceModel {
|
||||
override val title = name
|
||||
override val onClick = navigator(name)
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Page(arguments: Bundle?) {
|
||||
val context = LocalContext.current
|
||||
var selectableSubscriptionInfoListRemember = remember {
|
||||
mutableListOf<SubscriptionInfo>().toMutableStateList()
|
||||
}
|
||||
var callsSelectedId = rememberSaveable {
|
||||
mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
|
||||
}
|
||||
var textsSelectedId = rememberSaveable {
|
||||
mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
|
||||
}
|
||||
var mobileDataSelectedId = rememberSaveable {
|
||||
mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
|
||||
}
|
||||
var nonDdsRemember = rememberSaveable {
|
||||
mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
|
||||
}
|
||||
|
||||
subscriptionViewModel = SubscriptionInfoListViewModel(
|
||||
context.applicationContext as Application)
|
||||
|
||||
allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow)
|
||||
.collectLatestWithLifecycle(LocalLifecycleOwner.current) {
|
||||
selectableSubscriptionInfoListRemember.clear()
|
||||
selectableSubscriptionInfoListRemember.addAll(selectableSubscriptionInfoList)
|
||||
callsSelectedId.intValue = defaultVoiceSubId
|
||||
textsSelectedId.intValue = defaultSmsSubId
|
||||
mobileDataSelectedId.intValue = defaultDataSubId
|
||||
nonDdsRemember.intValue = nonDds
|
||||
}
|
||||
|
||||
PageImpl(selectableSubscriptionInfoListRemember,
|
||||
callsSelectedId,
|
||||
textsSelectedId,
|
||||
mobileDataSelectedId,
|
||||
nonDdsRemember)
|
||||
}
|
||||
|
||||
private fun allOfFlows(context: Context,
|
||||
selectableSubscriptionInfoListFlow: Flow<List<SubscriptionInfo>>) =
|
||||
combine(
|
||||
selectableSubscriptionInfoListFlow,
|
||||
context.defaultVoiceSubscriptionFlow(),
|
||||
context.defaultSmsSubscriptionFlow(),
|
||||
context.defaultDefaultDataSubscriptionFlow(),
|
||||
NetworkCellularGroupProvider::refreshUiStates,
|
||||
).flowOn(Dispatchers.Default)
|
||||
|
||||
fun refreshUiStates(
|
||||
inputSelectableSubscriptionInfoList: List<SubscriptionInfo>,
|
||||
inputDefaultVoiceSubId: Int,
|
||||
inputDefaultSmsSubId: Int,
|
||||
inputDefaultDateSubId: Int
|
||||
): Unit {
|
||||
selectableSubscriptionInfoList = inputSelectableSubscriptionInfoList
|
||||
defaultVoiceSubId = inputDefaultVoiceSubId
|
||||
defaultSmsSubId = inputDefaultSmsSubId
|
||||
defaultDataSubId = inputDefaultDateSubId
|
||||
nonDds = if (defaultDataSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
|
||||
SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
} else {
|
||||
selectableSubscriptionInfoList
|
||||
.filter { info ->
|
||||
(info.simSlotIndex != -1) && (info.subscriptionId != defaultDataSubId)
|
||||
}
|
||||
.map { it.subscriptionId }
|
||||
.firstOrNull() ?: SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PageImpl(selectableSubscriptionInfoList: List<SubscriptionInfo>,
|
||||
defaultVoiceSubId: MutableIntState,
|
||||
defaultSmsSubId: MutableIntState,
|
||||
defaultDataSubId: MutableIntState,
|
||||
nonDds: MutableIntState) {
|
||||
val context = LocalContext.current
|
||||
var activeSubscriptionInfoList: List<SubscriptionInfo> =
|
||||
selectableSubscriptionInfoList.filter { subscriptionInfo ->
|
||||
subscriptionInfo.simSlotIndex != -1
|
||||
}
|
||||
var subscriptionManager = context.getSystemService(SubscriptionManager::class.java)
|
||||
|
||||
val stringSims = stringResource(R.string.provider_network_settings_title)
|
||||
RegularScaffold(title = stringSims) {
|
||||
SimsSectionImpl(
|
||||
context,
|
||||
subscriptionManager,
|
||||
selectableSubscriptionInfoList
|
||||
)
|
||||
PrimarySimSectionImpl(
|
||||
subscriptionManager,
|
||||
activeSubscriptionInfoList,
|
||||
defaultVoiceSubId,
|
||||
defaultSmsSubId,
|
||||
defaultDataSubId,
|
||||
nonDds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimsSectionImpl(
|
||||
context: Context,
|
||||
subscriptionManager: SubscriptionManager?,
|
||||
subscriptionInfoList: List<SubscriptionInfo>
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
for (subInfo in subscriptionInfoList) {
|
||||
val checked = rememberSaveable() {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
//TODO: Add the Restricted TwoTargetSwitchPreference in SPA
|
||||
TwoTargetSwitchPreference(remember {
|
||||
object : SwitchPreferenceModel {
|
||||
override val title = subInfo.displayName.toString()
|
||||
override val summary = { subInfo.number }
|
||||
override val checked = {
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
checked.value = subscriptionManager?.isSubscriptionEnabled(
|
||||
subInfo.subscriptionId)?:false
|
||||
}
|
||||
}
|
||||
checked.value
|
||||
}
|
||||
override val onCheckedChange = { newChecked: Boolean ->
|
||||
startToggleSubscriptionDialog(context, subInfo, newChecked)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
startMobileNetworkSettings(context, subInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// + add sim
|
||||
if (showEuiccSettings(context)) {
|
||||
RestrictedPreference(
|
||||
model = object : PreferenceModel {
|
||||
override val title = stringResource(id = R.string.mobile_network_list_add_more)
|
||||
override val icon = @Composable { SettingsIcon(Icons.Outlined.Add) }
|
||||
override val onClick = {
|
||||
startAddSimFlow(context)
|
||||
}
|
||||
},
|
||||
restrictions = Restrictions(keys =
|
||||
listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrimarySimSectionImpl(
|
||||
subscriptionManager: SubscriptionManager?,
|
||||
activeSubscriptionInfoList: List<SubscriptionInfo>,
|
||||
callsSelectedId: MutableIntState,
|
||||
textsSelectedId: MutableIntState,
|
||||
mobileDataSelectedId: MutableIntState,
|
||||
nonDds: MutableIntState
|
||||
) {
|
||||
var state = rememberSaveable { mutableStateOf(false) }
|
||||
var callsAndSmsList = remember {
|
||||
mutableListOf(ListPreferenceOption(id = -1, text = "Loading"))
|
||||
}
|
||||
var dataList = remember {
|
||||
mutableListOf(ListPreferenceOption(id = -1, text = "Loading"))
|
||||
}
|
||||
|
||||
if (activeSubscriptionInfoList.size >= 2) {
|
||||
state.value = true
|
||||
callsAndSmsList.clear()
|
||||
dataList.clear()
|
||||
for (info in activeSubscriptionInfoList) {
|
||||
var item = ListPreferenceOption(
|
||||
id = info.subscriptionId,
|
||||
text = "${info.displayName}"
|
||||
)
|
||||
callsAndSmsList.add(item)
|
||||
dataList.add(item)
|
||||
}
|
||||
callsAndSmsList.add(ListPreferenceOption(
|
||||
id = SubscriptionManager.INVALID_SUBSCRIPTION_ID,
|
||||
text = stringResource(id = R.string.sim_calls_ask_first_prefs_title)
|
||||
))
|
||||
} else {
|
||||
// hide the primary sim
|
||||
state.value = false
|
||||
Log.d("NetworkCellularGroupProvider", "Hide primary sim")
|
||||
}
|
||||
|
||||
if (state.value) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var context = LocalContext.current
|
||||
val telephonyManagerForNonDds: TelephonyManager? =
|
||||
context.getSystemService(TelephonyManager::class.java)
|
||||
?.createForSubscriptionId(nonDds.intValue)
|
||||
val automaticDataChecked = rememberSaveable() {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
Category(title = stringResource(id = R.string.primary_sim_title)) {
|
||||
createPrimarySimListPreference(
|
||||
stringResource(id = R.string.primary_sim_calls_title),
|
||||
callsAndSmsList,
|
||||
callsSelectedId,
|
||||
ImageVector.vectorResource(R.drawable.ic_phone),
|
||||
) {
|
||||
callsSelectedId.intValue = it
|
||||
coroutineScope.launch {
|
||||
setDefaultVoice(subscriptionManager, it)
|
||||
}
|
||||
}
|
||||
createPrimarySimListPreference(
|
||||
stringResource(id = R.string.primary_sim_texts_title),
|
||||
callsAndSmsList,
|
||||
textsSelectedId,
|
||||
Icons.AutoMirrored.Outlined.Message,
|
||||
) {
|
||||
textsSelectedId.intValue = it
|
||||
coroutineScope.launch {
|
||||
setDefaultSms(subscriptionManager, it)
|
||||
}
|
||||
}
|
||||
createPrimarySimListPreference(
|
||||
stringResource(id = R.string.mobile_data_settings_title),
|
||||
dataList,
|
||||
mobileDataSelectedId,
|
||||
Icons.Outlined.DataUsage,
|
||||
) {
|
||||
mobileDataSelectedId.intValue = it
|
||||
coroutineScope.launch {
|
||||
// TODO: to fix the WifiPickerTracker crash when create
|
||||
// the wifiPickerTrackerHelper
|
||||
setDefaultData(context,
|
||||
subscriptionManager,
|
||||
null/*wifiPickerTrackerHelper*/,
|
||||
it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title)
|
||||
val autoDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg)
|
||||
SwitchPreference(remember {
|
||||
object : SwitchPreferenceModel {
|
||||
override val title = autoDataTitle
|
||||
override val summary = { autoDataSummary }
|
||||
override val changeable: () -> Boolean = {
|
||||
nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID
|
||||
}
|
||||
override val checked = {
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
automaticDataChecked.value = telephonyManagerForNonDds != null
|
||||
&& telephonyManagerForNonDds.isMobileDataPolicyEnabled(
|
||||
TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH)
|
||||
}
|
||||
}
|
||||
automaticDataChecked.value
|
||||
}
|
||||
override val onCheckedChange: ((Boolean) -> Unit)? =
|
||||
{ newChecked: Boolean ->
|
||||
coroutineScope.launch {
|
||||
setAutomaticData(telephonyManagerForNonDds, newChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.defaultVoiceSubscriptionFlow(): Flow<Int> =
|
||||
merge(
|
||||
flowOf(null), // kick an initial value
|
||||
broadcastReceiverFlow(
|
||||
IntentFilter(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)
|
||||
),
|
||||
).map { SubscriptionManager.getDefaultVoiceSubscriptionId() }
|
||||
.conflate().flowOn(Dispatchers.Default)
|
||||
|
||||
private fun Context.defaultSmsSubscriptionFlow(): Flow<Int> =
|
||||
merge(
|
||||
flowOf(null), // kick an initial value
|
||||
broadcastReceiverFlow(
|
||||
IntentFilter(SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED)
|
||||
),
|
||||
).map { SubscriptionManager.getDefaultSmsSubscriptionId() }
|
||||
.conflate().flowOn(Dispatchers.Default)
|
||||
|
||||
private fun Context.defaultDefaultDataSubscriptionFlow(): Flow<Int> =
|
||||
merge(
|
||||
flowOf(null), // kick an initial value
|
||||
broadcastReceiverFlow(
|
||||
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
|
||||
),
|
||||
).map { SubscriptionManager.getDefaultDataSubscriptionId() }
|
||||
.conflate().flowOn(Dispatchers.Default)
|
||||
|
||||
private fun startToggleSubscriptionDialog(
|
||||
context: Context,
|
||||
subInfo: SubscriptionInfo,
|
||||
newStatus: Boolean
|
||||
) {
|
||||
SubscriptionUtil.startToggleSubscriptionDialogActivity(
|
||||
context,
|
||||
subInfo.subscriptionId,
|
||||
newStatus
|
||||
)
|
||||
}
|
||||
|
||||
private fun startMobileNetworkSettings(context: Context, subInfo: SubscriptionInfo) {
|
||||
MobileNetworkUtils.launchMobileNetworkSettings(context, subInfo)
|
||||
}
|
||||
|
||||
private fun startAddSimFlow(context: Context) {
|
||||
val intent = Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION)
|
||||
intent.putExtra(EuiccManager.EXTRA_FORCE_PROVISION, true)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun showEuiccSettings(context: Context): Boolean {
|
||||
return MobileNetworkUtils.showEuiccSettings(context)
|
||||
}
|
||||
|
||||
private suspend fun setDefaultVoice(
|
||||
subscriptionManager: SubscriptionManager?,
|
||||
subId: Int): Unit = withContext(Dispatchers.Default) {
|
||||
subscriptionManager?.setDefaultVoiceSubscriptionId(subId)
|
||||
}
|
||||
|
||||
private suspend fun setDefaultSms(
|
||||
subscriptionManager: SubscriptionManager?,
|
||||
subId: Int): Unit = withContext(Dispatchers.Default) {
|
||||
subscriptionManager?.setDefaultSmsSubId(subId)
|
||||
}
|
||||
|
||||
private suspend fun setDefaultData(context: Context,
|
||||
subscriptionManager: SubscriptionManager?,
|
||||
wifiPickerTrackerHelper: WifiPickerTrackerHelper?,
|
||||
subId: Int): Unit = withContext(Dispatchers.Default) {
|
||||
subscriptionManager?.setDefaultDataSubId(subId)
|
||||
MobileNetworkUtils.setMobileDataEnabled(
|
||||
context,
|
||||
subId,
|
||||
true /* enabled */,
|
||||
true /* disableOtherSubscriptions */)
|
||||
if (wifiPickerTrackerHelper != null
|
||||
&& !wifiPickerTrackerHelper.isCarrierNetworkProvisionEnabled(subId)) {
|
||||
wifiPickerTrackerHelper.setCarrierNetworkEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setAutomaticData(telephonyManager: TelephonyManager?, newState: Boolean): Unit =
|
||||
withContext(Dispatchers.Default) {
|
||||
telephonyManager?.setMobileDataPolicyEnabled(
|
||||
TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
|
||||
newState)
|
||||
//TODO: setup backup calling
|
||||
}
|
||||
@@ -108,21 +108,22 @@ private fun primarySimBody(onboardingService: SimOnboardingService) {
|
||||
list,
|
||||
callsSelectedId,
|
||||
ImageVector.vectorResource(R.drawable.ic_phone),
|
||||
true
|
||||
onIdSelected = { callsSelectedId.intValue = it }
|
||||
)
|
||||
createPrimarySimListPreference(
|
||||
stringResource(id = R.string.primary_sim_texts_title),
|
||||
list,
|
||||
textsSelectedId,
|
||||
Icons.AutoMirrored.Outlined.Message,
|
||||
true
|
||||
onIdSelected = { textsSelectedId.intValue = it }
|
||||
)
|
||||
|
||||
createPrimarySimListPreference(
|
||||
stringResource(id = R.string.mobile_data_settings_title),
|
||||
list,
|
||||
mobileDataSelectedId,
|
||||
stringResource(id = R.string.mobile_data_settings_title),
|
||||
list,
|
||||
mobileDataSelectedId,
|
||||
Icons.Outlined.DataUsage,
|
||||
true
|
||||
onIdSelected = { mobileDataSelectedId.intValue = it }
|
||||
)
|
||||
|
||||
val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title)
|
||||
@@ -140,17 +141,18 @@ private fun primarySimBody(onboardingService: SimOnboardingService) {
|
||||
|
||||
@Composable
|
||||
fun createPrimarySimListPreference(
|
||||
title: String,
|
||||
list: List<ListPreferenceOption>,
|
||||
selectedId: MutableIntState,
|
||||
icon: ImageVector,
|
||||
enable: Boolean
|
||||
title: String,
|
||||
list: List<ListPreferenceOption>,
|
||||
selectedId: MutableIntState,
|
||||
icon: ImageVector,
|
||||
enable: Boolean = true,
|
||||
onIdSelected: (id: Int) -> Unit
|
||||
) = ListPreference(remember {
|
||||
object : ListPreferenceModel {
|
||||
override val title = title
|
||||
override val options = list
|
||||
override val selectedId = selectedId
|
||||
override val onIdSelected: (id: Int) -> Unit = { selectedId.intValue = it }
|
||||
override val onIdSelected = onIdSelected
|
||||
override val icon = @Composable {
|
||||
SettingsIcon(icon)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.settings.wifi;
|
||||
|
||||
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||
import static android.os.UserManager.DISALLOW_ADD_WIFI_CONFIG;
|
||||
import static android.os.UserManager.DISALLOW_CONFIG_WIFI;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
@@ -122,7 +123,7 @@ public class WifiDialogActivity extends ObservableActivity implements WifiDialog
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
if (!isConfigWifiAllowed()) {
|
||||
if (!isConfigWifiAllowed() || !isAddWifiConfigAllowed()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
@@ -393,6 +394,16 @@ public class WifiDialogActivity extends ObservableActivity implements WifiDialog
|
||||
return isConfigWifiAllowed;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean isAddWifiConfigAllowed() {
|
||||
UserManager userManager = getSystemService(UserManager.class);
|
||||
if (userManager != null && userManager.hasUserRestriction(DISALLOW_ADD_WIFI_CONFIG)) {
|
||||
Log.e(TAG, "The user is not allowed to add Wi-Fi configuration.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasWifiManager() {
|
||||
if (mWifiManager != null) return true;
|
||||
mWifiManager = getSystemService(WifiManager.class);
|
||||
|
||||
@@ -16,27 +16,58 @@
|
||||
|
||||
package com.android.settings.dashboard.profileselector;
|
||||
|
||||
import static android.os.UserManager.USER_TYPE_FULL_SYSTEM;
|
||||
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
|
||||
import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
|
||||
|
||||
import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.EXTRA_PROFILE;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.pm.UserInfo;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.android.settings.testutils.shadow.ShadowUserManager;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
@Ignore("b/313569889")
|
||||
@Config(shadows = {
|
||||
ShadowUserManager.class,
|
||||
})
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ProfileSelectLocationFragmentTest {
|
||||
private static final String PERSONAL_PROFILE_NAME = "personal";
|
||||
private static final String WORK_PROFILE_NAME = "work";
|
||||
private static final String PRIVATE_PROFILE_NAME = "private";
|
||||
@Rule
|
||||
public final MockitoRule rule = MockitoJUnit.rule();
|
||||
|
||||
private ShadowUserManager mUserManager;
|
||||
private ProfileSelectLocationFragment mProfileSelectLocationFragment;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mProfileSelectLocationFragment = new ProfileSelectLocationFragment();
|
||||
mUserManager = ShadowUserManager.getShadow();
|
||||
mUserManager.addProfile(
|
||||
new UserInfo(0, PERSONAL_PROFILE_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
|
||||
mUserManager.addProfile(
|
||||
new UserInfo(1, WORK_PROFILE_NAME, null, 0, USER_TYPE_PROFILE_MANAGED));
|
||||
mUserManager.addProfile(
|
||||
new UserInfo(11, PRIVATE_PROFILE_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
|
||||
mProfileSelectLocationFragment = spy(new ProfileSelectLocationFragment());
|
||||
when(mProfileSelectLocationFragment.getContext()).thenReturn(
|
||||
ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -46,7 +77,7 @@ public class ProfileSelectLocationFragmentTest {
|
||||
EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.PERSONAL);
|
||||
assertThat(mProfileSelectLocationFragment.getFragments()[1].getArguments().getInt(
|
||||
EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.WORK);
|
||||
assertThat(mProfileSelectLocationFragment.getFragments()[1].getArguments().getInt(
|
||||
assertThat(mProfileSelectLocationFragment.getFragments()[2].getArguments().getInt(
|
||||
EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.PRIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.development;
|
||||
|
||||
import static com.android.settings.development.BluetoothLeAudioModePreferenceController
|
||||
.LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.os.SystemProperties;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class BluetoothLeAudioModePreferenceControllerTest {
|
||||
|
||||
@Mock
|
||||
private PreferenceScreen mPreferenceScreen;
|
||||
@Mock
|
||||
private DevelopmentSettingsDashboardFragment mFragment;
|
||||
@Mock
|
||||
private BluetoothAdapter mBluetoothAdapter;
|
||||
@Mock
|
||||
private ListPreference mPreference;
|
||||
|
||||
private Context mContext;
|
||||
private BluetoothLeAudioModePreferenceController mController;
|
||||
private String[] mListValues;
|
||||
private String[] mListSummaries;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = RuntimeEnvironment.application;
|
||||
mListValues = mContext.getResources().getStringArray(
|
||||
R.array.bluetooth_leaudio_mode_values);
|
||||
mListSummaries = mContext.getResources().getStringArray(
|
||||
R.array.bluetooth_leaudio_mode);
|
||||
mController = spy(new BluetoothLeAudioModePreferenceController(mContext, mFragment));
|
||||
when(mPreferenceScreen.findPreference(mController.getPreferenceKey()))
|
||||
.thenReturn(mPreference);
|
||||
mController.mBluetoothAdapter = mBluetoothAdapter;
|
||||
mController.displayPreference(mPreferenceScreen);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onRebootDialogConfirmed_changeLeAudioMode_shouldSetLeAudioMode() {
|
||||
mController.mChanged = true;
|
||||
SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]);
|
||||
mController.mNewMode = mListValues[1];
|
||||
|
||||
mController.onRebootDialogConfirmed();
|
||||
assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0])
|
||||
.equals(mController.mNewMode)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onRebootDialogConfirmed_notChangeLeAudioMode_shouldNotSetLeAudioMode() {
|
||||
mController.mChanged = false;
|
||||
SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]);
|
||||
mController.mNewMode = mListValues[1];
|
||||
|
||||
mController.onRebootDialogConfirmed();
|
||||
assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0])
|
||||
.equals(mController.mNewMode)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onRebootDialogCanceled_shouldNotSetLeAudioMode() {
|
||||
mController.mChanged = true;
|
||||
SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]);
|
||||
mController.mNewMode = mListValues[1];
|
||||
|
||||
mController.onRebootDialogCanceled();
|
||||
assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0])
|
||||
.equals(mController.mNewMode)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package com.android.settings.wifi;
|
||||
|
||||
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
|
||||
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||
import static android.os.UserManager.DISALLOW_ADD_WIFI_CONFIG;
|
||||
import static android.os.UserManager.DISALLOW_CONFIG_WIFI;
|
||||
|
||||
import static com.android.settings.wifi.WifiDialogActivity.REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER;
|
||||
@@ -50,7 +51,6 @@ import com.android.wifitrackerlib.WifiEntry;
|
||||
import com.google.android.setupcompat.util.WizardManagerHelper;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
@@ -58,7 +58,6 @@ import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
@Ignore("b/314867581")
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class WifiDialogActivityTest {
|
||||
|
||||
@@ -242,6 +241,20 @@ public class WifiDialogActivityTest {
|
||||
assertThat(mActivity.isConfigWifiAllowed()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isAddWifiConfigAllowed_hasNoUserRestriction_returnTrue() {
|
||||
when(mUserManager.hasUserRestriction(DISALLOW_ADD_WIFI_CONFIG)).thenReturn(false);
|
||||
|
||||
assertThat(mActivity.isAddWifiConfigAllowed()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isAddWifiConfigAllowed_hasUserRestriction_returnFalse() {
|
||||
when(mUserManager.hasUserRestriction(DISALLOW_ADD_WIFI_CONFIG)).thenReturn(true);
|
||||
|
||||
assertThat(mActivity.isAddWifiConfigAllowed()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasPermissionForResult_noCallingPackage_returnFalse() {
|
||||
when(mActivity.getCallingPackage()).thenReturn(null);
|
||||
|
||||
Reference in New Issue
Block a user