Merge "Fix Java crash problem" into main
diff --git a/res/layout/keyboard_layout_picker.xml b/res/layout/keyboard_layout_picker.xml
index 6b163da..b25c228 100644
--- a/res/layout/keyboard_layout_picker.xml
+++ b/res/layout/keyboard_layout_picker.xml
@@ -20,6 +20,13 @@
android:id="@+id/keyboard_layout_picker_container"
android:orientation="vertical">
+ <ImageView
+ android:id="@+id/keyboard_layout_preview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"
+ android:scaleType="fitCenter"/>
+
<FrameLayout
android:id="@+id/keyboard_layout_title"
android:layout_width="match_parent"
diff --git a/res/xml/audio_stream_details_fragment.xml b/res/xml/audio_stream_details_fragment.xml
index 9727442..2a84939 100644
--- a/res/xml/audio_stream_details_fragment.xml
+++ b/res/xml/audio_stream_details_fragment.xml
@@ -25,10 +25,12 @@
android:layout="@layout/settings_entity_header"
android:selectable="false"
settings:allowDividerBelow="true"
- settings:searchable="false" />
+ settings:searchable="false"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController" />
<com.android.settingslib.widget.ActionButtonsPreference
android:key="audio_stream_button"
- settings:allowDividerBelow="true" />
+ settings:allowDividerBelow="true"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamButtonController" />
</PreferenceScreen>
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index e1ccad8..f0a2881 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -727,11 +727,10 @@
android:title="@string/enable_non_resizable_multi_window"
android:summary="@string/enable_non_resizable_multi_window_summary" />
- // TODO(b/315859328): Temporally removed since causing search indexing failure.
-<!-- <SwitchPreferenceCompat-->
-<!-- android:key="back_navigation_animation"-->
-<!-- android:title="@string/back_navigation_animation"-->
-<!-- android:summary="@string/back_navigation_animation_summary" />-->
+ <SwitchPreferenceCompat
+ android:key="back_navigation_animation"
+ android:title="@string/back_navigation_animation"
+ android:summary="@string/back_navigation_animation_summary" />
<Preference
android:key="reset_shortcut_manager_throttling"
diff --git a/src/com/android/settings/SettingsPreferenceFragment.java b/src/com/android/settings/SettingsPreferenceFragment.java
index d7a276e..66397c0 100644
--- a/src/com/android/settings/SettingsPreferenceFragment.java
+++ b/src/com/android/settings/SettingsPreferenceFragment.java
@@ -479,7 +479,10 @@
mDialogFragment.dismiss();
mDialogFragment = null;
}
- getListView().clearOnScrollListeners();
+ RecyclerView view = getListView();
+ if (view != null) {
+ view.clearOnScrollListeners();
+ }
}
super.onDetach();
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
index b71330a..059173c 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
@@ -290,6 +290,17 @@
}
@Override
+ protected Intent getFingerprintEnrollingIntent() {
+ final Intent ret = super.getFingerprintEnrollingIntent();
+ if (Flags.udfpsEnrollCalibration()) {
+ if (mCalibrator != null) {
+ ret.putExtras(mCalibrator.getExtrasForNextIntent(true));
+ }
+ }
+ return ret;
+ }
+
+ @Override
public void onBackPressed() {
stopLookingForFingerprint();
super.onBackPressed();
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
index bd52b64..aef3c06 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
@@ -385,7 +385,7 @@
}
if (Flags.udfpsEnrollCalibration()) {
if (mCalibrator != null) {
- intent.putExtras(mCalibrator.getExtrasForNextIntent());
+ intent.putExtras(mCalibrator.getExtrasForNextIntent(false));
}
}
return intent;
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
index 9809bcc..c54c6b5 100644
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
@@ -6,7 +6,7 @@
interface UdfpsEnrollCalibrator {
- val extrasForNextIntent: Bundle
+ fun getExtrasForNextIntent(isEnrolling: Boolean): Bundle
fun onSaveInstanceState(outState: Bundle)
diff --git a/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java b/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
index c095fee..fc3493c 100644
--- a/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
+++ b/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
@@ -17,11 +17,16 @@
import static com.android.settingslib.Utils.isAudioModeOngoingCall;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
@@ -33,36 +38,94 @@
import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
/**
- * Controller to maintain the {@link androidx.preference.PreferenceGroup} for all
- * available media devices. It uses {@link DevicePreferenceCallback}
- * to add/remove {@link Preference}
+ * Controller to maintain the {@link androidx.preference.PreferenceGroup} for all available media
+ * devices. It uses {@link DevicePreferenceCallback} to add/remove {@link Preference}
*/
public class AvailableMediaDeviceGroupController extends BasePreferenceController
implements LifecycleObserver, OnStart, OnStop, DevicePreferenceCallback, BluetoothCallback {
+ private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "AvailableMediaDeviceGroupController";
private static final String KEY = "available_device_list";
- @VisibleForTesting
- PreferenceGroup mPreferenceGroup;
- @VisibleForTesting
- LocalBluetoothManager mLocalBluetoothManager;
+ @VisibleForTesting PreferenceGroup mPreferenceGroup;
+ @VisibleForTesting LocalBluetoothManager mLocalBluetoothManager;
+ private final Executor mExecutor;
private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
private FragmentManager mFragmentManager;
+ private BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
+ new BluetoothLeBroadcastAssistant.Callback() {
+ @Override
+ public void onSearchStarted(int reason) {}
+
+ @Override
+ public void onSearchStartFailed(int reason) {}
+
+ @Override
+ public void onSearchStopped(int reason) {}
+
+ @Override
+ public void onSearchStopFailed(int reason) {}
+
+ @Override
+ public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+ @Override
+ public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
+ mBluetoothDeviceUpdater.forceUpdate();
+ }
+
+ @Override
+ public void onSourceAddFailed(
+ @NonNull BluetoothDevice sink,
+ @NonNull BluetoothLeBroadcastMetadata source,
+ int reason) {}
+
+ @Override
+ public void onSourceModified(
+ @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+ @Override
+ public void onSourceModifyFailed(
+ @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+ @Override
+ public void onSourceRemoved(
+ @NonNull BluetoothDevice sink, int sourceId, int reason) {
+ mBluetoothDeviceUpdater.forceUpdate();
+ }
+
+ @Override
+ public void onSourceRemoveFailed(
+ @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+ @Override
+ public void onReceiveStateChanged(
+ BluetoothDevice sink,
+ int sourceId,
+ BluetoothLeBroadcastReceiveState state) {}
+ };
public AvailableMediaDeviceGroupController(Context context) {
super(context, KEY);
mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+ mExecutor = Executors.newSingleThreadExecutor();
}
@Override
@@ -71,6 +134,18 @@
Log.e(TAG, "onStart() Bluetooth is not supported on this device");
return;
}
+ if (AudioSharingUtils.isFeatureEnabled()) {
+ LocalBluetoothLeBroadcastAssistant assistant =
+ mLocalBluetoothManager
+ .getProfileManager()
+ .getLeAudioBroadcastAssistantProfile();
+ if (assistant != null) {
+ if (DEBUG) {
+ Log.d(TAG, "onStart() Register callbacks for assistant.");
+ }
+ assistant.registerServiceCallBack(mExecutor, mAssistantCallback);
+ }
+ }
mBluetoothDeviceUpdater.registerCallback();
mLocalBluetoothManager.getEventManager().registerCallback(this);
mBluetoothDeviceUpdater.refreshPreference();
@@ -82,6 +157,18 @@
Log.e(TAG, "onStop() Bluetooth is not supported on this device");
return;
}
+ if (AudioSharingUtils.isFeatureEnabled()) {
+ LocalBluetoothLeBroadcastAssistant assistant =
+ mLocalBluetoothManager
+ .getProfileManager()
+ .getLeAudioBroadcastAssistantProfile();
+ if (assistant != null) {
+ if (DEBUG) {
+ Log.d(TAG, "onStop() Register callbacks for assistant.");
+ }
+ assistant.unregisterServiceCallBack(mAssistantCallback);
+ }
+ }
mBluetoothDeviceUpdater.unregisterCallback();
mLocalBluetoothManager.getEventManager().unregisterCallback(this);
}
@@ -130,8 +217,11 @@
public void init(DashboardFragment fragment) {
mFragmentManager = fragment.getParentFragmentManager();
- mBluetoothDeviceUpdater = new AvailableMediaBluetoothDeviceUpdater(fragment.getContext(),
- AvailableMediaDeviceGroupController.this, fragment.getMetricsCategory());
+ mBluetoothDeviceUpdater =
+ new AvailableMediaBluetoothDeviceUpdater(
+ fragment.getContext(),
+ AvailableMediaDeviceGroupController.this,
+ fragment.getMetricsCategory());
}
@VisibleForTesting
@@ -157,20 +247,20 @@
}
if (bluetoothProfile == BluetoothProfile.HEARING_AID) {
- HearingAidUtils.launchHearingAidPairingDialog(mFragmentManager, activeDevice,
- getMetricsCategory());
+ HearingAidUtils.launchHearingAidPairingDialog(
+ mFragmentManager, activeDevice, getMetricsCategory());
}
}
private void updateTitle() {
if (isAudioModeOngoingCall(mContext)) {
// in phone call
- mPreferenceGroup.
- setTitle(mContext.getString(R.string.connected_device_call_device_title));
+ mPreferenceGroup.setTitle(
+ mContext.getString(R.string.connected_device_call_device_title));
} else {
// without phone call
- mPreferenceGroup.
- setTitle(mContext.getString(R.string.connected_device_media_device_title));
+ mPreferenceGroup.setTitle(
+ mContext.getString(R.string.connected_device_media_device_title));
}
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java
index 9329cc29..70859c2 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java
@@ -47,6 +47,9 @@
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
@@ -260,6 +263,9 @@
return;
}
mLocalBtManager.getEventManager().registerCallback(this);
+ if (DEBUG) {
+ Log.d(TAG, "onStart() Register callbacks for broadcast and assistant.");
+ }
mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
mBluetoothDeviceUpdater.registerCallback();
@@ -281,15 +287,11 @@
return;
}
mLocalBtManager.getEventManager().unregisterCallback(this);
- // TODO: verify the reason for failing to unregister
- try {
- mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
- mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
- } catch (IllegalArgumentException e) {
- Log.e(
- TAG,
- "Fail to unregister broadcast or assistant callback due to " + e.getMessage());
+ if (DEBUG) {
+ Log.d(TAG, "onStop() Unregister callbacks for broadcast and assistant.");
}
+ mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+ mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
mBluetoothDeviceUpdater.unregisterCallback();
}
@@ -358,6 +360,28 @@
"Ignore onProfileConnectionStateChanged, no broadcast or assistant supported");
return;
}
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> handleOnProfileStateChanged(cachedDevice, bluetoothProfile));
+ }
+
+ /**
+ * Initialize the controller.
+ *
+ * @param fragment The fragment to provide the context and metrics category for {@link
+ * AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
+ */
+ public void init(DashboardFragment fragment) {
+ mFragment = fragment;
+ mBluetoothDeviceUpdater =
+ new AudioSharingBluetoothDeviceUpdater(
+ fragment.getContext(),
+ AudioSharingDevicePreferenceController.this,
+ fragment.getMetricsCategory());
+ }
+
+ private void handleOnProfileStateChanged(
+ @NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
boolean isLeAudioSupported = isLeAudioSupported(cachedDevice);
// For eligible (LE audio) remote device, we only check its connected LE audio profile.
if (isLeAudioSupported && bluetoothProfile != BluetoothProfile.LE_AUDIO) {
@@ -384,120 +408,143 @@
}
if (!isLeAudioSupported) {
// Handle connected ineligible (non LE audio) remote device
- if (isBroadcasting()) {
- // Show stop audio sharing dialog when an ineligible (non LE audio) remote device
- // connected during a sharing session.
- closeOpeningDialogs();
- AudioSharingStopDialogFragment.show(
- mFragment,
- cachedDevice.getName(),
- () -> mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId()));
- } else {
- // Do nothing for ineligible (non LE audio) remote device when no sharing session.
- if (DEBUG) {
- Log.d(
- TAG,
- "Ignore onProfileConnectionStateChanged for non LE audio without"
- + " sharing session");
- }
- }
+ handleOnProfileStateChangedForNonLeAudioDevice(cachedDevice);
} else {
- Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
- AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
// Handle connected eligible (LE audio) remote device
- if (isBroadcasting()) {
- // Show audio sharing switch or join dialog according to device count in the sharing
- // session.
- ArrayList<AudioSharingDeviceItem> deviceItemsInSharingSession =
- AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
- mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
- // Show audio sharing switch dialog when the third eligible (LE audio) remote device
- // connected during a sharing session.
- if (deviceItemsInSharingSession.size() >= 2) {
- closeOpeningDialogs();
- AudioSharingDisconnectDialogFragment.show(
- mFragment,
- deviceItemsInSharingSession,
- cachedDevice.getName(),
- (AudioSharingDeviceItem item) -> {
- // Remove all sources from the device user clicked
- for (CachedBluetoothDevice device :
- groupedDevices.get(item.getGroupId())) {
- for (BluetoothLeBroadcastReceiveState source :
- mAssistant.getAllSources(device.getDevice())) {
- mAssistant.removeSource(
- device.getDevice(), source.getSourceId());
- }
- }
- // Add current broadcast to the latest connected device
- mAssistant.addSource(
- cachedDevice.getDevice(),
- mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
- /* isGroupOp= */ true);
- });
- } else {
- // Show audio sharing join dialog when the first or second eligible (LE audio)
- // remote device connected during a sharing session.
- closeOpeningDialogs();
- AudioSharingJoinDialogFragment.show(
- mFragment,
- deviceItemsInSharingSession,
- cachedDevice.getName(),
- () -> {
- // Add current broadcast to the latest connected device
- mAssistant.addSource(
- cachedDevice.getDevice(),
- mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
- /* isGroupOp= */ true);
- });
- }
- } else {
- ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
- for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
- // Use random device in the group within the sharing session to
- // represent the group.
- CachedBluetoothDevice device = devices.get(0);
- if (device.getGroupId() == cachedDevice.getGroupId()) {
- continue;
- }
- deviceItems.add(AudioSharingUtils.buildAudioSharingDeviceItem(device));
- }
- // Show audio sharing join dialog when the second eligible (LE audio) remote device
- // connect and no sharing session.
- if (deviceItems.size() == 1) {
- closeOpeningDialogs();
- AudioSharingJoinDialogFragment.show(
- mFragment,
- deviceItems,
- cachedDevice.getName(),
- () -> {
- mTargetSinks = new ArrayList<>();
- for (List<CachedBluetoothDevice> devices :
- groupedDevices.values()) {
- for (CachedBluetoothDevice device : devices) {
- mTargetSinks.add(device.getDevice());
- }
- }
- mBroadcast.startBroadcast("test", null);
- });
- }
+ handleOnProfileStateChangedForLeAudioDevice(cachedDevice);
+ }
+ }
+
+ private void handleOnProfileStateChangedForNonLeAudioDevice(
+ @NonNull CachedBluetoothDevice cachedDevice) {
+ if (isBroadcasting()) {
+ // Show stop audio sharing dialog when an ineligible (non LE audio) remote device
+ // connected during a sharing session.
+ ThreadUtils.postOnMainThread(
+ () -> {
+ closeOpeningDialogs();
+ AudioSharingStopDialogFragment.show(
+ mFragment,
+ cachedDevice.getName(),
+ () -> mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId()));
+ });
+ } else {
+ // Do nothing for ineligible (non LE audio) remote device when no sharing session.
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Ignore onProfileConnectionStateChanged for non LE audio without"
+ + " sharing session");
}
}
}
- /**
- * Initialize the controller.
- *
- * @param fragment The fragment to provide the context and metrics category for {@link
- * AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
- */
- public void init(DashboardFragment fragment) {
- mFragment = fragment;
- mBluetoothDeviceUpdater =
- new AudioSharingBluetoothDeviceUpdater(
- fragment.getContext(),
- AudioSharingDevicePreferenceController.this,
- fragment.getMetricsCategory());
+ private void handleOnProfileStateChangedForLeAudioDevice(
+ @NonNull CachedBluetoothDevice cachedDevice) {
+ Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
+ AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
+ if (isBroadcasting()) {
+ if (groupedDevices.containsKey(cachedDevice.getGroupId())
+ && groupedDevices.get(cachedDevice.getGroupId()).stream()
+ .anyMatch(
+ device ->
+ AudioSharingUtils.hasBroadcastSource(
+ device, mLocalBtManager))) {
+ Log.d(
+ TAG,
+ "Automatically add another device within the same group to the sharing: "
+ + cachedDevice.getDevice().getAnonymizedAddress());
+ addSourceToTargetDevices(ImmutableList.of(cachedDevice.getDevice()));
+ return;
+ }
+ // Show audio sharing switch or join dialog according to device count in the sharing
+ // session.
+ ArrayList<AudioSharingDeviceItem> deviceItemsInSharingSession =
+ AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
+ mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
+ // Show audio sharing switch dialog when the third eligible (LE audio) remote device
+ // connected during a sharing session.
+ if (deviceItemsInSharingSession.size() >= 2) {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ closeOpeningDialogs();
+ AudioSharingDisconnectDialogFragment.show(
+ mFragment,
+ deviceItemsInSharingSession,
+ cachedDevice.getName(),
+ (AudioSharingDeviceItem item) -> {
+ // Remove all sources from the device user clicked
+ if (groupedDevices.containsKey(item.getGroupId())) {
+ for (CachedBluetoothDevice device :
+ groupedDevices.get(item.getGroupId())) {
+ for (BluetoothLeBroadcastReceiveState source :
+ mAssistant.getAllSources(
+ device.getDevice())) {
+ mAssistant.removeSource(
+ device.getDevice(),
+ source.getSourceId());
+ }
+ }
+ }
+ // Add current broadcast to the latest connected device
+ mAssistant.addSource(
+ cachedDevice.getDevice(),
+ mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
+ /* isGroupOp= */ true);
+ });
+ });
+ } else {
+ // Show audio sharing join dialog when the first or second eligible (LE audio)
+ // remote device connected during a sharing session.
+ ThreadUtils.postOnMainThread(
+ () -> {
+ closeOpeningDialogs();
+ AudioSharingJoinDialogFragment.show(
+ mFragment,
+ deviceItemsInSharingSession,
+ cachedDevice.getName(),
+ () -> {
+ // Add current broadcast to the latest connected device
+ mAssistant.addSource(
+ cachedDevice.getDevice(),
+ mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
+ /* isGroupOp= */ true);
+ });
+ });
+ }
+ } else {
+ ArrayList<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
+ for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
+ // Use random device in the group within the sharing session to represent the group.
+ CachedBluetoothDevice device = devices.get(0);
+ if (device.getGroupId() == cachedDevice.getGroupId()) {
+ continue;
+ }
+ deviceItems.add(AudioSharingUtils.buildAudioSharingDeviceItem(device));
+ }
+ // Show audio sharing join dialog when the second eligible (LE audio) remote
+ // device connect and no sharing session.
+ if (deviceItems.size() == 1) {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ closeOpeningDialogs();
+ AudioSharingJoinDialogFragment.show(
+ mFragment,
+ deviceItems,
+ cachedDevice.getName(),
+ () -> {
+ mTargetSinks = new ArrayList<>();
+ for (List<CachedBluetoothDevice> devices :
+ groupedDevices.values()) {
+ for (CachedBluetoothDevice device : devices) {
+ mTargetSinks.add(device.getDevice());
+ }
+ }
+ mBroadcast.startBroadcast("test", null);
+ });
+ });
+ }
+ }
}
private boolean isLeAudioSupported(CachedBluetoothDevice cachedDevice) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
index b5361f2..3d4ef82 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
@@ -41,6 +41,8 @@
import java.util.Optional;
import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
public class AudioSharingUtils {
private static final String TAG = "AudioSharingUtils";
private static final boolean DEBUG = BluetoothUtils.D;
@@ -237,7 +239,7 @@
* @return An Optional containing the active LE Audio device, or an empty Optional if not found.
*/
public static Optional<CachedBluetoothDevice> getActiveSinkOnAssistant(
- LocalBluetoothManager manager) {
+ @Nullable LocalBluetoothManager manager) {
if (manager == null) {
Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
return Optional.empty();
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
new file mode 100644
index 0000000..bb729d6
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
@@ -0,0 +1,66 @@
+/*
+ * 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.audiostreams;
+
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.widget.ActionButtonsPreference;
+
+public class AudioStreamButtonController extends BasePreferenceController
+ implements DefaultLifecycleObserver {
+ private static final String KEY = "audio_stream_button";
+ private @Nullable ActionButtonsPreference mPreference;
+ private int mBroadcastId = -1;
+
+ public AudioStreamButtonController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @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);
+ }
+ super.displayPreference(screen);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY;
+ }
+
+ /** Initialize with broadcast id */
+ void init(int broadcastId) {
+ mBroadcastId = broadcastId;
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
index 1e69829..e1dc228 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
@@ -17,16 +17,28 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
import android.content.Context;
+import android.os.Bundle;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
public class AudioStreamDetailsFragment extends DashboardFragment {
+ static final String BROADCAST_NAME_ARG = "broadcast_name";
+ static final String BROADCAST_ID_ARG = "broadcast_id";
private static final String TAG = "AudioStreamDetailsFragment";
@Override
public void onAttach(Context context) {
super.onAttach(context);
+ Bundle arguments = getArguments();
+ if (arguments != null) {
+ use(AudioStreamHeaderController.class)
+ .init(
+ this,
+ arguments.getString(BROADCAST_NAME_ARG),
+ arguments.getInt(BROADCAST_ID_ARG));
+ use(AudioStreamButtonController.class).init(arguments.getInt(BROADCAST_ID_ARG));
+ }
}
@Override
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
new file mode 100644
index 0000000..89f24bc
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
@@ -0,0 +1,85 @@
+/*
+ * 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.audiostreams;
+
+import android.content.Context;
+
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.widget.LayoutPreference;
+
+import javax.annotation.Nullable;
+
+public class AudioStreamHeaderController extends BasePreferenceController
+ implements DefaultLifecycleObserver {
+ private static final String KEY = "audio_stream_header";
+ private @Nullable EntityHeaderController mHeaderController;
+ private @Nullable DashboardFragment mFragment;
+ private String mBroadcastName = "";
+ private int mBroadcastId = -1;
+
+ public AudioStreamHeaderController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @Override
+ public final void displayPreference(PreferenceScreen screen) {
+ LayoutPreference headerPreference = screen.findPreference(KEY);
+ if (headerPreference != null && mFragment != null) {
+ mHeaderController =
+ EntityHeaderController.newInstance(
+ mFragment.getActivity(),
+ mFragment,
+ headerPreference.findViewById(R.id.entity_header));
+ if (mBroadcastName != null) {
+ mHeaderController.setLabel(mBroadcastName);
+ }
+ 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);
+ }
+ super.displayPreference(screen);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY;
+ }
+
+ /** Initialize with {@link AudioStreamDetailsFragment} and broadcast name and id */
+ void init(
+ AudioStreamDetailsFragment audioStreamDetailsFragment,
+ String broadcastName,
+ int broadcastId) {
+ mFragment = audioStreamDetailsFragment;
+ mBroadcastName = broadcastName;
+ mBroadcastId = broadcastId;
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
index 5acbc1f..198e8e5 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -80,8 +80,8 @@
});
}
- /** Removes all sources from LE broadcasts associated for all active sinks. */
- void removeSource() {
+ /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
+ void removeSource(int broadcastId) {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
return;
@@ -93,14 +93,17 @@
if (DEBUG) {
Log.d(
TAG,
- "removeSource(): remove all sources from sink : "
+ "removeSource(): remove all sources with broadcast id :"
+ + broadcastId
+ + " from sink : "
+ sink.getAddress());
}
- var sources = mLeBroadcastAssistant.getAllSources(sink);
- if (!sources.isEmpty()) {
- mLeBroadcastAssistant.removeSource(
- sink, sources.get(0).getSourceId());
- }
+ mLeBroadcastAssistant.getAllSources(sink).stream()
+ .filter(state -> state.getBroadcastId() == broadcastId)
+ .forEach(
+ state ->
+ mLeBroadcastAssistant.removeSource(
+ sink, state.getSourceId()));
}
});
}
@@ -121,6 +124,12 @@
return mLeBroadcastAssistant;
}
+ static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
+ return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
+ && state.getBigEncryptionState()
+ == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
+ }
+
private static List<BluetoothDevice> getActiveSinksOnAssistant(
@Nullable LocalBluetoothManager manager) {
if (manager == null) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index 45f0c2f..3c005b2 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -22,9 +22,9 @@
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Bundle;
-import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -42,8 +42,11 @@
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.nio.charset.StandardCharsets;
@@ -57,11 +60,22 @@
implements DefaultLifecycleObserver {
private static final String TAG = "AudioStreamsProgressCategoryController";
private static final boolean DEBUG = BluetoothUtils.D;
+ private final BluetoothCallback mBluetoothCallback =
+ new BluetoothCallback() {
+ @Override
+ public void onActiveDeviceChanged(
+ @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+ if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+ mExecutor.execute(() -> init(activeDevice != null));
+ }
+ }
+ };
private final Executor mExecutor;
private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
private final AudioStreamsHelper mAudioStreamsHelper;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+ private final @Nullable LocalBluetoothManager mBluetoothManager;
private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
new ConcurrentHashMap<>();
private AudioStreamsProgressCategoryPreference mCategoryPreference;
@@ -69,7 +83,8 @@
public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
super(context, preferenceKey);
mExecutor = Executors.newSingleThreadExecutor();
- mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(mContext));
+ mBluetoothManager = Utils.getLocalBtManager(mContext);
+ mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
}
@@ -87,48 +102,24 @@
@Override
public void onStart(@NonNull LifecycleOwner owner) {
- if (mLeBroadcastAssistant == null) {
- Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
- return;
- }
- mBroadcastIdToPreferenceMap.clear();
- if (mCategoryPreference != null) {
- mCategoryPreference.removeAll();
+ if (mBluetoothManager != null) {
+ mBluetoothManager.getEventManager().registerCallback(mBluetoothCallback);
}
mExecutor.execute(
() -> {
- mLeBroadcastAssistant.registerServiceCallBack(
- mExecutor, mBroadcastAssistantCallback);
- if (DEBUG) {
- Log.d(TAG, "scanAudioStreamsStart()");
- }
- mLeBroadcastAssistant.startSearchingForSources(emptyList());
- // Display currently connected streams
- var unused =
- ThreadUtils.postOnBackgroundThread(
- () ->
- mAudioStreamsHelper
- .getAllSources()
- .forEach(this::handleSourceConnected));
+ boolean hasActive =
+ AudioSharingUtils.getActiveSinkOnAssistant(mBluetoothManager)
+ .isPresent();
+ init(hasActive);
});
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
- if (mLeBroadcastAssistant == null) {
- Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
- return;
+ if (mBluetoothManager != null) {
+ mBluetoothManager.getEventManager().unregisterCallback(mBluetoothCallback);
}
- mExecutor.execute(
- () -> {
- if (mLeBroadcastAssistant.isSearchInProgress()) {
- if (DEBUG) {
- Log.d(TAG, "scanAudioStreamsStop()");
- }
- mLeBroadcastAssistant.stopSearchingForSources();
- }
- mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
- });
+ mExecutor.execute(this::stopScanning);
}
void setScanning(boolean isScanning) {
@@ -142,7 +133,10 @@
Preference.OnPreferenceClickListener addSourceOrShowDialog =
preference -> {
if (DEBUG) {
- Log.d(TAG, "preferenceClicked(): attempt to join broadcast");
+ Log.d(
+ TAG,
+ "preferenceClicked(): attempt to join broadcast id : "
+ + source.getBroadcastId());
}
if (source.isEncrypted()) {
ThreadUtils.postOnMainThread(
@@ -177,11 +171,13 @@
}
});
}
- mAudioStreamsHelper.removeSource();
+ mAudioStreamsHelper.removeSource(broadcastId);
}
void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
- // TODO(chelseahao): only continue when the state indicates a successful connection
+ if (!AudioStreamsHelper.isConnected(state)) {
+ return;
+ }
mBroadcastIdToPreferenceMap.compute(
state.getBroadcastId(),
(k, v) -> {
@@ -194,7 +190,7 @@
ThreadUtils.postOnMainThread(
() -> {
preference.setIsConnected(
- true, p -> launchDetailFragment((AudioStreamPreference) p));
+ true, p -> launchDetailFragment(state.getBroadcastId()));
if (mCategoryPreference != null && !existed) {
mCategoryPreference.addPreference(preference);
}
@@ -208,11 +204,73 @@
AudioSharingUtils.toastMessage(mContext, msg);
}
- private boolean launchDetailFragment(AudioStreamPreference preference) {
+ private void init(boolean hasActive) {
+ mBroadcastIdToPreferenceMap.clear();
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mCategoryPreference != null) {
+ mCategoryPreference.removeAll();
+ mCategoryPreference.setVisible(hasActive);
+ }
+ });
+ if (hasActive) {
+ startScanning();
+ } else {
+ stopScanning();
+ }
+ }
+
+ private void startScanning() {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "startScanning(): LeBroadcastAssistant is null!");
+ return;
+ }
+ if (mLeBroadcastAssistant.isSearchInProgress()) {
+ showToast("Failed to start scanning, please try again.");
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "startScanning()");
+ }
+ mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+ mLeBroadcastAssistant.startSearchingForSources(emptyList());
+
+ // Display currently connected streams
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () ->
+ mAudioStreamsHelper
+ .getAllSources()
+ .forEach(this::handleSourceConnected));
+ }
+
+ private void stopScanning() {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "stopScanning(): LeBroadcastAssistant is null!");
+ return;
+ }
+ if (mLeBroadcastAssistant.isSearchInProgress()) {
+ if (DEBUG) {
+ Log.d(TAG, "stopScanning()");
+ }
+ mLeBroadcastAssistant.stopSearchingForSources();
+ }
+ mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+ }
+
+ 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(
- Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO,
- (String) preference.getTitle());
+ AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) preference.getTitle());
+ broadcast.putInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG, broadcastId);
new SubSettingLauncher(mContext)
.setTitleText("Audio stream details")
@@ -240,8 +298,8 @@
(dialog, which) -> {
var code =
((EditText)
- layout.requireViewById(
- R.id.broadcast_edit_text))
+ layout.requireViewById(
+ R.id.broadcast_edit_text))
.getText()
.toString();
mAudioStreamsHelper.addSource(
diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
index 0348e11..8279588 100644
--- a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
+++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
@@ -41,6 +41,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
@@ -357,6 +359,11 @@
return fragment;
}
+ @VisibleForTesting
+ void setViewPager(ViewPager2 viewPager) {
+ mViewPager = viewPager;
+ }
+
interface FragmentConstructor {
Fragment constructAndGetFragment();
}
@@ -376,6 +383,15 @@
mChildFragments = fragment.getFragments();
}
+ @VisibleForTesting
+ ViewPagerAdapter(
+ @NonNull FragmentManager fragmentManager,
+ @NonNull Lifecycle lifecycle,
+ ProfileSelectFragment profileSelectFragment) {
+ super(fragmentManager, lifecycle);
+ mChildFragments = profileSelectFragment.getFragments();
+ }
+
@Override
public Fragment createFragment(int position) {
return mChildFragments[position];
@@ -386,7 +402,8 @@
return mChildFragments.length;
}
- private int getTabForPosition(int position) {
+ @VisibleForTesting
+ int getTabForPosition(int position) {
if (position >= mChildFragments.length) {
Log.e(TAG, "tab requested for out of bound position " + position);
return PERSONAL_TAB;
diff --git a/src/com/android/settings/development/BackAnimationPreferenceController.java b/src/com/android/settings/development/BackAnimationPreferenceController.java
index 95ffc25..8c731a4 100644
--- a/src/com/android/settings/development/BackAnimationPreferenceController.java
+++ b/src/com/android/settings/development/BackAnimationPreferenceController.java
@@ -18,6 +18,7 @@
import static com.android.window.flags.Flags.predictiveBackSystemAnimations;
+import android.annotation.Nullable;
import android.content.Context;
import android.provider.Settings;
@@ -28,8 +29,6 @@
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.development.DeveloperOptionsPreferenceController;
-import java.util.Objects;
-
/**
* PreferenceController for enabling/disabling animation related to back button and back gestures.
*/
@@ -51,9 +50,8 @@
public BackAnimationPreferenceController(Context context,
- DevelopmentSettingsDashboardFragment fragment) {
+ @Nullable DevelopmentSettingsDashboardFragment fragment) {
super(context);
- Objects.requireNonNull(fragment);
mFragment = fragment;
}
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index a483f9f..b52409d 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -737,7 +737,7 @@
controllers.add(new OverlaySettingsPreferenceController(context));
controllers.add(new StylusHandwritingPreferenceController(context));
controllers.add(new IngressRateLimitPreferenceController((context)));
- // controllers.add(new BackAnimationPreferenceController(context, fragment));
+ controllers.add(new BackAnimationPreferenceController(context, fragment));
controllers.add(new PhantomProcessPreferenceController(context));
controllers.add(new ContrastPreferenceController(
context, context.getSystemService(UiModeManager.class)));
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
index 11740ec..e934964 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerContent.java
@@ -27,6 +27,20 @@
public class NewKeyboardLayoutPickerContent extends DashboardFragment {
private static final String TAG = "KeyboardLayoutPicker";
+ private NewKeyboardLayoutPickerController mNewKeyboardLayoutPickerController;
+ private ControllerUpdateCallback mControllerUpdateCallback;
+
+ public interface ControllerUpdateCallback {
+ /**
+ * Called when mNewKeyBoardLayoutPickerController been initialized.
+ */
+ void onControllerUpdated(NewKeyboardLayoutPickerController
+ newKeyboardLayoutPickerController);
+ }
+
+ public void setControllerUpdateCallback(ControllerUpdateCallback controllerUpdateCallback) {
+ this.mControllerUpdateCallback = controllerUpdateCallback;
+ }
@Override
public void onAttach(Context context) {
@@ -40,7 +54,11 @@
getActivity().finish();
return;
}
- use(NewKeyboardLayoutPickerController.class).initialize(this);
+ mNewKeyboardLayoutPickerController = use(NewKeyboardLayoutPickerController.class);
+ mNewKeyboardLayoutPickerController.initialize(this);
+ if (mControllerUpdateCallback != null) {
+ mControllerUpdateCallback.onControllerUpdated(mNewKeyboardLayoutPickerController);
+ }
}
@Override
@@ -56,4 +74,8 @@
protected int getPreferenceScreenResId() {
return R.xml.new_keyboard_layout_picker_fragment;
}
+
+ public NewKeyboardLayoutPickerController getController() {
+ return mNewKeyboardLayoutPickerController;
+ }
}
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
index 9545276..ac8037f 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerController.java
@@ -59,6 +59,7 @@
private String mFinalSelectedLayout;
private String mLayout;
private MetricsFeatureProvider mMetricsFeatureProvider;
+ private KeyboardLayoutSelectedCallback mKeyboardLayoutSelectedCallback;
public NewKeyboardLayoutPickerController(Context context, String key) {
super(context, key);
@@ -100,7 +101,7 @@
@Override
public void onStop() {
- if (!mLayout.equals(mFinalSelectedLayout)) {
+ if (mLayout != null && !mLayout.equals(mFinalSelectedLayout)) {
String change = "From:" + mLayout + ", to:" + mFinalSelectedLayout;
mMetricsFeatureProvider.action(
mContext, SettingsEnums.ACTION_PK_LAYOUT_CHANGED, change);
@@ -121,6 +122,14 @@
return AVAILABLE;
}
+ /**
+ * Registers {@link KeyboardLayoutSelectedCallback} and get updated.
+ */
+ public void registerKeyboardSelectedCallback(KeyboardLayoutSelectedCallback
+ keyboardLayoutSelectedCallback) {
+ this.mKeyboardLayoutSelectedCallback = keyboardLayoutSelectedCallback;
+ }
+
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (!(preference instanceof TickButtonPreference)) {
@@ -128,6 +137,9 @@
}
final TickButtonPreference pref = (TickButtonPreference) preference;
+ if (mKeyboardLayoutSelectedCallback != null && mPreferenceMap.containsKey(preference)) {
+ mKeyboardLayoutSelectedCallback.onSelected(mPreferenceMap.get(preference));
+ }
pref.setSelected(true);
if (mPreviousSelection != null && !mPreviousSelection.equals(preference.getKey())) {
TickButtonPreference preSelectedPref = mScreen.findPreference(mPreviousSelection);
@@ -166,6 +178,9 @@
pref.setTitle(layout.getLabel());
if (mLayout.equals(layout.getLabel())) {
+ if (mKeyboardLayoutSelectedCallback != null) {
+ mKeyboardLayoutSelectedCallback.onSelected(layout);
+ }
pref.setSelected(true);
mPreviousSelection = layout.getDescriptor();
}
@@ -200,4 +215,11 @@
}
return label;
}
+
+ public interface KeyboardLayoutSelectedCallback {
+ /**
+ * Called when KeyboardLayout been selected.
+ */
+ void onSelected(KeyboardLayout keyboardLayout);
+ }
}
diff --git a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerFragment.java b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerFragment.java
index 88cacd2..f583971 100644
--- a/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerFragment.java
+++ b/src/com/android/settings/inputmethod/NewKeyboardLayoutPickerFragment.java
@@ -16,35 +16,75 @@
package com.android.settings.inputmethod;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.graphics.drawable.Drawable;
+import android.hardware.input.InputManager;
+import android.hardware.input.KeyboardLayout;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ImageView;
import androidx.fragment.app.Fragment;
import com.android.settings.R;
+//TODO: b/316243168 - [Physical Keyboard Setting] Refactor NewKeyboardLayoutPickerFragment
public class NewKeyboardLayoutPickerFragment extends Fragment {
+ private static final int DEFAULT_KEYBOARD_PREVIEW_WIDTH = 1630;
+ private static final int DEFAULT_KEYBOARD_PREVIEW_HEIGHT = 540;
+
+ private ImageView mKeyboardLayoutPreview;
+ private InputManager mInputManager;
+ private final NewKeyboardLayoutPickerController.KeyboardLayoutSelectedCallback
+ mKeyboardLayoutSelectedCallback =
+ new NewKeyboardLayoutPickerController.KeyboardLayoutSelectedCallback() {
+ @Override
+ public void onSelected(KeyboardLayout keyboardLayout) {
+ if (mInputManager != null && mKeyboardLayoutPreview != null) {
+ Drawable previewDrawable = mInputManager.getKeyboardLayoutPreview(
+ keyboardLayout,
+ DEFAULT_KEYBOARD_PREVIEW_WIDTH, DEFAULT_KEYBOARD_PREVIEW_HEIGHT);
+ mKeyboardLayoutPreview.setVisibility(
+ previewDrawable == null ? GONE : VISIBLE);
+ if (previewDrawable != null) {
+ mKeyboardLayoutPreview.setImageDrawable(previewDrawable);
+ }
+ }
+ }
+ };
+
+ private final NewKeyboardLayoutPickerContent.ControllerUpdateCallback
+ mControllerUpdateCallback =
+ newKeyboardLayoutPickerController -> {
+ if (newKeyboardLayoutPickerController != null) {
+ newKeyboardLayoutPickerController.registerKeyboardSelectedCallback(
+ mKeyboardLayoutSelectedCallback);
+ }
+ };
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
-
+ mInputManager = requireContext().getSystemService(InputManager.class);
ViewGroup fragmentView = (ViewGroup) inflater.inflate(
R.layout.keyboard_layout_picker, container, false);
+ mKeyboardLayoutPreview = fragmentView.findViewById(R.id.keyboard_layout_preview);
getActivity().getSupportFragmentManager()
.beginTransaction()
.replace(R.id.keyboard_layout_title, new NewKeyboardLayoutPickerTitle())
.commit();
NewKeyboardLayoutPickerContent fragment = new NewKeyboardLayoutPickerContent();
+ fragment.setControllerUpdateCallback(mControllerUpdateCallback);
fragment.setArguments(getArguments());
getActivity().getSupportFragmentManager()
.beginTransaction()
.replace(R.id.keyboard_layouts, fragment)
.commit();
-
return fragmentView;
}
}
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index bb19c59..52066a1 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -39,7 +39,6 @@
import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeSelectedOptionsState
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.LocalNavController
-import com.android.settingslib.spa.framework.compose.OnBackEffect
import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuBox
import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox
import com.android.settingslib.spa.widget.editor.SettingsOutlinedTextField
@@ -98,29 +97,35 @@
val networkTypeSelectedOptionsState = remember {
getNetworkTypeSelectedOptionsState(apnData.networkType)
}
- OnBackEffect{
- validateAndSaveApnData(
- apnDataInit,
- apnData,
- context,
- uriInit,
- networkTypeSelectedOptionsState
- )
- }
val navController = LocalNavController.current
RegularScaffold(
- title = if(apnDataInit.newApn) stringResource(id = R.string.apn_add) else stringResource(id = R.string.apn_edit),
+ title = if (apnDataInit.newApn) stringResource(id = R.string.apn_add) else stringResource(id = R.string.apn_edit),
+ actions = {
+ IconButton(onClick = {
+ if (!apnData.validEnabled) apnData = apnData.copy(validEnabled = true)
+ val valid = validateAndSaveApnData(
+ apnDataInit,
+ apnData,
+ context,
+ uriInit,
+ networkTypeSelectedOptionsState
+ )
+ if (valid) navController.navigateBack()
+ }) { Icon(imageVector = Icons.Outlined.Done, contentDescription = null) }
+ },
) {
Column {
SettingsOutlinedTextField(
value = apnData.name,
label = stringResource(R.string.apn_name),
- enabled = apnData.nameEnabled
+ enabled = apnData.nameEnabled,
+ errorMessage = validateName(apnData.validEnabled, apnData.name, context)
) { apnData = apnData.copy(name = it) }
SettingsOutlinedTextField(
value = apnData.apn,
label = stringResource(R.string.apn_apn),
- enabled = apnData.apnEnabled
+ enabled = apnData.apnEnabled,
+ errorMessage = validateAPN(apnData.validEnabled, apnData.apn, context)
) { apnData = apnData.copy(apn = it) }
SettingsOutlinedTextField(
value = apnData.proxy,
@@ -150,7 +155,7 @@
SettingsOutlinedTextField(
value = apnData.mmsc,
label = stringResource(R.string.apn_mmsc),
- errorMessage = validateMMSC(apnData.mmsc, context),
+ errorMessage = validateMMSC(apnData.validEnabled, apnData.mmsc, context),
enabled = apnData.mmscEnabled
) { apnData = apnData.copy(mmsc = it) }
SettingsOutlinedTextField(
@@ -172,7 +177,11 @@
SettingsOutlinedTextField(
value = apnData.apnType,
label = stringResource(R.string.apn_type),
- enabled = apnData.apnTypeEnabled
+ enabled = apnData.apnTypeEnabled,
+ errorMessage = validateAPNType(
+ apnData.validEnabled, apnData.apnType,
+ apnData.customizedConfig.readOnlyApnTypes, context
+ )
) { apnData = apnData.copy(apnType = updateApnType(apnData.copy(apnType = it))) }
SettingsExposedDropdownMenuBox(
label = stringResource(R.string.apn_protocol),
@@ -209,7 +218,6 @@
override val title = stringResource(R.string.menu_delete)
override val onClick = {
deleteApn(uriInit, context)
- apnData = apnData.copy(saveEnabled = false)
navController.navigateBack()
}
}
diff --git a/src/com/android/settings/network/apn/ApnStatus.kt b/src/com/android/settings/network/apn/ApnStatus.kt
index 668ea9b..38c6684 100644
--- a/src/com/android/settings/network/apn/ApnStatus.kt
+++ b/src/com/android/settings/network/apn/ApnStatus.kt
@@ -69,7 +69,7 @@
val networkTypeEnabled: Boolean = true,
val newApn: Boolean = false,
val subId: Int = -1,
- val saveEnabled: Boolean = true,
+ val validEnabled: Boolean = false,
val customizedConfig: CustomizedConfig = CustomizedConfig()
) {
fun getContentValues(context: Context): ContentValues {
@@ -96,7 +96,8 @@
values.put(Telephony.Carriers.EDITED_STATUS, Telephony.Carriers.USER_EDITED)
if (newApn) {
val simCarrierId =
- context.getSystemService(TelephonyManager::class.java)!!.createForSubscriptionId(subId)
+ context.getSystemService(TelephonyManager::class.java)!!
+ .createForSubscriptionId(subId)
.getSimCarrierId()
values.put(Telephony.Carriers.CARRIER_ID, simCarrierId)
}
@@ -231,22 +232,12 @@
uriInit: Uri,
networkTypeSelectedOptionsState: SnapshotStateList<Int>
): Boolean {
- // Can not be saved
- if (!apnData.saveEnabled) {
- return false
- }
// Nothing to do if it's a read only APN
if (apnData.customizedConfig.readOnlyApn) {
return true
}
- var errorMsg = validateApnData(apnData, context)
+ val errorMsg = validateApnData(apnData, context)
if (errorMsg != null) {
- //TODO: showError(this)
- return false
- }
- errorMsg = validateMMSC(apnData.mmsc, context)
- if (errorMsg != null) {
- //TODO: showError(this)
return false
}
val newApnData = apnData.copy(networkType = getNetworkType(networkTypeSelectedOptionsState))
@@ -268,37 +259,23 @@
* @return An error message if the apn data is invalid, otherwise return null.
*/
fun validateApnData(apnData: ApnData, context: Context): String? {
- var errorMsg: String? = null
+ var errorMsg: String?
val name = apnData.name
val apn = apnData.apn
- if (name == "") {
- errorMsg = context.resources.getString(R.string.error_name_empty)
+ errorMsg = if (name == "") {
+ context.resources.getString(R.string.error_name_empty)
} else if (apn == "") {
- errorMsg = context.resources.getString(R.string.error_apn_empty)
+ context.resources.getString(R.string.error_apn_empty)
+ } else {
+ validateMMSC(apnData.validEnabled, apnData.mmsc, context)
}
if (errorMsg == null) {
- // if carrier does not allow editing certain apn types, make sure type does not include
- // those
- if (!ArrayUtils.isEmpty(apnData.customizedConfig.readOnlyApnTypes)
- && apnTypesMatch(
- apnData.customizedConfig.readOnlyApnTypes,
- getUserEnteredApnType(apnData.apnType, apnData.customizedConfig.readOnlyApnTypes)
- )
- ) {
- val stringBuilder = StringBuilder()
- for (type in apnData.customizedConfig.readOnlyApnTypes) {
- stringBuilder.append(type).append(", ")
- Log.d(TAG, "validateApnData: appending type: $type")
- }
- // remove last ", "
- if (stringBuilder.length >= 2) {
- stringBuilder.delete(stringBuilder.length - 2, stringBuilder.length)
- }
- errorMsg = String.format(
- context.resources.getString(R.string.error_adding_apn_type),
- stringBuilder
- )
- }
+ errorMsg = validateAPNType(
+ apnData.validEnabled,
+ apnData.apnType,
+ apnData.customizedConfig.readOnlyApnTypes,
+ context
+ )
}
return errorMsg
}
@@ -536,7 +513,39 @@
contentResolver.delete(uri, null, null)
}
-fun validateMMSC(mmsc: String, context: Context): String? {
- return if (mmsc.matches(Regex("^https?:\\/\\/.+"))) null
- else context.resources.getString(R.string.error_mmsc_valid)
+fun validateMMSC(validEnabled: Boolean, mmsc: String, context: Context): String? {
+ return if (validEnabled && !mmsc.matches(Regex("^https?:\\/\\/.+")))
+ context.resources.getString(R.string.error_mmsc_valid)
+ else null
+}
+
+fun validateName(validEnabled: Boolean, name: String, context: Context): String? {
+ return if (validEnabled && (name == "")) context.resources.getString(R.string.error_name_empty)
+ else null
+}
+
+fun validateAPN(validEnabled: Boolean, apn: String, context: Context): String? {
+ return if (validEnabled && (apn == "")) context.resources.getString(R.string.error_apn_empty)
+ else null
+}
+
+fun validateAPNType(
+ validEnabled: Boolean,
+ apnType: String,
+ readOnlyApnTypes: List<String>,
+ context: Context
+): String? {
+ // if carrier does not allow editing certain apn types, make sure type does not include those
+ if (validEnabled && !ArrayUtils.isEmpty(readOnlyApnTypes)
+ && apnTypesMatch(
+ readOnlyApnTypes,
+ getUserEnteredApnType(apnType, readOnlyApnTypes)
+ )
+ ) {
+ return String.format(
+ context.resources.getString(R.string.error_adding_apn_type),
+ readOnlyApnTypes.joinToString(", ")
+ )
+ }
+ return null
}
\ No newline at end of file
diff --git a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
index b5e76920..b8f140f 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceMaintainer.java
@@ -18,6 +18,7 @@
import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
import static android.provider.Settings.Secure.HIDE_PRIVATESPACE_ENTRY_POINT;
+import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
import android.app.ActivityManager;
import android.app.IActivityManager;
@@ -96,6 +97,7 @@
IActivityManager am = ActivityManager.getService();
try {
+ //TODO(b/313926659): To check and handle failure of startProfile
am.startProfile(mUserHandle.getIdentifier());
} catch (RemoteException e) {
Log.e(TAG, "Failed to start private profile");
@@ -104,6 +106,7 @@
Log.i(TAG, "Private space created with id: " + mUserHandle.getIdentifier());
resetPrivateSpaceSettings();
+ setUserSetupComplete();
}
return true;
}
@@ -250,4 +253,14 @@
private void resetPrivateSpaceSettings() {
setHidePrivateSpaceEntryPointSetting(HIDE_PRIVATE_SPACE_ENTRY_POINT_DISABLED_VAL);
}
+
+ /**
+ * Sets the USER_SETUP_COMPLETE for private profile on which device theme is applied to the
+ * profile.
+ */
+ @GuardedBy("this")
+ private void setUserSetupComplete() {
+ Settings.Secure.putIntForUser(mContext.getContentResolver(), USER_SETUP_COMPLETE,
+ 1, mUserHandle.getIdentifier());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
index 06dd42b..e5964d0 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
@@ -17,20 +17,29 @@
import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE;
import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioManager;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
@@ -42,6 +51,7 @@
import com.android.settings.R;
import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
import com.android.settings.bluetooth.Utils;
+import com.android.settings.flags.Flags;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowAudioManager;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
@@ -51,9 +61,12 @@
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.HearingAidInfo;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
@@ -63,42 +76,46 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.Executor;
/** Tests for {@link AvailableMediaDeviceGroupController}. */
@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {
- ShadowAudioManager.class,
- ShadowBluetoothAdapter.class,
- ShadowBluetoothUtils.class,
- ShadowAlertDialogCompat.class,
-})
+@Config(
+ shadows = {
+ ShadowAudioManager.class,
+ ShadowBluetoothAdapter.class,
+ ShadowBluetoothUtils.class,
+ ShadowAlertDialogCompat.class,
+ })
public class AvailableMediaDeviceGroupControllerTest {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
private static final String PREFERENCE_KEY_1 = "pref_key_1";
- @Mock
- private AvailableMediaBluetoothDeviceUpdater mAvailableMediaBluetoothDeviceUpdater;
- @Mock
- private PreferenceScreen mPreferenceScreen;
+ @Mock private AvailableMediaBluetoothDeviceUpdater mAvailableMediaBluetoothDeviceUpdater;
+ @Mock private PreferenceScreen mPreferenceScreen;
+
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private PreferenceManager mPreferenceManager;
- @Mock
- private PackageManager mPackageManager;
- @Mock
- private BluetoothEventManager mEventManager;
- @Mock
- private LocalBluetoothManager mLocalBluetoothManager;
- @Mock
- private CachedBluetoothDeviceManager mCachedDeviceManager;
- @Mock
- private CachedBluetoothDevice mCachedBluetoothDevice;
+
+ @Mock private PackageManager mPackageManager;
+ @Mock private BluetoothEventManager mEventManager;
+ @Mock private LocalBluetoothManager mLocalBluetoothManager;
+ @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
+ @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
+ @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+ @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
private PreferenceGroup mPreferenceGroup;
private Context mContext;
private Preference mPreference;
private AvailableMediaDeviceGroupController mAvailableMediaDeviceGroupController;
private AudioManager mAudioManager;
+ private ShadowBluetoothAdapter mShadowBluetoothAdapter;
@Before
public void setUp() {
@@ -113,19 +130,26 @@
doReturn(mPackageManager).when(mContext).getPackageManager();
doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
+ mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ mShadowBluetoothAdapter.setEnabled(true);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
mAudioManager = mContext.getSystemService(AudioManager.class);
doReturn(mEventManager).when(mLocalBluetoothManager).getEventManager();
+ when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
- when(mCachedDeviceManager.findDevice(any(BluetoothDevice.class))).thenReturn(
- mCachedBluetoothDevice);
+ when(mCachedDeviceManager.findDevice(any(BluetoothDevice.class)))
+ .thenReturn(mCachedBluetoothDevice);
when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS);
- mAvailableMediaDeviceGroupController = spy(
- new AvailableMediaDeviceGroupController(mContext));
- mAvailableMediaDeviceGroupController.
- setBluetoothDeviceUpdater(mAvailableMediaBluetoothDeviceUpdater);
+ mAvailableMediaDeviceGroupController =
+ spy(new AvailableMediaDeviceGroupController(mContext));
+ mAvailableMediaDeviceGroupController.setBluetoothDeviceUpdater(
+ mAvailableMediaBluetoothDeviceUpdater);
mAvailableMediaDeviceGroupController.setFragmentManager(
mActivity.getSupportFragmentManager());
mAvailableMediaDeviceGroupController.mPreferenceGroup = mPreferenceGroup;
@@ -176,34 +200,55 @@
mAvailableMediaDeviceGroupController.onStart();
verify(mAvailableMediaBluetoothDeviceUpdater).registerCallback();
- verify(mLocalBluetoothManager.getEventManager()).registerCallback(
- any(BluetoothCallback.class));
+ verify(mLocalBluetoothManager.getEventManager())
+ .registerCallback(any(BluetoothCallback.class));
verify(mAvailableMediaBluetoothDeviceUpdater).refreshPreference();
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void testRegister_audioSharingOn() {
+ setUpBroadcast();
+ // register the callback in onStart()
+ mAvailableMediaDeviceGroupController.onStart();
+ verify(mAssistant)
+ .registerServiceCallBack(
+ any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+ }
+
+ @Test
public void testUnregister() {
// unregister the callback in onStop()
mAvailableMediaDeviceGroupController.onStop();
verify(mAvailableMediaBluetoothDeviceUpdater).unregisterCallback();
- verify(mLocalBluetoothManager.getEventManager()).unregisterCallback(
- any(BluetoothCallback.class));
+ verify(mLocalBluetoothManager.getEventManager())
+ .unregisterCallback(any(BluetoothCallback.class));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+ public void testUnregister_audioSharingOn() {
+ setUpBroadcast();
+ // unregister the callback in onStop()
+ mAvailableMediaDeviceGroupController.onStop();
+ verify(mAssistant)
+ .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
}
@Test
public void testGetAvailabilityStatus_noBluetoothFeature_returnUnSupported() {
doReturn(false).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
- assertThat(mAvailableMediaDeviceGroupController.getAvailabilityStatus()).isEqualTo(
- UNSUPPORTED_ON_DEVICE);
+ assertThat(mAvailableMediaDeviceGroupController.getAvailabilityStatus())
+ .isEqualTo(UNSUPPORTED_ON_DEVICE);
}
@Test
public void testGetAvailabilityStatus_BluetoothFeature_returnSupported() {
doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
- assertThat(mAvailableMediaDeviceGroupController.getAvailabilityStatus()).isEqualTo(
- AVAILABLE_UNSEARCHABLE);
+ assertThat(mAvailableMediaDeviceGroupController.getAvailabilityStatus())
+ .isEqualTo(AVAILABLE_UNSEARCHABLE);
}
@Test
@@ -211,8 +256,8 @@
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
mAvailableMediaDeviceGroupController.onAudioModeChanged();
- assertThat(mPreferenceGroup.getTitle()).isEqualTo(
- mContext.getText(R.string.connected_device_call_device_title));
+ assertThat(mPreferenceGroup.getTitle())
+ .isEqualTo(mContext.getText(R.string.connected_device_call_device_title));
}
@Test
@@ -220,8 +265,8 @@
mAudioManager.setMode(AudioManager.MODE_NORMAL);
mAvailableMediaDeviceGroupController.onAudioModeChanged();
- assertThat(mPreferenceGroup.getTitle()).isEqualTo(
- mContext.getText(R.string.connected_device_media_device_title));
+ assertThat(mPreferenceGroup.getTitle())
+ .isEqualTo(mContext.getText(R.string.connected_device_media_device_title));
}
@Test
@@ -243,16 +288,31 @@
@Test
public void onActiveDeviceChanged_hearingAidProfile_launchHearingAidPairingDialog() {
when(mCachedBluetoothDevice.isConnectedAshaHearingAidDevice()).thenReturn(true);
- when(mCachedBluetoothDevice.getDeviceMode()).thenReturn(
- HearingAidInfo.DeviceMode.MODE_BINAURAL);
- when(mCachedBluetoothDevice.getDeviceSide()).thenReturn(
- HearingAidInfo.DeviceSide.SIDE_LEFT);
+ when(mCachedBluetoothDevice.getDeviceMode())
+ .thenReturn(HearingAidInfo.DeviceMode.MODE_BINAURAL);
+ when(mCachedBluetoothDevice.getDeviceSide())
+ .thenReturn(HearingAidInfo.DeviceSide.SIDE_LEFT);
- mAvailableMediaDeviceGroupController.onActiveDeviceChanged(mCachedBluetoothDevice,
- BluetoothProfile.HEARING_AID);
+ mAvailableMediaDeviceGroupController.onActiveDeviceChanged(
+ mCachedBluetoothDevice, BluetoothProfile.HEARING_AID);
shadowMainLooper().idle();
final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog.isShowing()).isTrue();
}
+
+ private void setUpBroadcast() {
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+ doNothing()
+ .when(mAssistant)
+ .registerServiceCallBack(
+ any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+ doNothing()
+ .when(mAssistant)
+ .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+ }
}
diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
index 3df6449..17e0d1c 100644
--- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
@@ -39,17 +39,24 @@
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
+import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.Lifecycle;
+import androidx.viewpager2.widget.ViewPager2;
import com.android.settings.SettingsActivity;
import com.android.settings.SettingsPreferenceFragmentTest;
+import com.android.settings.dashboard.profileselector.ProfileSelectFragment.ViewPagerAdapter;
import com.android.settings.testutils.shadow.ShadowUserManager;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
@@ -75,6 +82,9 @@
private TestProfileSelectFragment mFragment;
private FragmentActivity mActivity;
private ShadowUserManager mUserManager;
+ @Mock private FragmentManager mFragmentManager;
+ @Mock private Lifecycle mLifecycle;
+ @Mock private FragmentTransaction mFragmentTransaction;
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
@@ -97,6 +107,16 @@
public void getTabId_setArgumentWork_setCorrectTab() {
final Bundle bundle = new Bundle();
bundle.putInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, WORK_TAB);
+ ViewPager2 viewPager = new ViewPager2(mContext);
+ TestProfileSelectFragment profileSelectFragment = new TestProfileSelectFragment();
+ ViewPagerAdapter viewPagerAdapter =
+ new TestViewPagerAdapter(mFragmentManager, mLifecycle, profileSelectFragment);
+
+ when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
+ viewPager.setAdapter(viewPagerAdapter);
+ mFragment.setViewPager(viewPager);
+ profileSelectFragment.setViewPager(viewPager);
+ mFragmentManager.beginTransaction().add(profileSelectFragment, "tag");
assertThat(mFragment.getTabId(mActivity, bundle)).isEqualTo(WORK_TAB);
}
@@ -105,6 +125,16 @@
public void getTabId_setArgumentPrivate_setCorrectTab() {
final Bundle bundle = new Bundle();
bundle.putInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, PRIVATE_TAB);
+ ViewPager2 viewPager = new ViewPager2(mContext);
+ TestProfileSelectFragment profileSelectFragment = new TestProfileSelectFragment();
+ ViewPagerAdapter viewPagerAdapter =
+ new TestViewPagerAdapter(mFragmentManager, mLifecycle, profileSelectFragment);
+
+ when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
+ viewPager.setAdapter(viewPagerAdapter);
+ mFragment.setViewPager(viewPager);
+ profileSelectFragment.setViewPager(viewPager);
+ mFragmentManager.beginTransaction().add(profileSelectFragment, "tag");
assertThat(mFragment.getTabId(mActivity, bundle)).isEqualTo(PRIVATE_TAB);
}
@@ -113,6 +143,16 @@
public void getTabId_setArgumentPersonal_setCorrectTab() {
final Bundle bundle = new Bundle();
bundle.putInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, PERSONAL_TAB);
+ ViewPager2 viewPager = new ViewPager2(mContext);
+ TestProfileSelectFragment profileSelectFragment = new TestProfileSelectFragment();
+ ViewPagerAdapter viewPagerAdapter =
+ new TestViewPagerAdapter(mFragmentManager, mLifecycle, profileSelectFragment);
+
+ when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
+ viewPager.setAdapter(viewPagerAdapter);
+ mFragment.setViewPager(viewPager);
+ profileSelectFragment.setViewPager(viewPager);
+ mFragmentManager.beginTransaction().add(profileSelectFragment, "tag");
assertThat(mFragment.getTabId(mActivity, bundle)).isEqualTo(PERSONAL_TAB);
}
@@ -310,4 +350,18 @@
};
}
}
+
+ static class TestViewPagerAdapter extends ViewPagerAdapter {
+ TestViewPagerAdapter(
+ @NonNull FragmentManager fragmentManager,
+ @NonNull Lifecycle lifecycle,
+ ProfileSelectFragment profileSelectFragment) {
+ super(fragmentManager, lifecycle, profileSelectFragment);
+ }
+
+ @Override
+ int getTabForPosition(int position) {
+ return position;
+ }
+ }
}
diff --git a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
index 1d27326..1605ae6 100644
--- a/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
+++ b/tests/unit/src/com/android/settings/privatespace/PrivateSpaceMaintainerTest.java
@@ -198,4 +198,40 @@
assertThat(privateSpaceMaintainer.doesPrivateSpaceExist()).isFalse();
assertThat(privateSpaceMaintainer.lockPrivateSpace()).isFalse();
}
+
+ /**
+ * Tests that {@link PrivateSpaceMaintainer#createPrivateSpace()} when no PS exists sets
+ * USER_SETUP_COMPLETE setting.
+ */
+ @Test
+ public void createPrivateSpace_psDoesNotExist_setsUserSetupComplete() {
+ PrivateSpaceMaintainer privateSpaceMaintainer =
+ PrivateSpaceMaintainer.getInstance(mContext);
+ privateSpaceMaintainer.createPrivateSpace();
+ assertThat(getSecureUserSetupComplete()).isEqualTo(1);
+ }
+
+ /**
+ * Tests that {@link PrivateSpaceMaintainer#createPrivateSpace()} when PS exists does not
+ * change USER_SETUP_COMPLETE setting.
+ */
+ @Test
+ public void createPrivateSpace_pSExists_doesNotChangeUserSetupSetting() {
+ PrivateSpaceMaintainer privateSpaceMaintainer =
+ PrivateSpaceMaintainer.getInstance(mContext);
+ privateSpaceMaintainer.createPrivateSpace();
+ assertThat(getSecureUserSetupComplete()).isEqualTo(1);
+ privateSpaceMaintainer.createPrivateSpace();
+ assertThat(getSecureUserSetupComplete()).isEqualTo(1);
+ }
+
+ private int getSecureUserSetupComplete() {
+ PrivateSpaceMaintainer privateSpaceMaintainer =
+ PrivateSpaceMaintainer.getInstance(mContext);
+ return Settings.Secure.getIntForUser(
+ mContentResolver,
+ Settings.Secure.USER_SETUP_COMPLETE,
+ 0,
+ privateSpaceMaintainer.getPrivateProfileHandle().getIdentifier());
+ }
}