diff --git a/res/layout/qrcode_scanner_fragment.xml b/res/layout/qrcode_scanner_fragment.xml
index e6d1c32bfe4..d402dc36ee2 100644
--- a/res/layout/qrcode_scanner_fragment.xml
+++ b/res/layout/qrcode_scanner_fragment.xml
@@ -17,7 +17,6 @@
@@ -26,36 +25,22 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="3"
- android:layout_marginBottom="35dp">
+ android:layout_marginBottom="55dp">
-
-
-
-
+ android:layout_marginTop="20dp"/>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index 357818cd137..0e35fed6a11 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -233,6 +233,26 @@
+
+
+
+ - Disabled
+
+ - Unicast
+
+ - Unicast and Broadcast
+
+
+
+
+
+ - disabled
+
+ - unicast
+
+ - broadcast
+
+
- Use System Default: %1$d
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 491d58fa6b8..b760f68e872 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -249,7 +249,8 @@
Disable Bluetooth LE audio
Disables Bluetooth LE audio feature if the device supports LE audio hardware capabilities.
-
+
+ Bluetooth LE Audio mode
Show LE audio toggle in Device Details
@@ -9623,10 +9624,6 @@
Allow voice activation
Voice activation turns-on approved apps, hands-free, using voice command. Built-in adaptive sensing ensures data stays private only to you.\n\nMore about protected adaptive sensing
-
- Improve voice activation
-
- 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\nMore about private intelligence
Full screen notifications
diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml
index d3aad228b07..5de8dfcf0d7 100644
--- a/res/xml/bluetooth_audio_sharing.xml
+++ b/res/xml/bluetooth_audio_sharing.xml
@@ -45,9 +45,15 @@
+
+
+
+ android:title="Find an audio stream">
-
+
+ android:title="Audio streams nearby"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController">
+
+
+
+
\ No newline at end of file
diff --git a/res/xml/bluetooth_audio_streams_dialog.xml b/res/xml/bluetooth_audio_streams_dialog.xml
index 502e55a1305..024e53763b9 100644
--- a/res/xml/bluetooth_audio_streams_dialog.xml
+++ b/res/xml/bluetooth_audio_streams_dialog.xml
@@ -16,6 +16,7 @@
-->
@@ -23,70 +24,78 @@
android:id="@+id/dialog_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:paddingStart="25dp"
+ android:paddingEnd="25dp"
android:orientation="vertical">
-
+ android:layout_marginBottom="@dimen/broadcast_dialog_margin">
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ style="@style/BroadcastActionButton"/>
-
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ style="@style/BroadcastActionButton"/>
+
\ No newline at end of file
diff --git a/res/xml/bluetooth_audio_streams_qr_code.xml b/res/xml/bluetooth_audio_streams_qr_code.xml
index c750963dbf0..50b14295d5e 100644
--- a/res/xml/bluetooth_audio_streams_qr_code.xml
+++ b/res/xml/bluetooth_audio_streams_qr_code.xml
@@ -36,7 +36,7 @@
android:gravity="start"
android:textSize="15sp"
android:textColor="?android:attr/textColorPrimary"
- android:text="Scan this QR code with another device connected to LE audio headphones to start sharing audio"/>
+ android:text="To listen to this audio stream, other people can connect compatible headphones to their Android device. They can then scan this QR code."/>
+
+
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index d44927fe512..fb5e2809434 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -373,6 +373,13 @@
android:title="@string/bluetooth_disable_leaudio"
android:summary="@string/bluetooth_disable_leaudio_summary" />
+
+
diff --git a/res/xml/network_provider_internet.xml b/res/xml/network_provider_internet.xml
index 1a8ee0872cc..04f248ed745 100644
--- a/res/xml/network_provider_internet.xml
+++ b/res/xml/network_provider_internet.xml
@@ -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"/>
diff --git a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
index 20232991475..2f04b62a821 100644
--- a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
@@ -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);
+ }
}
}
});
diff --git a/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java b/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
index d2400bbbcbb..0fb1769063c 100644
--- a/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
@@ -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);
+ }
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
index 81465ed73cb..44c947d3c56 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
@@ -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();
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
index a3eb188fa2a..644e05eb86e 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
@@ -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);
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
index 94929614230..2022eb260d6 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
@@ -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);
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
new file mode 100644
index 00000000000..da0eb2eb78e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
@@ -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);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
new file mode 100644
index 00000000000..dbb40ece2b0
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
@@ -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);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
index 924b04d3da2..242ce20efe9 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
@@ -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();
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
index bb729d67ec3..47597cf370f 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
@@ -16,39 +16,170 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.ActionButtonsPreference;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
public class AudioStreamButtonController extends BasePreferenceController
implements DefaultLifecycleObserver {
+ private static final String TAG = "AudioStreamButtonController";
private static final String KEY = "audio_stream_button";
+ private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+ new AudioStreamsBroadcastAssistantCallback() {
+ @Override
+ public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoved(sink, sourceId, reason);
+ updateButton();
+ }
+
+ @Override
+ public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoveFailed(sink, sourceId, reason);
+ updateButton();
+ }
+
+ @Override
+ public void onReceiveStateChanged(
+ BluetoothDevice sink,
+ int sourceId,
+ BluetoothLeBroadcastReceiveState state) {
+ super.onReceiveStateChanged(sink, sourceId, state);
+ if (mAudioStreamsHelper.isConnected(state)) {
+ updateButton();
+ }
+ }
+
+ @Override
+ public void onSourceAddFailed(
+ BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+ super.onSourceAddFailed(sink, source, reason);
+ updateButton();
+ }
+
+ @Override
+ public void onSourceLost(int broadcastId) {
+ super.onSourceLost(broadcastId);
+ updateButton();
+ }
+ };
+
+ private final AudioStreamsRepository mAudioStreamsRepository =
+ AudioStreamsRepository.getInstance();
+ private final Executor mExecutor;
+ private final AudioStreamsHelper mAudioStreamsHelper;
+ private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private @Nullable ActionButtonsPreference mPreference;
private int mBroadcastId = -1;
public AudioStreamButtonController(Context context, String preferenceKey) {
super(context, preferenceKey);
+ mExecutor = Executors.newSingleThreadExecutor();
+ mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
+ mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
+ return;
+ }
+ mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
+ return;
+ }
+ mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
@Override
public final void displayPreference(PreferenceScreen screen) {
mPreference = screen.findPreference(getPreferenceKey());
- if (mPreference != null) {
- mPreference.setButton1Enabled(true);
- // TODO(chelseahao): update this based on stream connection state
- mPreference
- .setButton1Text(R.string.bluetooth_device_context_disconnect)
- .setButton1Icon(R.drawable.ic_settings_close);
- }
+ updateButton();
super.displayPreference(screen);
}
+ private void updateButton() {
+ if (mPreference != null) {
+ if (mAudioStreamsHelper.getAllConnectedSources().stream()
+ .map(BluetoothLeBroadcastReceiveState::getBroadcastId)
+ .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId)) {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setButton1Enabled(true);
+ mPreference
+ .setButton1Text(
+ R.string.bluetooth_device_context_disconnect)
+ .setButton1Icon(R.drawable.ic_settings_close)
+ .setButton1OnClickListener(
+ unused -> {
+ if (mPreference != null) {
+ mPreference.setButton1Enabled(false);
+ }
+ mAudioStreamsHelper.removeSource(mBroadcastId);
+ });
+ }
+ });
+ } else {
+ View.OnClickListener clickToRejoin =
+ unused ->
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ var metadata =
+ mAudioStreamsRepository.getSavedMetadata(
+ mContext, mBroadcastId);
+ if (metadata != null) {
+ mAudioStreamsHelper.addSource(metadata);
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setButton1Enabled(
+ false);
+ }
+ });
+ }
+ });
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setButton1Enabled(true);
+ mPreference
+ .setButton1Text(R.string.bluetooth_device_context_connect)
+ .setButton1Icon(R.drawable.ic_add_24dp)
+ .setButton1OnClickListener(clickToRejoin);
+ }
+ });
+ }
+ } else {
+ Log.w(TAG, "updateButton(): preference is null!");
+ }
+ }
+
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
index 5981c9e1876..131c8f6baaf 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
@@ -104,7 +104,7 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
private Dialog getErrorDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mActivity)
.setTitle("Can't listen to audio stream")
- .setSubTitle1("Can't play this audio stream. Learn more")
+ .setSubTitle2("Can't play this audio stream. Learn more")
.setRightButtonText("Close")
.setRightButtonOnClickListener(
unused -> {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
index 89f24bccdbd..3524543bfc5 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
@@ -16,22 +16,64 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.LayoutPreference;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
import javax.annotation.Nullable;
public class AudioStreamHeaderController extends BasePreferenceController
implements DefaultLifecycleObserver {
+ private static final String TAG = "AudioStreamHeaderController";
private static final String KEY = "audio_stream_header";
+ private final Executor mExecutor;
+ private final AudioStreamsHelper mAudioStreamsHelper;
+ @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+ private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+ new AudioStreamsBroadcastAssistantCallback() {
+ @Override
+ public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoved(sink, sourceId, reason);
+ updateSummary();
+ }
+
+ @Override
+ public void onSourceLost(int broadcastId) {
+ super.onSourceLost(broadcastId);
+ updateSummary();
+ }
+
+ @Override
+ public void onReceiveStateChanged(
+ BluetoothDevice sink,
+ int sourceId,
+ BluetoothLeBroadcastReceiveState state) {
+ super.onReceiveStateChanged(sink, sourceId, state);
+ if (mAudioStreamsHelper.isConnected(state)) {
+ updateSummary();
+ }
+ }
+ };
+
private @Nullable EntityHeaderController mHeaderController;
private @Nullable DashboardFragment mFragment;
private String mBroadcastName = "";
@@ -39,6 +81,27 @@ public class AudioStreamHeaderController extends BasePreferenceController
public AudioStreamHeaderController(Context context, String preferenceKey) {
super(context, preferenceKey);
+ mExecutor = Executors.newSingleThreadExecutor();
+ mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
+ mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
+ return;
+ }
+ mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
+ return;
+ }
+ mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
@Override
@@ -55,14 +118,37 @@ public class AudioStreamHeaderController extends BasePreferenceController
}
mHeaderController.setIcon(
screen.getContext().getDrawable(R.drawable.ic_bt_audio_sharing));
- // TODO(chelseahao): update this based on stream connection state
- mHeaderController.setSummary("Listening now");
- mHeaderController.done(true);
screen.addPreference(headerPreference);
+ updateSummary();
}
super.displayPreference(screen);
}
+ private void updateSummary() {
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ var latestSummary =
+ mAudioStreamsHelper.getAllConnectedSources().stream()
+ .map(
+ BluetoothLeBroadcastReceiveState
+ ::getBroadcastId)
+ .anyMatch(
+ connectedBroadcastId ->
+ connectedBroadcastId
+ == mBroadcastId)
+ ? "Listening now"
+ : "";
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mHeaderController != null) {
+ mHeaderController.setSummary(latestSummary);
+ mHeaderController.done(true);
+ }
+ });
+ });
+ }
+
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index 678f9524a37..c2e11786110 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -21,8 +21,10 @@ import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.util.AttributeSet;
+import android.view.View;
import androidx.annotation.Nullable;
+import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;
@@ -56,7 +58,6 @@ class AudioStreamPreference extends TwoTargetPreference {
}
mIsConnected = isConnected;
setSummary(summary);
- setOrder(isConnected ? 0 : 1);
setOnPreferenceClickListener(onPreferenceClickListener);
notifyChanged();
}
@@ -70,6 +71,23 @@ class AudioStreamPreference extends TwoTargetPreference {
mAudioStream.setState(state);
}
+ void setAudioStreamMetadata(BluetoothLeBroadcastMetadata metadata) {
+ mAudioStream.setMetadata(metadata);
+ }
+
+ int getAudioStreamBroadcastId() {
+ return mAudioStream.getBroadcastId();
+ }
+
+ int getAudioStreamRssi() {
+ return mAudioStream.getRssi();
+ }
+
+ @Nullable
+ BluetoothLeBroadcastMetadata getAudioStreamMetadata() {
+ return mAudioStream.getMetadata();
+ }
+
AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
return mAudioStream.getState();
}
@@ -84,25 +102,31 @@ class AudioStreamPreference extends TwoTargetPreference {
return R.layout.preference_widget_lock;
}
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ View divider =
+ holder.findViewById(
+ com.android.settingslib.widget.preference.twotarget.R.id
+ .two_target_divider);
+ if (divider != null) {
+ divider.setVisibility(View.GONE);
+ }
+ }
+
static AudioStreamPreference fromMetadata(
- Context context,
- BluetoothLeBroadcastMetadata source,
- AudioStreamsProgressCategoryController.AudioStreamState streamState) {
+ Context context, BluetoothLeBroadcastMetadata source) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(source));
- preference.setAudioStream(new AudioStream(source.getBroadcastId(), streamState));
+ preference.setAudioStream(new AudioStream(source));
return preference;
}
static AudioStreamPreference fromReceiveState(
- Context context,
- BluetoothLeBroadcastReceiveState receiveState,
- AudioStreamsProgressCategoryController.AudioStreamState streamState) {
+ Context context, BluetoothLeBroadcastReceiveState receiveState) {
AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
preference.setTitle(getBroadcastName(receiveState));
- preference.setAudioStream(
- new AudioStream(
- receiveState.getSourceId(), receiveState.getBroadcastId(), streamState));
+ preference.setAudioStream(new AudioStream(receiveState));
return preference;
}
@@ -127,41 +151,45 @@ class AudioStreamPreference extends TwoTargetPreference {
}
private static final class AudioStream {
- private int mSourceId;
- private int mBroadcastId;
- private AudioStreamsProgressCategoryController.AudioStreamState mState;
+ private static final int UNAVAILABLE = -1;
+ @Nullable private BluetoothLeBroadcastMetadata mMetadata;
+ @Nullable private BluetoothLeBroadcastReceiveState mReceiveState;
+ private AudioStreamsProgressCategoryController.AudioStreamState mState =
+ AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
- private AudioStream(
- int broadcastId, AudioStreamsProgressCategoryController.AudioStreamState state) {
- mBroadcastId = broadcastId;
- mState = state;
+ private AudioStream(BluetoothLeBroadcastMetadata metadata) {
+ mMetadata = metadata;
}
- private AudioStream(
- int sourceId,
- int broadcastId,
- AudioStreamsProgressCategoryController.AudioStreamState state) {
- mSourceId = sourceId;
- mBroadcastId = broadcastId;
- mState = state;
+ private AudioStream(BluetoothLeBroadcastReceiveState receiveState) {
+ mReceiveState = receiveState;
}
- // TODO(chelseahao): use this to handleSourceRemoved
- private int getSourceId() {
- return mSourceId;
- }
-
- // TODO(chelseahao): use this to handleSourceRemoved
private int getBroadcastId() {
- return mBroadcastId;
+ return mMetadata != null
+ ? mMetadata.getBroadcastId()
+ : mReceiveState != null ? mReceiveState.getBroadcastId() : UNAVAILABLE;
+ }
+
+ private int getRssi() {
+ return mMetadata != null ? mMetadata.getRssi() : Integer.MAX_VALUE;
}
private AudioStreamsProgressCategoryController.AudioStreamState getState() {
return mState;
}
+ @Nullable
+ private BluetoothLeBroadcastMetadata getMetadata() {
+ return mMetadata;
+ }
+
private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
mState = state;
}
+
+ private void setMetadata(BluetoothLeBroadcastMetadata metadata) {
+ mMetadata = metadata;
+ }
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
index 84e753c5526..9fb5b21fed9 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
@@ -24,21 +24,12 @@ import android.util.Log;
import com.android.settingslib.bluetooth.BluetoothUtils;
-import java.util.Locale;
-
public class AudioStreamsBroadcastAssistantCallback
implements BluetoothLeBroadcastAssistant.Callback {
private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
private static final boolean DEBUG = BluetoothUtils.D;
- private final AudioStreamsProgressCategoryController mCategoryController;
-
- public AudioStreamsBroadcastAssistantCallback(
- AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
- mCategoryController = audioStreamsProgressCategoryController;
- }
-
@Override
public void onReceiveStateChanged(
BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
@@ -52,45 +43,30 @@ public class AudioStreamsBroadcastAssistantCallback
+ " state: "
+ state);
}
- mCategoryController.handleSourceConnected(state);
}
@Override
public void onSearchStartFailed(int reason) {
Log.w(TAG, "onSearchStartFailed() reason : " + reason);
- mCategoryController.showToast(
- String.format(Locale.US, "Failed to start scanning, reason %d", reason));
}
@Override
public void onSearchStarted(int reason) {
- if (mCategoryController == null) {
- Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
- return;
- }
if (DEBUG) {
Log.d(TAG, "onSearchStarted() reason : " + reason);
}
- mCategoryController.setScanning(true);
}
@Override
public void onSearchStopFailed(int reason) {
Log.w(TAG, "onSearchStopFailed() reason : " + reason);
- mCategoryController.showToast(
- String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
}
@Override
public void onSearchStopped(int reason) {
- if (mCategoryController == null) {
- Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
- return;
- }
if (DEBUG) {
Log.d(TAG, "onSearchStopped() reason : " + reason);
}
- mCategoryController.setScanning(false);
}
@Override
@@ -106,8 +82,6 @@ public class AudioStreamsBroadcastAssistantCallback
+ " reason: "
+ reason);
}
- mCategoryController.showToast(
- String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
}
@Override
@@ -126,14 +100,9 @@ public class AudioStreamsBroadcastAssistantCallback
@Override
public void onSourceFound(BluetoothLeBroadcastMetadata source) {
- if (mCategoryController == null) {
- Log.w(TAG, "onSourceFound() : mCategoryController is null!");
- return;
- }
if (DEBUG) {
Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
}
- mCategoryController.handleSourceFound(source);
}
@Override
@@ -141,7 +110,6 @@ public class AudioStreamsBroadcastAssistantCallback
if (DEBUG) {
Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
}
- mCategoryController.handleSourceLost(broadcastId);
}
@Override
@@ -153,12 +121,6 @@ public class AudioStreamsBroadcastAssistantCallback
@Override
public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason);
- mCategoryController.showToast(
- String.format(
- Locale.US,
- "Failed to remove source %d for sink %s",
- sourceId,
- sink.getAddress()));
}
@Override
@@ -166,8 +128,5 @@ public class AudioStreamsBroadcastAssistantCallback
if (DEBUG) {
Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
}
- mCategoryController.showToast(
- String.format(
- Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress()));
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
new file mode 100644
index 00000000000..34ffc911269
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.util.Log;
+
+import java.util.Locale;
+
+public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
+ private static final String TAG = "AudioStreamsProgressCategoryCallback";
+
+ private final AudioStreamsProgressCategoryController mCategoryController;
+
+ public AudioStreamsProgressCategoryCallback(
+ AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
+ mCategoryController = audioStreamsProgressCategoryController;
+ }
+
+ @Override
+ public void onReceiveStateChanged(
+ BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
+ super.onReceiveStateChanged(sink, sourceId, state);
+ mCategoryController.handleSourceConnected(state);
+ }
+
+ @Override
+ public void onSearchStartFailed(int reason) {
+ super.onSearchStartFailed(reason);
+ mCategoryController.showToast(
+ String.format(Locale.US, "Failed to start scanning, reason %d", reason));
+ }
+
+ @Override
+ public void onSearchStarted(int reason) {
+ super.onSearchStarted(reason);
+ if (mCategoryController == null) {
+ Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
+ return;
+ }
+ mCategoryController.setScanning(true);
+ }
+
+ @Override
+ public void onSearchStopFailed(int reason) {
+ super.onSearchStopFailed(reason);
+ mCategoryController.showToast(
+ String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
+ }
+
+ @Override
+ public void onSearchStopped(int reason) {
+ super.onSearchStopped(reason);
+ if (mCategoryController == null) {
+ Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
+ return;
+ }
+ mCategoryController.setScanning(false);
+ }
+
+ @Override
+ public void onSourceAddFailed(
+ BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+ super.onSourceAddFailed(sink, source, reason);
+ mCategoryController.showToast(
+ String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
+ }
+
+ @Override
+ public void onSourceFound(BluetoothLeBroadcastMetadata source) {
+ super.onSourceFound(source);
+ if (mCategoryController == null) {
+ Log.w(TAG, "onSourceFound() : mCategoryController is null!");
+ return;
+ }
+ mCategoryController.handleSourceFound(source);
+ }
+
+ @Override
+ public void onSourceLost(int broadcastId) {
+ super.onSourceLost(broadcastId);
+ mCategoryController.handleSourceLost(broadcastId);
+ }
+
+ @Override
+ public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoveFailed(sink, sourceId, reason);
+ mCategoryController.showToast(
+ String.format(
+ Locale.US,
+ "Failed to remove source %d for sink %s",
+ sourceId,
+ sink.getAddress()));
+ }
+
+ @Override
+ public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+ super.onSourceRemoved(sink, sourceId, reason);
+ mCategoryController.handleSourceRemoved();
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index cb9975da9aa..c6f342a421f 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
+
import static java.util.Collections.emptyList;
import android.app.AlertDialog;
@@ -43,8 +45,10 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -53,6 +57,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.nio.charset.StandardCharsets;
+import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -74,25 +79,77 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
};
+ private final Preference.OnPreferenceClickListener mAddSourceOrShowDialog =
+ preference -> {
+ var p = (AudioStreamPreference) preference;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "preferenceClicked(): attempt to join broadcast id : "
+ + p.getAudioStreamBroadcastId());
+ }
+ var source = p.getAudioStreamMetadata();
+ if (source != null) {
+ if (source.isEncrypted()) {
+ ThreadUtils.postOnMainThread(() -> launchPasswordDialog(source, p));
+ } else {
+ moveToState(p, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
+ }
+ }
+ return true;
+ };
+
+ private final Preference.OnPreferenceClickListener mLaunchDetailFragment =
+ preference -> {
+ var p = (AudioStreamPreference) preference;
+ Bundle broadcast = new Bundle();
+ broadcast.putString(
+ AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle());
+ broadcast.putInt(
+ AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
+
+ new SubSettingLauncher(mContext)
+ .setTitleText("Audio stream details")
+ .setDestination(AudioStreamDetailsFragment.class.getName())
+ // TODO(chelseahao): Add logging enum
+ .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
+ .setArguments(broadcast)
+ .launch();
+ return true;
+ };
+
+ private final AudioStreamsRepository mAudioStreamsRepository =
+ AudioStreamsRepository.getInstance();
+
enum AudioStreamState {
+ UNKNOWN,
// When mTimedSourceFromQrCode is present and this source has not been synced.
WAIT_FOR_SYNC,
// When source has been synced but not added to any sink.
SYNCED,
// When addSource is called for this source and waiting for response.
- WAIT_FOR_SOURCE_ADD,
+ ADD_SOURCE_WAIT_FOR_RESPONSE,
// Source is added to active sink.
SOURCE_ADDED,
}
+ private final Comparator mComparator =
+ Comparator.comparing(
+ p ->
+ p.getAudioStreamState()
+ == AudioStreamsProgressCategoryController
+ .AudioStreamState.SOURCE_ADDED)
+ .thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
+ .reversed();
+
private final Executor mExecutor;
- private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
+ private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
- private TimedSourceFromQrCode mTimedSourceFromQrCode;
+ private @Nullable TimedSourceFromQrCode mTimedSourceFromQrCode;
private AudioStreamsProgressCategoryPreference mCategoryPreference;
private AudioStreamsDashboardFragment mFragment;
@@ -102,7 +159,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mBluetoothManager = Utils.getLocalBtManager(mContext);
mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
- mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
+ mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
}
@Override
@@ -155,41 +212,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
void handleSourceFound(BluetoothLeBroadcastMetadata source) {
- Preference.OnPreferenceClickListener addSourceOrShowDialog =
- preference -> {
- if (DEBUG) {
- Log.d(
- TAG,
- "preferenceClicked(): attempt to join broadcast id : "
- + source.getBroadcastId());
- }
- if (source.isEncrypted()) {
- ThreadUtils.postOnMainThread(
- () ->
- launchPasswordDialog(
- source, (AudioStreamPreference) preference));
- } else {
- mAudioStreamsHelper.addSource(source);
- ((AudioStreamPreference) preference)
- .setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- (AudioStreamPreference) preference,
- AudioStreamState.WAIT_FOR_SOURCE_ADD,
- null);
- }
- return true;
- };
-
var broadcastIdFound = source.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
broadcastIdFound,
(k, v) -> {
if (v == null) {
- return addNewPreference(
- source, AudioStreamState.SYNCED, addSourceOrShowDialog);
+ // No existing preference for this source founded, add one and set initial
+ // state to SYNCED.
+ return addNewPreference(source, AudioStreamState.SYNCED);
}
var fromState = v.getAudioStreamState();
- if (fromState == AudioStreamState.WAIT_FOR_SYNC) {
+ if (fromState == AudioStreamState.WAIT_FOR_SYNC
+ && mTimedSourceFromQrCode != null) {
var pendingSource = mTimedSourceFromQrCode.get();
if (pendingSource == null) {
Log.w(
@@ -198,15 +232,20 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
+ fromState
+ " for broadcastId : "
+ broadcastIdFound);
- v.setAudioStreamState(AudioStreamState.SYNCED);
+ v.setAudioStreamMetadata(source);
+ moveToState(v, AudioStreamState.SYNCED);
return v;
}
- mAudioStreamsHelper.addSource(pendingSource);
- mTimedSourceFromQrCode.consumed();
- v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ // A preference with source founded is existed from a QR code scan. As the
+ // source is now synced, we update the preference with pendingSource from QR
+ // code scan and add source with it (since it has the password).
+ v.setAudioStreamMetadata(pendingSource);
+ moveToState(v, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else {
+ // A preference with source founded existed either because it's already
+ // connected (SOURCE_ADDED), or other unexpected reason. We update the
+ // preference with this source and won't change it's state.
+ v.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED) {
Log.w(
TAG,
@@ -229,18 +268,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
metadataFromQrCode.getBroadcastId(),
(k, v) -> {
if (v == null) {
- mTimedSourceFromQrCode.waitForConsume();
- return addNewPreference(
- metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC, null);
+ // No existing preference for this source from the QR code scan, add one and
+ // set initial state to WAIT_FOR_SYNC.
+ return addNewPreference(metadataFromQrCode, AudioStreamState.WAIT_FOR_SYNC);
}
var fromState = v.getAudioStreamState();
if (fromState == AudioStreamState.SYNCED) {
- mAudioStreamsHelper.addSource(metadataFromQrCode);
- mTimedSourceFromQrCode.consumed();
- v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- v, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ // A preference with source from the QR code is existed because it has been
+ // founded during scanning, now we have the password, we can add source.
+ v.setAudioStreamMetadata(metadataFromQrCode);
+ moveToState(v, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else {
+ v.setAudioStreamMetadata(metadataFromQrCode);
Log.w(
TAG,
"handleSourceFromQrCode(): unexpected state : "
@@ -265,54 +304,71 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper.removeSource(broadcastId);
}
+ void handleSourceRemoved() {
+ for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
+ var preference = entry.getValue();
+
+ // Look for preference has SOURCE_ADDED state, re-check if they are still connected. If
+ // not, means the source is removed from the sink, we move back the preference to SYNCED
+ // state.
+ if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
+ && mAudioStreamsHelper.getAllConnectedSources().stream()
+ .noneMatch(
+ connected ->
+ connected.getBroadcastId()
+ == preference.getAudioStreamBroadcastId())) {
+
+ ThreadUtils.postOnMainThread(
+ () -> {
+ var metadata = preference.getAudioStreamMetadata();
+
+ if (metadata != null) {
+ moveToState(preference, AudioStreamState.SYNCED);
+ } else {
+ handleSourceLost(preference.getAudioStreamBroadcastId());
+ }
+ });
+
+ return;
+ }
+ }
+ }
+
void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
if (!mAudioStreamsHelper.isConnected(receiveState)) {
return;
}
- var sourceAddedState = AudioStreamState.SOURCE_ADDED;
var broadcastIdConnected = receiveState.getBroadcastId();
mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected,
(k, v) -> {
if (v == null) {
- return addNewPreference(
- receiveState,
- sourceAddedState,
- p -> launchDetailFragment(broadcastIdConnected));
+ // No existing preference for this source even if it's already connected,
+ // add one and set initial state to SOURCE_ADDED. This could happen because
+ // we retrieves the connected source during onStart() from
+ // AudioStreamsHelper#getAllConnectedSources() even before the source is
+ // founded by scanning.
+ return addNewPreference(receiveState, AudioStreamState.SOURCE_ADDED);
}
var fromState = v.getAudioStreamState();
- if (fromState == AudioStreamState.WAIT_FOR_SOURCE_ADD
+ if (fromState == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE
|| fromState == AudioStreamState.SYNCED
- || fromState == AudioStreamState.WAIT_FOR_SYNC) {
- if (mTimedSourceFromQrCode != null) {
- mTimedSourceFromQrCode.consumed();
- }
+ || fromState == AudioStreamState.WAIT_FOR_SYNC
+ || fromState == AudioStreamState.SOURCE_ADDED) {
+ // Expected state, do nothing
} else {
- if (fromState != AudioStreamState.SOURCE_ADDED) {
- Log.w(
- TAG,
- "handleSourceConnected(): unexpected state : "
- + fromState
- + " for broadcastId : "
- + broadcastIdConnected);
- }
+ Log.w(
+ TAG,
+ "handleSourceConnected(): unexpected state : "
+ + fromState
+ + " for broadcastId : "
+ + broadcastIdConnected);
}
- v.setAudioStreamState(sourceAddedState);
- updatePreferenceConnectionState(
- v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected));
+ moveToState(v, AudioStreamState.SOURCE_ADDED);
return v;
});
}
- private static String getPreferenceSummary(AudioStreamState state) {
- return switch (state) {
- case WAIT_FOR_SYNC -> "Scanning...";
- case WAIT_FOR_SOURCE_ADD -> "Connecting...";
- case SOURCE_ADDED -> "Listening now";
- default -> "";
- };
- }
-
void showToast(String msg) {
AudioSharingUtils.toastMessage(mContext, msg);
}
@@ -322,7 +378,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
ThreadUtils.postOnMainThread(
() -> {
if (mCategoryPreference != null) {
- mCategoryPreference.removeAll();
+ mCategoryPreference.removeAudioStreamPreferences();
mCategoryPreference.setVisible(hasActive);
}
});
@@ -348,7 +404,6 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
Log.d(TAG, "startScanning()");
}
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
- mLeBroadcastAssistant.startSearchingForSources(emptyList());
// Handle QR code scan and display currently connected streams
var unused =
@@ -358,6 +413,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mAudioStreamsHelper
.getAllConnectedSources()
.forEach(this::handleSourceConnected);
+ mLeBroadcastAssistant.startSearchingForSources(emptyList());
});
}
@@ -374,68 +430,93 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
if (mTimedSourceFromQrCode != null) {
- mTimedSourceFromQrCode.consumed();
+ mTimedSourceFromQrCode.cleanup();
+ mTimedSourceFromQrCode = null;
}
}
private AudioStreamPreference addNewPreference(
- BluetoothLeBroadcastReceiveState receiveState,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
- var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState, state);
- updatePreferenceConnectionState(preference, state, onClickListener);
+ BluetoothLeBroadcastReceiveState receiveState, AudioStreamState state) {
+ var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState);
+ moveToState(preference, state);
return preference;
}
private AudioStreamPreference addNewPreference(
- BluetoothLeBroadcastMetadata metadata,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
- var preference = AudioStreamPreference.fromMetadata(mContext, metadata, state);
- updatePreferenceConnectionState(preference, state, onClickListener);
+ BluetoothLeBroadcastMetadata metadata, AudioStreamState state) {
+ var preference = AudioStreamPreference.fromMetadata(mContext, metadata);
+ moveToState(preference, state);
return preference;
}
- private void updatePreferenceConnectionState(
- AudioStreamPreference preference,
- AudioStreamState state,
- Preference.OnPreferenceClickListener onClickListener) {
+ private void moveToState(AudioStreamPreference preference, AudioStreamState state) {
+ if (preference.getAudioStreamState() == state) {
+ return;
+ }
+ preference.setAudioStreamState(state);
+
+ // Perform action according to the new state
+ if (state == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed(preference.getAudioStreamBroadcastId());
+ }
+ var metadata = preference.getAudioStreamMetadata();
+ if (metadata != null) {
+ mAudioStreamsHelper.addSource(metadata);
+ // Cache the metadata that used for add source, if source is added successfully, we
+ // will save it persistently.
+ mAudioStreamsRepository.cacheMetadata(metadata);
+ }
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.consumed(preference.getAudioStreamBroadcastId());
+ }
+ // Saved connected metadata for user to re-join this broadcast later.
+ var cached =
+ mAudioStreamsRepository.getCachedMetadata(
+ preference.getAudioStreamBroadcastId());
+ if (cached != null) {
+ mAudioStreamsRepository.saveMetadata(mContext, cached);
+ }
+ } else if (state == AudioStreamState.WAIT_FOR_SYNC) {
+ if (mTimedSourceFromQrCode != null) {
+ mTimedSourceFromQrCode.waitForConsume();
+ }
+ }
+
+ // Get preference click listener according to the new state
+ Preference.OnPreferenceClickListener listener;
+ if (state == AudioStreamState.SYNCED) {
+ listener = mAddSourceOrShowDialog;
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ listener = mLaunchDetailFragment;
+ } else {
+ listener = null;
+ }
+
+ // Get preference summary according to the new state
+ String summary;
+ if (state == AudioStreamState.WAIT_FOR_SYNC) {
+ summary = "Scanning...";
+ } else if (state == AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE) {
+ summary = "Connecting...";
+ } else if (state == AudioStreamState.SOURCE_ADDED) {
+ summary = "Listening now";
+ } else {
+ summary = "";
+ }
+
+ // Update UI
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(
- state == AudioStreamState.SOURCE_ADDED,
- getPreferenceSummary(state),
- onClickListener);
+ state == AudioStreamState.SOURCE_ADDED, summary, listener);
if (mCategoryPreference != null) {
- mCategoryPreference.addPreference(preference);
+ mCategoryPreference.addAudioStreamPreference(preference, mComparator);
}
});
}
- private boolean launchDetailFragment(int broadcastId) {
- if (!mBroadcastIdToPreferenceMap.containsKey(broadcastId)) {
- Log.w(
- TAG,
- "launchDetailFragment(): broadcastId not exist in BroadcastIdToPreferenceMap!");
- return false;
- }
- AudioStreamPreference preference = mBroadcastIdToPreferenceMap.get(broadcastId);
-
- Bundle broadcast = new Bundle();
- broadcast.putString(
- AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) preference.getTitle());
- broadcast.putInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG, broadcastId);
-
- new SubSettingLauncher(mContext)
- .setTitleText("Audio stream details")
- .setDestination(AudioStreamDetailsFragment.class.getName())
- // TODO(chelseahao): Add logging enum
- .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
- .setArguments(broadcast)
- .launch();
- return true;
- }
-
private void launchPasswordDialog(
BluetoothLeBroadcastMetadata source, AudioStreamPreference preference) {
View layout =
@@ -457,15 +538,16 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
R.id.broadcast_edit_text))
.getText()
.toString();
- mAudioStreamsHelper.addSource(
+ var metadata =
new BluetoothLeBroadcastMetadata.Builder(source)
.setBroadcastCode(
code.getBytes(StandardCharsets.UTF_8))
- .build());
- preference.setAudioStreamState(
- AudioStreamState.WAIT_FOR_SOURCE_ADD);
- updatePreferenceConnectionState(
- preference, AudioStreamState.WAIT_FOR_SOURCE_ADD, null);
+ .build();
+ // Update the metadata after user entered the password
+ preference.setAudioStreamMetadata(metadata);
+ moveToState(
+ preference,
+ AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
})
.create();
alertDialog.show();
@@ -474,16 +556,17 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private AudioStreamsDialogFragment.DialogBuilder getNoLeDeviceDialog() {
return new AudioStreamsDialogFragment.DialogBuilder(mContext)
.setTitle("Connect compatible headphones")
- .setSubTitle1(
+ .setSubTitle2(
"To listen to an audio stream, first connect headphones that support LE"
+ " Audio to this device. Learn more")
.setLeftButtonText("Close")
.setLeftButtonOnClickListener(AlertDialog::dismiss)
.setRightButtonText("Connect a device")
.setRightButtonOnClickListener(
- unused ->
- mContext.startActivity(
- new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)));
+ dialog -> {
+ mContext.startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
+ dialog.dismiss();
+ });
}
private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableDialog(
@@ -495,8 +578,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
.setLeftButtonText("Close")
.setLeftButtonOnClickListener(AlertDialog::dismiss)
.setRightButtonText("Retry")
- // TODO(chelseahao): Add retry action
- .setRightButtonOnClickListener(AlertDialog::dismiss);
+ .setRightButtonOnClickListener(
+ dialog -> {
+ if (mFragment != null) {
+ Intent intent = new Intent(mContext, QrCodeScanModeActivity.class);
+ intent.setAction(
+ BluetoothBroadcastUtils
+ .ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER);
+ mFragment.startActivityForResult(
+ intent, REQUEST_SCAN_BT_BROADCAST_QR_CODE);
+ dialog.dismiss();
+ }
+ });
}
private class TimedSourceFromQrCode {
@@ -529,11 +622,18 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
mTimer.start();
}
- private void consumed() {
+ private void cleanup() {
mTimer.cancel();
mSourceFromQrCode = null;
}
+ private void consumed(int broadcastId) {
+ if (mSourceFromQrCode == null || broadcastId != mSourceFromQrCode.getBroadcastId()) {
+ return;
+ }
+ cleanup();
+ }
+
private BluetoothLeBroadcastMetadata get() {
return mSourceFromQrCode;
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
index d2599000ba3..33adc312178 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
@@ -19,9 +19,15 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
import android.util.AttributeSet;
+import androidx.annotation.NonNull;
+
import com.android.settings.ProgressCategory;
import com.android.settings.R;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
public AudioStreamsProgressCategoryPreference(Context context) {
@@ -46,6 +52,37 @@ public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
init();
}
+ void addAudioStreamPreference(
+ @NonNull AudioStreamPreference preference,
+ Comparator comparator) {
+ super.addPreference(preference);
+
+ List preferences = getAllAudioStreamPreferences();
+ preferences.sort(comparator);
+ for (int i = 0; i < preferences.size(); i++) {
+ // setOrder to i + 1, since the order 0 preference should always be the
+ // "audio_streams_scan_qr_code"
+ preferences.get(i).setOrder(i + 1);
+ }
+ }
+
+ void removeAudioStreamPreferences() {
+ List streams = getAllAudioStreamPreferences();
+ for (var toRemove : streams) {
+ removePreference(toRemove);
+ }
+ }
+
+ private List getAllAudioStreamPreferences() {
+ List streams = new ArrayList<>();
+ for (int i = 0; i < getPreferenceCount(); i++) {
+ if (getPreference(i) instanceof AudioStreamPreference) {
+ streams.add((AudioStreamPreference) getPreference(i));
+ }
+ }
+ return streams;
+ }
+
private void init() {
setEmptyTextRes(R.string.audio_streams_empty);
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
index 42b38ee2466..2366e70fb33 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
@@ -24,6 +24,9 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
@@ -34,6 +37,7 @@ import com.android.settingslib.qrcode.QrCodeGenerator;
import com.google.zxing.WriterException;
+import java.nio.charset.StandardCharsets;
import java.util.Optional;
public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
@@ -49,30 +53,47 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
public final View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
- getQrCodeBitmap()
- .ifPresent(
- bm ->
+
+ BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
+
+ if (broadcastMetadata != null) {
+ getQrCodeBitmap(broadcastMetadata)
+ .ifPresent(
+ bm -> {
((ImageView) view.requireViewById(R.id.qrcode_view))
- .setImageBitmap(bm));
+ .setImageBitmap(bm);
+ ((TextView) view.requireViewById(R.id.password))
+ .setText(
+ "Password: "
+ + new String(
+ broadcastMetadata
+ .getBroadcastCode(),
+ StandardCharsets.UTF_8));
+ });
+ }
return view;
}
- private Optional getQrCodeBitmap() {
- String broadcastMetadata = getBroadcastMetadataQrCode();
- if (broadcastMetadata.isEmpty()) {
+ private Optional getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata) {
+ if (metadata == null) {
Log.d(TAG, "onCreateView: broadcastMetadata is empty!");
return Optional.empty();
}
-
+ String metadataStr = BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
+ if (metadataStr.isEmpty()) {
+ Log.d(TAG, "onCreateView: metadataStr is empty!");
+ return Optional.empty();
+ }
+ Log.d("chelsea", metadataStr);
try {
int qrcodeSize = getContext().getResources().getDimensionPixelSize(R.dimen.qrcode_size);
- Bitmap bitmap = QrCodeGenerator.encodeQrCode(broadcastMetadata, qrcodeSize);
+ Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize);
return Optional.of(bitmap);
} catch (WriterException e) {
Log.d(
TAG,
"onCreateView: broadcastMetadata "
- + broadcastMetadata
+ + metadata
+ " qrCode generation exception "
+ e);
}
@@ -80,23 +101,24 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
return Optional.empty();
}
- private String getBroadcastMetadataQrCode() {
+ @Nullable
+ private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
LocalBluetoothLeBroadcast localBluetoothLeBroadcast =
Utils.getLocalBtManager(getActivity())
.getProfileManager()
.getLeAudioBroadcastProfile();
if (localBluetoothLeBroadcast == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: localBluetoothLeBroadcast is null!");
- return "";
+ return null;
}
BluetoothLeBroadcastMetadata metadata =
localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata();
if (metadata == null) {
Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!");
- return "";
+ return null;
}
- return BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
+ return metadata;
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java
new file mode 100644
index 00000000000..65245acf177
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nullable;
+
+/** Manages the caching and storage of Bluetooth audio stream metadata. */
+public class AudioStreamsRepository {
+
+ private static final String TAG = "AudioStreamsRepository";
+ private static final boolean DEBUG = BluetoothUtils.D;
+
+ private static final String PREF_KEY = "bluetooth_audio_stream_pref";
+ private static final String METADATA_KEY = "bluetooth_audio_stream_metadata";
+
+ @Nullable
+ private static AudioStreamsRepository sInstance = null;
+
+ private AudioStreamsRepository() {}
+
+ /**
+ * Gets the single instance of AudioStreamsRepository.
+ *
+ * @return The AudioStreamsRepository instance.
+ */
+ public static synchronized AudioStreamsRepository getInstance() {
+ if (sInstance == null) {
+ sInstance = new AudioStreamsRepository();
+ }
+ return sInstance;
+ }
+
+ private final ConcurrentHashMap
+ mBroadcastIdToMetadataCacheMap = new ConcurrentHashMap<>();
+
+ /**
+ * Caches BluetoothLeBroadcastMetadata in a local cache.
+ *
+ * @param metadata The BluetoothLeBroadcastMetadata to be cached.
+ */
+ void cacheMetadata(BluetoothLeBroadcastMetadata metadata) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "cacheMetadata(): broadcastId "
+ + metadata.getBroadcastId()
+ + " saved in local cache.");
+ }
+ mBroadcastIdToMetadataCacheMap.put(metadata.getBroadcastId(), metadata);
+ }
+
+ /**
+ * Gets cached BluetoothLeBroadcastMetadata by broadcastId.
+ *
+ * @param broadcastId The broadcastId to look up in the cache.
+ * @return The cached BluetoothLeBroadcastMetadata or null if not found.
+ */
+ @Nullable
+ BluetoothLeBroadcastMetadata getCachedMetadata(int broadcastId) {
+ var metadata = mBroadcastIdToMetadataCacheMap.get(broadcastId);
+ if (metadata == null) {
+ Log.w(
+ TAG,
+ "getCachedMetadata(): broadcastId not found in"
+ + " mBroadcastIdToMetadataCacheMap.");
+ return null;
+ }
+ return metadata;
+ }
+
+ /**
+ * Saves metadata to SharedPreferences asynchronously.
+ *
+ * @param context The context.
+ * @param metadata The BluetoothLeBroadcastMetadata to be saved.
+ */
+ void saveMetadata(Context context, BluetoothLeBroadcastMetadata metadata) {
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+ if (sharedPref != null) {
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(
+ METADATA_KEY,
+ BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(
+ metadata));
+ editor.apply();
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "saveMetadata(): broadcastId "
+ + metadata.getBroadcastId()
+ + " metadata saved in storage.");
+ }
+ }
+ });
+ }
+
+ /**
+ * Gets saved metadata from SharedPreferences.
+ *
+ * @param context The context.
+ * @param broadcastId The broadcastId to retrieve metadata for.
+ * @return The saved BluetoothLeBroadcastMetadata or null if not found.
+ */
+ @Nullable
+ BluetoothLeBroadcastMetadata getSavedMetadata(Context context, int broadcastId) {
+ SharedPreferences sharedPref = context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+ if (sharedPref != null) {
+ String savedMetadataStr = sharedPref.getString(METADATA_KEY, null);
+ if (savedMetadataStr == null) {
+ Log.w(TAG, "getSavedMetadata(): savedMetadataStr is null");
+ return null;
+ }
+ var savedMetadata =
+ BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+ savedMetadataStr);
+ if (savedMetadata == null || savedMetadata.getBroadcastId() != broadcastId) {
+ Log.w(TAG, "getSavedMetadata(): savedMetadata doesn't match broadcast Id.");
+ return null;
+ }
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "getSavedMetadata(): broadcastId "
+ + savedMetadata.getBroadcastId()
+ + " metadata found in storage.");
+ }
+ return savedMetadata;
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
index 549e7258347..24e1ca3e349 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
@@ -16,7 +16,6 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
-import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
@@ -58,14 +57,12 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController
};
private final LocalBluetoothManager mLocalBtManager;
- private final AudioStreamsHelper mAudioStreamsHelper;
private AudioStreamsDashboardFragment mFragment;
private Preference mPreference;
public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
super(context, preferenceKey);
mLocalBtManager = Utils.getLocalBtManager(mContext);
- mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
}
public void setFragment(AudioStreamsDashboardFragment fragment) {
@@ -124,10 +121,6 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController
});
}
- void addSource(BluetoothLeBroadcastMetadata source) {
- mAudioStreamsHelper.addSource(source);
- }
-
private void updateVisibility() {
ThreadUtils.postOnBackgroundThread(
() -> {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
index 2b52039768e..378128d069e 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
@@ -229,6 +229,7 @@ public class QrCodeScanModeFragment extends InstrumentedFragment
}
mErrorMessage.setVisibility(View.INVISIBLE);
+ mTextureView.setVisibility(View.INVISIBLE);
triggerVibrationForQrCodeRecognition(getContext());
diff --git a/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java
new file mode 100644
index 00000000000..06cfe65043e
--- /dev/null
+++ b/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java
@@ -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;
+ }
+}
diff --git a/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java
index f1b81b4a2d6..2a544f263e3 100644
--- a/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java
+++ b/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java
@@ -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);
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index 73567bcf392..504eda8132b 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -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));
diff --git a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java
index c40212b7e01..c6b1bdb6ada 100644
--- a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java
+++ b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java
@@ -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 targets = ams.getAccessibilityShortcutTargets(
- AccessibilityManager.ACCESSIBILITY_BUTTON);
+ ShortcutConstants.UserShortcutType.SOFTWARE);
return !targets.isEmpty();
}
diff --git a/src/com/android/settings/network/MobileNetworkListFragment.kt b/src/com/android/settings/network/MobileNetworkListFragment.kt
index 09b1150d196..e7228666da7 100644
--- a/src/com/android/settings/network/MobileNetworkListFragment.kt
+++ b/src/com/android/settings/network/MobileNetworkListFragment.kt
@@ -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
diff --git a/src/com/android/settings/network/SubscriptionInfoListViewModel.kt b/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
index ed930d4c8db..f6820023c92 100644
--- a/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
+++ b/src/com/android/settings/network/SubscriptionInfoListViewModel.kt
@@ -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())
}
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index ac1af804549..41852e55dfe 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -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(
diff --git a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
index aafe49382db..12258068319 100644
--- a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
+++ b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt
@@ -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
diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
new file mode 100644
index 00000000000..e746d4a79a0
--- /dev/null
+++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
@@ -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 = 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().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>) =
+ combine(
+ selectableSubscriptionInfoListFlow,
+ context.defaultVoiceSubscriptionFlow(),
+ context.defaultSmsSubscriptionFlow(),
+ context.defaultDefaultDataSubscriptionFlow(),
+ NetworkCellularGroupProvider::refreshUiStates,
+ ).flowOn(Dispatchers.Default)
+
+ fun refreshUiStates(
+ inputSelectableSubscriptionInfoList: List,
+ 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,
+ defaultVoiceSubId: MutableIntState,
+ defaultSmsSubId: MutableIntState,
+ defaultDataSubId: MutableIntState,
+ nonDds: MutableIntState) {
+ val context = LocalContext.current
+ var activeSubscriptionInfoList: List =
+ 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
+) {
+ 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,
+ 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 =
+ 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 =
+ 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 =
+ 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
+ }
\ No newline at end of file
diff --git a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt
index 7704f84ec40..5752a4f4f02 100644
--- a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt
+++ b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt
@@ -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,
- selectedId: MutableIntState,
- icon: ImageVector,
- enable: Boolean
+ title: String,
+ list: List,
+ 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)
}
diff --git a/src/com/android/settings/wifi/WifiDialogActivity.java b/src/com/android/settings/wifi/WifiDialogActivity.java
index 7e901c27ba6..eb3d88a9668 100644
--- a/src/com/android/settings/wifi/WifiDialogActivity.java
+++ b/src/com/android/settings/wifi/WifiDialogActivity.java
@@ -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);
diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java
index e30759af8c1..22fec8f058e 100644
--- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java
@@ -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);
}
}
diff --git a/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java
new file mode 100644
index 00000000000..f35fb17f8ca
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java
@@ -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();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/wifi/WifiDialogActivityTest.java b/tests/robotests/src/com/android/settings/wifi/WifiDialogActivityTest.java
index ff0395db72f..d1cbd0ee1b7 100644
--- a/tests/robotests/src/com/android/settings/wifi/WifiDialogActivityTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/WifiDialogActivityTest.java
@@ -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);