diff --git a/aconfig/settings_connecteddevice_flag_declarations.aconfig b/aconfig/settings_connecteddevice_flag_declarations.aconfig
index 2d66c30446a..7942ccd1416 100644
--- a/aconfig/settings_connecteddevice_flag_declarations.aconfig
+++ b/aconfig/settings_connecteddevice_flag_declarations.aconfig
@@ -14,3 +14,14 @@ flag {
description: "Gates whether to require an auth challenge for changing USB preferences"
bug: "317367746"
}
+
+
+flag {
+ name: "enable_bonded_bluetooth_device_searchable"
+ namespace: "pixel_cross_device_control"
+ description: "Set bonded bluetooth devices under connected devices page to be searchable by Settings search."
+ bug: "319056077"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/res/color/modes_set_schedule_text_color.xml b/res/color/modes_set_schedule_text_color.xml
new file mode 100644
index 00000000000..5ceb68e709c
--- /dev/null
+++ b/res/color/modes_set_schedule_text_color.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/res/drawable/ic_zen_mode_action_change_icon.xml b/res/drawable/ic_zen_mode_action_change_icon.xml
new file mode 100644
index 00000000000..4cf4167314a
--- /dev/null
+++ b/res/drawable/ic_zen_mode_action_change_icon.xml
@@ -0,0 +1,25 @@
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/modes_schedule_day_toggle.xml b/res/drawable/modes_schedule_day_toggle.xml
new file mode 100644
index 00000000000..c09f5972833
--- /dev/null
+++ b/res/drawable/modes_schedule_day_toggle.xml
@@ -0,0 +1,47 @@
+
+
+
+ -
+
+
+
-
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/modes_icon_list.xml b/res/layout/modes_icon_list.xml
new file mode 100644
index 00000000000..87e647eb7a6
--- /dev/null
+++ b/res/layout/modes_icon_list.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/modes_icon_list_item.xml b/res/layout/modes_icon_list_item.xml
new file mode 100644
index 00000000000..aa45de33b72
--- /dev/null
+++ b/res/layout/modes_icon_list_item.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
diff --git a/res/layout/modes_set_schedule_layout.xml b/res/layout/modes_set_schedule_layout.xml
new file mode 100644
index 00000000000..5758cfb4be2
--- /dev/null
+++ b/res/layout/modes_set_schedule_layout.xml
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c72c17d79b2..d972e138eec 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -497,4 +497,9 @@
264dp
30dp
+
+
+ 96dp
+ 56dp
+ 32dp
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1f5c11d82b3..fd6a084bdd6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7963,6 +7963,15 @@
Schedule
+
+ Set a schedule
+
+
+ Schedule
+
+
+ %1$d hr, %2$d min
+
Schedule
@@ -9303,6 +9312,15 @@
Change to always interrupt
+
+ Rename
+
+
+ Change icon
+
+
+ Change icon
+
Warning
diff --git a/res/xml/modes_icon_picker.xml b/res/xml/modes_icon_picker.xml
new file mode 100644
index 00000000000..cb0ff302672
--- /dev/null
+++ b/res/xml/modes_icon_picker.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml
index f2822741bc7..cf090be46d3 100644
--- a/res/xml/modes_rule_settings.xml
+++ b/res/xml/modes_rule_settings.xml
@@ -28,6 +28,10 @@
android:selectable="false"
android:layout="@layout/modes_activation_button"/>
+
+
diff --git a/res/xml/modes_set_schedule.xml b/res/xml/modes_set_schedule.xml
new file mode 100644
index 00000000000..dd73ec814b6
--- /dev/null
+++ b/res/xml/modes_set_schedule.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java
index 02205c1b63a..a79ba8073ac 100644
--- a/src/com/android/settings/SettingsActivity.java
+++ b/src/com/android/settings/SettingsActivity.java
@@ -283,7 +283,7 @@ public class SettingsActivity extends SettingsBaseActivity
createUiFromIntent(savedState, intent);
}
- protected void createUiFromIntent(Bundle savedState, Intent intent) {
+ protected void createUiFromIntent(@Nullable Bundle savedState, Intent intent) {
long startTime = System.currentTimeMillis();
final FeatureFactory factory = FeatureFactory.getFeatureFactory();
diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java
index 5be761efc79..56a3005f6dd 100644
--- a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java
+++ b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java
@@ -19,6 +19,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.input.InputManager;
import android.util.FeatureFlagUtils;
+import android.util.Log;
import android.view.InputDevice;
import androidx.annotation.VisibleForTesting;
@@ -26,19 +27,29 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
+import com.android.settings.R;
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater;
+import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.dock.DockUpdater;
import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater;
import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.flags.Flags;
import com.android.settings.overlay.DockUpdaterFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+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 com.android.settingslib.search.SearchIndexableRaw;
+
+import java.util.List;
/**
* Controller to maintain the {@link androidx.preference.PreferenceGroup} for all
@@ -49,6 +60,7 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
DevicePreferenceCallback {
private static final String KEY = "connected_device_list";
+ private static final String TAG = "ConnectedDeviceGroupController";
@VisibleForTesting
PreferenceGroup mPreferenceGroup;
@@ -58,11 +70,13 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
private StylusDeviceUpdater mStylusDeviceUpdater;
private final PackageManager mPackageManager;
private final InputManager mInputManager;
+ private final LocalBluetoothManager mLocalBluetoothManager;
public ConnectedDeviceGroupController(Context context) {
super(context, KEY);
mPackageManager = context.getPackageManager();
mInputManager = context.getSystemService(InputManager.class);
+ mLocalBluetoothManager = Utils.getLocalBluetoothManager(context);
}
@Override
@@ -221,4 +235,31 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
}
return false;
}
+
+ @Override
+ public void updateDynamicRawDataToIndex(List rawData) {
+ if (!Flags.enableBondedBluetoothDeviceSearchable()) {
+ return;
+ }
+ if (mLocalBluetoothManager == null) {
+ Log.d(TAG, "Bluetooth is not supported");
+ return;
+ }
+ for (CachedBluetoothDevice cachedDevice :
+ mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()) {
+ if (!BluetoothDeviceFilter.BONDED_DEVICE_FILTER.matches(cachedDevice.getDevice())) {
+ continue;
+ }
+ if (BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ cachedDevice.getDevice())) {
+ continue;
+ }
+ SearchIndexableRaw data = new SearchIndexableRaw(mContext);
+ // Include the identity address as well to ensure the key is unique.
+ data.key = cachedDevice.getName() + cachedDevice.getIdentityAddress();
+ data.title = cachedDevice.getName();
+ data.summaryOn = mContext.getString(R.string.connected_devices_dashboard_title);
+ rawData.add(data);
+ }
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceController.java
index d2f23edfe8a..581ad62bb31 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceController.java
@@ -57,9 +57,10 @@ public class AudioSharingCompatibilityPreferenceController extends TogglePrefere
@Nullable private TwoStatePreference mPreference;
private final Executor mExecutor;
private final MetricsFeatureProvider mMetricsFeatureProvider;
- private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
+ private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
- private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+ @VisibleForTesting
+ protected final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@Override
public void onBroadcastStarted(int reason, int broadcastId) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
index c3248c7e573..c7d740740e7 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
@@ -20,6 +20,8 @@ import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
+import androidx.annotation.VisibleForTesting;
+
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController;
@@ -31,7 +33,6 @@ public class AudioSharingDashboardFragment extends DashboardFragment
private static final String TAG = "AudioSharingDashboardFrag";
SettingsMainSwitchBar mMainSwitchBar;
- private AudioSharingSwitchBarController mSwitchBarController;
private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController;
private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController;
@@ -83,9 +84,10 @@ public class AudioSharingDashboardFragment extends DashboardFragment
final SettingsActivity activity = (SettingsActivity) getActivity();
mMainSwitchBar = activity.getSwitchBar();
mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title));
- mSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this);
- mSwitchBarController.init(this);
- getSettingsLifecycle().addObserver(mSwitchBarController);
+ AudioSharingSwitchBarController switchBarController =
+ new AudioSharingSwitchBarController(activity, mMainSwitchBar, this);
+ switchBarController.init(this);
+ getSettingsLifecycle().addObserver(switchBarController);
mMainSwitchBar.show();
}
@@ -99,6 +101,19 @@ public class AudioSharingDashboardFragment extends DashboardFragment
onProfilesConnectedForAttachedPreferences();
}
+ /** Test only: set mock controllers for the {@link AudioSharingDashboardFragment} */
+ @VisibleForTesting
+ protected void setControllers(
+ AudioSharingDeviceVolumeGroupController volumeGroupController,
+ AudioSharingCallAudioPreferenceController callAudioController,
+ AudioSharingPlaySoundPreferenceController playSoundController,
+ AudioStreamsCategoryController streamsCategoryController) {
+ mAudioSharingDeviceVolumeGroupController = volumeGroupController;
+ mAudioSharingCallAudioPreferenceController = callAudioController;
+ mAudioSharingPlaySoundPreferenceController = playSoundController;
+ mAudioStreamsCategoryController = streamsCategoryController;
+ }
+
private void updateVisibilityForAttachedPreferences() {
mAudioSharingDeviceVolumeGroupController.updateVisibility();
mAudioSharingCallAudioPreferenceController.updateVisibility();
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
index 6f7de8ca74e..3d111fd8e3b 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
@@ -20,9 +20,11 @@ import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.util.Log;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@@ -48,13 +50,17 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
* @param item The device item clicked.
*/
void onItemClick(AudioSharingDeviceItem item);
+
+ /** Called when users click the cancel button in the dialog. */
+ void onCancelClick();
}
@Nullable private static DialogEventListener sListener;
+ private static Pair[] sEventData = new Pair[0];
@Override
public int getMetricsCategory() {
- return SettingsEnums.DIALOG_START_AUDIO_SHARING;
+ return SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE;
}
/**
@@ -63,14 +69,17 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
* @param host The Fragment this dialog will be hosted.
* @param deviceItems The connected device items eligible for audio sharing.
* @param listener The callback to handle the user action on this dialog.
+ * @param eventData The eventData to log with for dialog onClick events.
*/
public static void show(
@NonNull Fragment host,
@NonNull List deviceItems,
- @NonNull DialogEventListener listener) {
+ @NonNull DialogEventListener listener,
+ @NonNull Pair[] eventData) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
final FragmentManager manager = host.getChildFragmentManager();
sListener = listener;
+ sEventData = eventData;
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, return.");
@@ -84,7 +93,19 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
dialogFrag.show(manager, TAG);
}
+ /** Return the tag of {@link AudioSharingDialogFragment} dialog. */
+ public static @NonNull String tag() {
+ return TAG;
+ }
+
+ /** Test only: get the event data passed to the dialog. */
+ @VisibleForTesting
+ protected @NonNull Pair[] getEventData() {
+ return sEventData;
+ }
+
@Override
+ @NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle arguments = requireArguments();
List deviceItems =
@@ -93,12 +114,17 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
AudioSharingDialogFactory.newBuilder(getActivity())
.setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
.setIsCustomBodyEnabled(true);
+ if (deviceItems == null) {
+ Log.d(TAG, "Create dialog error: null deviceItems");
+ return builder.build();
+ }
if (deviceItems.isEmpty()) {
builder.setTitle(R.string.audio_sharing_share_dialog_title)
.setCustomImage(R.drawable.audio_sharing_guidance)
.setCustomMessage(R.string.audio_sharing_dialog_connect_device_content)
.setNegativeButton(
- R.string.audio_sharing_close_button_label, (dig, which) -> dismiss());
+ R.string.audio_sharing_close_button_label,
+ (dig, which) -> onCancelClick());
} else if (deviceItems.size() == 1) {
AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems);
builder.setTitle(
@@ -111,11 +137,16 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
v -> {
if (sListener != null) {
sListener.onItemClick(deviceItem);
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED,
+ sEventData);
}
dismiss();
})
.setCustomNegativeButton(
- R.string.audio_sharing_no_thanks_button_label, v -> dismiss());
+ R.string.audio_sharing_no_thanks_button_label, v -> onCancelClick());
} else {
builder.setTitle(R.string.audio_sharing_share_with_more_dialog_title)
.setCustomMessage(R.string.audio_sharing_dialog_share_more_content)
@@ -130,8 +161,20 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
dismiss();
},
AudioSharingDeviceAdapter.ActionType.SHARE))
- .setCustomNegativeButton(com.android.settings.R.string.cancel, v -> dismiss());
+ .setCustomNegativeButton(
+ com.android.settings.R.string.cancel, v -> onCancelClick());
}
return builder.build();
}
+
+ private void onCancelClick() {
+ if (sListener != null) {
+ sListener.onCancelClick();
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED,
+ sEventData);
+ }
+ dismiss();
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java
index c329e82cc7a..5458a9f259b 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java
@@ -24,6 +24,7 @@ import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.util.Log;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -33,15 +34,21 @@ import androidx.fragment.app.Fragment;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+import com.android.settingslib.utils.ThreadUtils;
+
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.Executor;
public class AudioSharingDialogHandler {
@@ -51,6 +58,7 @@ public class AudioSharingDialogHandler {
@Nullable private final LocalBluetoothManager mLocalBtManager;
@Nullable private final LocalBluetoothLeBroadcast mBroadcast;
@Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
private List mTargetSinks = new ArrayList<>();
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
@@ -119,9 +127,7 @@ public class AudioSharingDialogHandler {
new SubSettingLauncher(mContext)
.setDestination(AudioSharingDashboardFragment.class.getName())
.setSourceMetricsCategory(
- (mHostFragment != null
- && mHostFragment
- instanceof DashboardFragment)
+ (mHostFragment instanceof DashboardFragment)
? ((DashboardFragment) mHostFragment)
.getMetricsCategory()
: SettingsEnums.PAGE_UNKNOWN)
@@ -146,6 +152,7 @@ public class AudioSharingDialogHandler {
mLocalBtManager != null
? mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile()
: null;
+ mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
}
/** Register callbacks for dialog handler */
@@ -191,6 +198,18 @@ public class AudioSharingDialogHandler {
List deviceItemsInSharingSession =
AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
+ AudioSharingStopDialogFragment.DialogEventListener listener =
+ () -> {
+ cachedDevice.setActive();
+ AudioSharingUtils.stopBroadcasting(mLocalBtManager);
+ };
+ Pair[] eventData =
+ AudioSharingUtils.buildAudioSharingDialogEventData(
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
+ SettingsEnums.DIALOG_STOP_AUDIO_SHARING,
+ userTriggered,
+ deviceItemsInSharingSession.size(),
+ /* candidateDeviceCount= */ 0);
postOnMainThread(
() -> {
closeOpeningDialogsOtherThan(AudioSharingStopDialogFragment.tag());
@@ -198,10 +217,8 @@ public class AudioSharingDialogHandler {
mHostFragment,
deviceItemsInSharingSession,
cachedDevice,
- () -> {
- cachedDevice.setActive();
- AudioSharingUtils.stopBroadcasting(mLocalBtManager);
- });
+ listener,
+ eventData);
});
} else {
if (userTriggered) {
@@ -252,6 +269,20 @@ public class AudioSharingDialogHandler {
// Show audio sharing switch dialog when the third eligible (LE audio) remote device
// connected during a sharing session.
if (deviceItemsInSharingSession.size() >= 2) {
+ AudioSharingDisconnectDialogFragment.DialogEventListener listener =
+ (AudioSharingDeviceItem item) -> {
+ // Remove all sources from the device user clicked
+ removeSourceForGroup(item.getGroupId(), groupedDevices);
+ // Add current broadcast to the latest connected device
+ addSourceForGroup(groupId, groupedDevices);
+ };
+ Pair[] eventData =
+ AudioSharingUtils.buildAudioSharingDialogEventData(
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
+ SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE,
+ userTriggered,
+ deviceItemsInSharingSession.size(),
+ /* candidateDeviceCount= */ 1);
postOnMainThread(
() -> {
closeOpeningDialogsOtherThan(
@@ -260,16 +291,29 @@ public class AudioSharingDialogHandler {
mHostFragment,
deviceItemsInSharingSession,
cachedDevice,
- (AudioSharingDeviceItem item) -> {
- // Remove all sources from the device user clicked
- removeSourceForGroup(item.getGroupId(), groupedDevices);
- // Add current broadcast to the latest connected device
- addSourceForGroup(groupId, groupedDevices);
- });
+ listener,
+ eventData);
});
} else {
// Show audio sharing join dialog when the first or second eligible (LE audio)
// remote device connected during a sharing session.
+ AudioSharingJoinDialogFragment.DialogEventListener listener =
+ new AudioSharingJoinDialogFragment.DialogEventListener() {
+ @Override
+ public void onShareClick() {
+ addSourceForGroup(groupId, groupedDevices);
+ }
+
+ @Override
+ public void onCancelClick() {}
+ };
+ Pair[] eventData =
+ AudioSharingUtils.buildAudioSharingDialogEventData(
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
+ SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
+ userTriggered,
+ deviceItemsInSharingSession.size(),
+ /* candidateDeviceCount= */ 1);
postOnMainThread(
() -> {
closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag());
@@ -277,15 +321,8 @@ public class AudioSharingDialogHandler {
mHostFragment,
deviceItemsInSharingSession,
cachedDevice,
- new AudioSharingJoinDialogFragment.DialogEventListener() {
- @Override
- public void onShareClick() {
- addSourceForGroup(groupId, groupedDevices);
- }
-
- @Override
- public void onCancelClick() {}
- });
+ listener,
+ eventData);
});
}
} else {
@@ -302,39 +339,43 @@ public class AudioSharingDialogHandler {
// Show audio sharing join dialog when the second eligible (LE audio) remote
// device connect and no sharing session.
if (deviceItems.size() == 1) {
+ AudioSharingJoinDialogFragment.DialogEventListener listener =
+ new AudioSharingJoinDialogFragment.DialogEventListener() {
+ @Override
+ public void onShareClick() {
+ mTargetSinks = new ArrayList<>();
+ for (List devices :
+ groupedDevices.values()) {
+ for (CachedBluetoothDevice device : devices) {
+ mTargetSinks.add(device.getDevice());
+ }
+ }
+ Log.d(TAG, "Start broadcast with sinks = " + mTargetSinks.size());
+ if (mBroadcast != null) {
+ mBroadcast.startPrivateBroadcast();
+ }
+ }
+
+ @Override
+ public void onCancelClick() {
+ if (userTriggered) {
+ cachedDevice.setActive();
+ }
+ }
+ };
+
+ Pair[] eventData =
+ AudioSharingUtils.buildAudioSharingDialogEventData(
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
+ SettingsEnums.DIALOG_START_AUDIO_SHARING,
+ userTriggered,
+ /* deviceCountInSharing= */ 0,
+ /* candidateDeviceCount= */ 2);
postOnMainThread(
() -> {
closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag());
AudioSharingJoinDialogFragment.show(
- mHostFragment,
- deviceItems,
- cachedDevice,
- new AudioSharingJoinDialogFragment.DialogEventListener() {
- @Override
- public void onShareClick() {
- mTargetSinks = new ArrayList<>();
- for (List devices :
- groupedDevices.values()) {
- for (CachedBluetoothDevice device : devices) {
- mTargetSinks.add(device.getDevice());
- }
- }
- Log.d(
- TAG,
- "Start broadcast with sinks: "
- + mTargetSinks.size());
- if (mBroadcast != null) {
- mBroadcast.startPrivateBroadcast();
- }
- }
-
- @Override
- public void onCancelClick() {
- if (userTriggered) {
- cachedDevice.setActive();
- }
- }
- });
+ mHostFragment, deviceItems, cachedDevice, listener, eventData);
});
} else if (userTriggered) {
cachedDevice.setActive();
@@ -346,9 +387,12 @@ public class AudioSharingDialogHandler {
if (mHostFragment == null) return;
List fragments = mHostFragment.getChildFragmentManager().getFragments();
for (Fragment fragment : fragments) {
- if (fragment instanceof DialogFragment && !fragment.getTag().equals(tag)) {
+ if (fragment instanceof DialogFragment
+ && fragment.getTag() != null
+ && !fragment.getTag().equals(tag)) {
Log.d(TAG, "Remove staled opening dialog " + fragment.getTag());
((DialogFragment) fragment).dismiss();
+ logDialogDismissEvent(fragment);
}
}
}
@@ -365,6 +409,7 @@ public class AudioSharingDialogHandler {
&& AudioSharingUtils.getGroupId(device) == groupId) {
Log.d(TAG, "Remove staled opening dialog for group " + groupId);
((DialogFragment) fragment).dismiss();
+ logDialogDismissEvent(fragment);
}
}
}
@@ -382,6 +427,7 @@ public class AudioSharingDialogHandler {
"Remove staled opening dialog for device "
+ cachedDevice.getDevice().getAnonymizedAddress());
((DialogFragment) fragment).dismiss();
+ logDialogDismissEvent(fragment);
}
}
}
@@ -409,9 +455,9 @@ public class AudioSharingDialogHandler {
Log.d(TAG, "Fail to remove source for group " + groupId);
return;
}
- groupedDevices.get(groupId).stream()
+ groupedDevices.getOrDefault(groupId, ImmutableList.of()).stream()
.map(CachedBluetoothDevice::getDevice)
- .filter(device -> device != null)
+ .filter(Objects::nonNull)
.forEach(
device -> {
for (BluetoothLeBroadcastReceiveState source :
@@ -431,9 +477,9 @@ public class AudioSharingDialogHandler {
Log.d(TAG, "Fail to add source due to invalid group id, group = " + groupId);
return;
}
- groupedDevices.get(groupId).stream()
+ groupedDevices.getOrDefault(groupId, ImmutableList.of()).stream()
.map(CachedBluetoothDevice::getDevice)
- .filter(device -> device != null)
+ .filter(Objects::nonNull)
.forEach(
device ->
mAssistant.addSource(
@@ -449,4 +495,29 @@ public class AudioSharingDialogHandler {
private boolean isBroadcasting() {
return mBroadcast != null && mBroadcast.isEnabled(null);
}
+
+ private void logDialogDismissEvent(Fragment fragment) {
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ int pageId = SettingsEnums.PAGE_UNKNOWN;
+ if (fragment instanceof AudioSharingJoinDialogFragment) {
+ pageId =
+ ((AudioSharingJoinDialogFragment) fragment)
+ .getMetricsCategory();
+ } else if (fragment instanceof AudioSharingStopDialogFragment) {
+ pageId =
+ ((AudioSharingStopDialogFragment) fragment)
+ .getMetricsCategory();
+ } else if (fragment instanceof AudioSharingDisconnectDialogFragment) {
+ pageId =
+ ((AudioSharingDisconnectDialogFragment) fragment)
+ .getMetricsCategory();
+ }
+ mMetricsFeatureProvider.action(
+ mContext,
+ SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
+ pageId);
+ });
+ }
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
index e859693a17d..5f6d84a1929 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
@@ -20,16 +20,20 @@ import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.util.Log;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.utils.ThreadUtils;
import java.util.List;
import java.util.Locale;
@@ -55,6 +59,7 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
@Nullable private static DialogEventListener sListener;
@Nullable private static CachedBluetoothDevice sNewDevice;
+ private static Pair[] sEventData = new Pair[0];
@Override
public int getMetricsCategory() {
@@ -70,12 +75,14 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
* @param deviceItems The existing connected device items in audio sharing session.
* @param newDevice The latest connected device triggered this dialog.
* @param listener The callback to handle the user action on this dialog.
+ * @param eventData The eventData to log with for dialog onClick events.
*/
public static void show(
@NonNull Fragment host,
@NonNull List deviceItems,
@NonNull CachedBluetoothDevice newDevice,
- @NonNull DialogEventListener listener) {
+ @NonNull DialogEventListener listener,
+ @NonNull Pair[] eventData) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
FragmentManager manager = host.getChildFragmentManager();
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
@@ -91,6 +98,7 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
newGroupId));
sListener = listener;
sNewDevice = newDevice;
+ sEventData = eventData;
return;
} else {
Log.d(
@@ -101,10 +109,22 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
+ "dismiss current dialog.",
newGroupId));
dialog.dismiss();
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () ->
+ FeatureFactory.getFeatureFactory()
+ .getMetricsFeatureProvider()
+ .action(
+ dialog.getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
+ SettingsEnums
+ .DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
}
}
sListener = listener;
sNewDevice = newDevice;
+ sEventData = eventData;
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
@@ -125,28 +145,54 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
return sNewDevice;
}
+ /** Test only: get the event data passed to the dialog. */
+ @VisibleForTesting
+ protected @NonNull Pair[] getEventData() {
+ return sEventData;
+ }
+
@Override
+ @NonNull
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Bundle arguments = requireArguments();
List deviceItems =
arguments.getParcelable(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, List.class);
- return AudioSharingDialogFactory.newBuilder(getActivity())
- .setTitle(R.string.audio_sharing_disconnect_dialog_title)
- .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
- .setIsCustomBodyEnabled(true)
- .setCustomMessage(R.string.audio_sharing_dialog_disconnect_content)
- .setCustomDeviceActions(
- new AudioSharingDeviceAdapter(
- getContext(),
- deviceItems,
- (AudioSharingDeviceItem item) -> {
- if (sListener != null) {
- sListener.onItemClick(item);
- }
+ AudioSharingDialogFactory.DialogBuilder builder =
+ AudioSharingDialogFactory.newBuilder(getActivity())
+ .setTitle(R.string.audio_sharing_disconnect_dialog_title)
+ .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
+ .setIsCustomBodyEnabled(true)
+ .setCustomMessage(R.string.audio_sharing_dialog_disconnect_content)
+ .setCustomNegativeButton(
+ com.android.settings.R.string.cancel,
+ v -> {
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED,
+ sEventData);
dismiss();
- },
- AudioSharingDeviceAdapter.ActionType.REMOVE))
- .setCustomNegativeButton(com.android.settings.R.string.cancel, v -> dismiss())
- .build();
+ });
+ if (deviceItems == null) {
+ Log.d(TAG, "Create dialog error: null deviceItems");
+ return builder.build();
+ }
+ builder.setCustomDeviceActions(
+ new AudioSharingDeviceAdapter(
+ getContext(),
+ deviceItems,
+ (AudioSharingDeviceItem item) -> {
+ if (sListener != null) {
+ sListener.onItemClick(item);
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED,
+ sEventData);
+ }
+ dismiss();
+ },
+ AudioSharingDeviceAdapter.ActionType.REMOVE));
+ return builder.build();
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
index 4982179ccfc..7eebbcb2156 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
@@ -20,9 +20,11 @@ import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.util.Log;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@@ -52,6 +54,7 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
@Nullable private static DialogEventListener sListener;
@Nullable private static CachedBluetoothDevice sNewDevice;
+ private static Pair[] sEventData = new Pair[0];
@Override
public int getMetricsCategory() {
@@ -69,16 +72,19 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
* @param deviceItems The existing connected device items eligible for audio sharing.
* @param newDevice The latest connected device triggered this dialog.
* @param listener The callback to handle the user action on this dialog.
+ * @param eventData The eventData to log with for dialog onClick events.
*/
public static void show(
@NonNull Fragment host,
@NonNull List deviceItems,
@NonNull CachedBluetoothDevice newDevice,
- @NonNull DialogEventListener listener) {
+ @NonNull DialogEventListener listener,
+ @NonNull Pair[] eventData) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
final FragmentManager manager = host.getChildFragmentManager();
sListener = listener;
sNewDevice = newDevice;
+ sEventData = eventData;
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
if (dialog != null) {
Log.d(TAG, "Dialog is showing, update the content.");
@@ -104,7 +110,14 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
return sNewDevice;
}
+ /** Test only: get the event data passed to the dialog. */
+ @VisibleForTesting
+ protected @NonNull Pair[] getEventData() {
+ return sEventData;
+ }
+
@Override
+ @NonNull
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Bundle arguments = requireArguments();
List deviceItems =
@@ -121,6 +134,11 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
v -> {
if (sListener != null) {
sListener.onShareClick();
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED,
+ sEventData);
}
dismiss();
})
@@ -129,11 +147,20 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
v -> {
if (sListener != null) {
sListener.onCancelClick();
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED,
+ sEventData);
}
dismiss();
})
.build();
- updateDialog(deviceItems, newDeviceName, dialog);
+ if (deviceItems == null) {
+ Log.d(TAG, "Fail to create dialog: null deviceItems");
+ } else {
+ updateDialog(deviceItems, newDeviceName, dialog);
+ }
dialog.show();
AudioSharingDialogHelper.updateMessageStyle(dialog);
return dialog;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java
index 54eb722ba50..d27d3a20d72 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java
@@ -23,6 +23,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
@@ -50,7 +51,8 @@ public class AudioSharingPreferenceController extends BasePreferenceController
@Nullable private Preference mPreference;
private final Executor mExecutor;
- private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+ @VisibleForTesting
+ protected final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@Override
public void onBroadcastStarted(int reason, int broadcastId) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
index affd54acf5e..beac4b0fd33 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
@@ -20,16 +20,20 @@ import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import android.util.Log;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.google.common.collect.Iterables;
@@ -52,6 +56,7 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
@Nullable private static DialogEventListener sListener;
@Nullable private static CachedBluetoothDevice sCachedDevice;
+ private static Pair[] sEventData = new Pair[0];
@Override
public int getMetricsCategory() {
@@ -67,12 +72,14 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
* @param deviceItems The existing connected device items in audio sharing session.
* @param newDevice The latest connected device triggered this dialog.
* @param listener The callback to handle the user action on this dialog.
+ * @param eventData The eventData to log with for dialog onClick events.
*/
public static void show(
@NonNull Fragment host,
@NonNull List deviceItems,
@NonNull CachedBluetoothDevice newDevice,
- @NonNull DialogEventListener listener) {
+ @NonNull DialogEventListener listener,
+ @NonNull Pair[] eventData) {
if (!AudioSharingUtils.isFeatureEnabled()) return;
final FragmentManager manager = host.getChildFragmentManager();
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
@@ -88,6 +95,7 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
newGroupId));
sListener = listener;
sCachedDevice = newDevice;
+ sEventData = eventData;
return;
} else {
Log.d(
@@ -98,10 +106,21 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
+ "dismiss current dialog.",
newGroupId));
dialog.dismiss();
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () ->
+ FeatureFactory.getFeatureFactory()
+ .getMetricsFeatureProvider()
+ .action(
+ dialog.getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
+ SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
}
}
sListener = listener;
sCachedDevice = newDevice;
+ sEventData = eventData;
Log.d(TAG, "Show up the dialog.");
final Bundle bundle = new Bundle();
bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
@@ -121,23 +140,34 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
return sCachedDevice;
}
+ /** Test only: get the event data passed to the dialog. */
+ @VisibleForTesting
+ protected @NonNull Pair[] getEventData() {
+ return sEventData;
+ }
+
@Override
+ @NonNull
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Bundle arguments = requireArguments();
List deviceItems =
arguments.getParcelable(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, List.class);
String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
- String customMessage =
- deviceItems.size() == 1
- ? getString(
- R.string.audio_sharing_stop_dialog_content,
- Iterables.getOnlyElement(deviceItems).getName())
- : (deviceItems.size() == 2
- ? getString(
- R.string.audio_sharing_stop_dialog_with_two_content,
- deviceItems.get(0).getName(),
- deviceItems.get(1).getName())
- : getString(R.string.audio_sharing_stop_dialog_with_more_content));
+ String customMessage = "";
+ if (deviceItems != null) {
+ customMessage =
+ deviceItems.size() == 1
+ ? getString(
+ R.string.audio_sharing_stop_dialog_content,
+ Iterables.getOnlyElement(deviceItems).getName())
+ : (deviceItems.size() == 2
+ ? getString(
+ R.string.audio_sharing_stop_dialog_with_two_content,
+ deviceItems.get(0).getName(),
+ deviceItems.get(1).getName())
+ : getString(
+ R.string.audio_sharing_stop_dialog_with_more_content));
+ }
AlertDialog dialog =
AudioSharingDialogFactory.newBuilder(getActivity())
.setTitle(
@@ -150,10 +180,21 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
(dlg, which) -> {
if (sListener != null) {
sListener.onStopSharingClick();
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED,
+ sEventData);
}
})
.setNegativeButton(
- com.android.settings.R.string.cancel, (dlg, which) -> dismiss())
+ com.android.settings.R.string.cancel,
+ (dlg, which) ->
+ mMetricsFeatureProvider.action(
+ getContext(),
+ SettingsEnums
+ .ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED,
+ sEventData))
.build();
dialog.show();
AudioSharingDialogHelper.updateMessageStyle(dialog);
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
index 475be85a8a3..5022579ce65 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
@@ -16,6 +16,7 @@
package com.android.settings.connecteddevice.audiosharing;
+import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcast;
@@ -29,24 +30,27 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.util.FeatureFlagUtils;
import android.util.Log;
+import android.util.Pair;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.fragment.app.Fragment;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
-import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.SettingsMainSwitchBar;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils;
import com.google.common.collect.ImmutableList;
@@ -56,6 +60,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -91,14 +96,15 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
@Nullable private final LocalBluetoothProfileManager mProfileManager;
@Nullable private final LocalBluetoothLeBroadcast mBroadcast;
@Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
- @Nullable private DashboardFragment mFragment;
+ @Nullable private Fragment mFragment;
private final Executor mExecutor;
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
private final OnAudioSharingStateChangedListener mListener;
private Map> mGroupedConnectedDevices = new HashMap<>();
private List mTargetActiveSinks = new ArrayList<>();
private List mDeviceItemsForSharing = new ArrayList<>();
@VisibleForTesting IntentFilter mIntentFilter;
- private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
+ private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
@VisibleForTesting
BroadcastReceiver mReceiver =
@@ -110,7 +116,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
}
};
- private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+ @VisibleForTesting
+ protected final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@Override
public void onBroadcastStarted(int reason, int broadcastId) {
@@ -182,7 +189,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
public void onPlaybackStopped(int reason, int broadcastId) {}
};
- private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+ private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
new BluetoothLeBroadcastAssistant.Callback() {
@Override
public void onSearchStarted(int reason) {}
@@ -251,9 +258,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
@Override
public void onReceiveStateChanged(
- BluetoothDevice sink,
+ @NonNull BluetoothDevice sink,
int sourceId,
- BluetoothLeBroadcastReceiveState state) {}
+ @NonNull BluetoothLeBroadcastReceiveState state) {}
};
AudioSharingSwitchBarController(
@@ -273,6 +280,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
? null
: mProfileManager.getLeAudioBroadcastAssistantProfile();
mExecutor = Executors.newSingleThreadExecutor();
+ mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
}
@Override
@@ -378,7 +386,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
*
* @param fragment The fragment to host the {@link AudioSharingSwitchBarController} dialog.
*/
- public void init(DashboardFragment fragment) {
+ public void init(@NonNull Fragment fragment) {
this.mFragment = fragment;
}
@@ -494,34 +502,58 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
}
private void handleOnBroadcastReady() {
- AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager);
- mTargetActiveSinks.clear();
+ Pair[] eventData =
+ AudioSharingUtils.buildAudioSharingDialogEventData(
+ SettingsEnums.AUDIO_SHARING_SETTINGS,
+ SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
+ /* userTriggered= */ false,
+ /* deviceCountInSharing= */ mTargetActiveSinks.isEmpty() ? 0 : 1,
+ /* candidateDeviceCount= */ mDeviceItemsForSharing.size());
+ if (!mTargetActiveSinks.isEmpty()) {
+ Log.d(TAG, "handleOnBroadcastReady: automatically add source to active sinks.");
+ AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager);
+ mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING);
+ mTargetActiveSinks.clear();
+ }
if (mFragment == null) {
- Log.w(TAG, "Dialog fail to show due to null fragment.");
+ Log.d(TAG, "handleOnBroadcastReady: dialog fail to show due to null fragment.");
mGroupedConnectedDevices.clear();
mDeviceItemsForSharing.clear();
return;
}
+ showDialog(eventData);
+ }
+
+ private void showDialog(Pair[] eventData) {
+ AudioSharingDialogFragment.DialogEventListener listener =
+ new AudioSharingDialogFragment.DialogEventListener() {
+ @Override
+ public void onItemClick(@NonNull AudioSharingDeviceItem item) {
+ AudioSharingUtils.addSourceToTargetSinks(
+ mGroupedConnectedDevices
+ .getOrDefault(item.getGroupId(), ImmutableList.of())
+ .stream()
+ .map(CachedBluetoothDevice::getDevice)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList()),
+ mBtManager);
+ mGroupedConnectedDevices.clear();
+ mDeviceItemsForSharing.clear();
+ }
+
+ @Override
+ public void onCancelClick() {
+ mGroupedConnectedDevices.clear();
+ mDeviceItemsForSharing.clear();
+ }
+ };
AudioSharingUtils.postOnMainThread(
mContext,
() -> {
// Check nullability to pass NullAway check
if (mFragment != null) {
AudioSharingDialogFragment.show(
- mFragment,
- mDeviceItemsForSharing,
- item -> {
- AudioSharingUtils.addSourceToTargetSinks(
- mGroupedConnectedDevices
- .getOrDefault(
- item.getGroupId(), ImmutableList.of())
- .stream()
- .map(CachedBluetoothDevice::getDevice)
- .collect(Collectors.toList()),
- mBtManager);
- mGroupedConnectedDevices.clear();
- mDeviceItemsForSharing.clear();
- });
+ mFragment, mDeviceItemsForSharing, listener, eventData);
}
});
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
index f63717eb536..29f605c94a4 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
@@ -16,6 +16,12 @@
package com.android.settings.connecteddevice.audiosharing;
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT;
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING;
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID;
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID;
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
@@ -25,6 +31,7 @@ import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.provider.Settings;
import android.util.Log;
+import android.util.Pair;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -54,6 +61,14 @@ public class AudioSharingUtils {
private static final String TAG = "AudioSharingUtils";
private static final boolean DEBUG = BluetoothUtils.D;
+ public enum MetricKey {
+ METRIC_KEY_SOURCE_PAGE_ID,
+ METRIC_KEY_PAGE_ID,
+ METRIC_KEY_USER_TRIGGERED,
+ METRIC_KEY_DEVICE_COUNT_IN_SHARING,
+ METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ }
+
/**
* Fetch {@link CachedBluetoothDevice}s connected to the broadcast assistant. The devices are
* grouped by CSIP group id.
@@ -121,7 +136,7 @@ public class AudioSharingUtils {
boolean filterByInSharing) {
List orderedDevices = new ArrayList<>();
for (List devices : groupedConnectedDevices.values()) {
- @Nullable CachedBluetoothDevice leadDevice = getLeadDevice(devices);
+ CachedBluetoothDevice leadDevice = getLeadDevice(devices);
if (leadDevice == null) {
Log.d(TAG, "Skip due to no lead device");
continue;
@@ -206,7 +221,7 @@ public class AudioSharingUtils {
return buildOrderedConnectedLeadDevices(
localBtManager, groupedConnectedDevices, filterByInSharing)
.stream()
- .map(device -> buildAudioSharingDeviceItem(device))
+ .map(AudioSharingUtils::buildAudioSharingDeviceItem)
.collect(Collectors.toList());
}
@@ -315,8 +330,9 @@ public class AudioSharingUtils {
manager.getProfileManager().getLeAudioBroadcastProfile();
if (broadcast == null) {
Log.d(TAG, "Skip stop broadcasting due to broadcast profile is null");
+ } else {
+ broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
}
- broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
}
/**
@@ -378,9 +394,32 @@ public class AudioSharingUtils {
return false;
}
VolumeControlProfile vc = profileManager.getVolumeControlProfile();
- if (vc == null || !vc.isProfileReady()) {
- return false;
- }
- return true;
+ return vc != null && vc.isProfileReady();
+ }
+
+ /**
+ * Build audio sharing dialog log event data
+ *
+ * @param sourcePageId The source page id on which the dialog is shown. *
+ * @param pageId The page id of the dialog.
+ * @param userTriggered Indicates whether the dialog is triggered by user click.
+ * @param deviceCountInSharing The count of the devices joining the audio sharing.
+ * @param candidateDeviceCount The count of the eligible devices to join the audio sharing.
+ * @return The event data to be attached to the audio sharing action logs.
+ */
+ @NonNull
+ public static Pair[] buildAudioSharingDialogEventData(
+ int sourcePageId,
+ int pageId,
+ boolean userTriggered,
+ int deviceCountInSharing,
+ int candidateDeviceCount) {
+ return new Pair[] {
+ Pair.create(METRIC_KEY_SOURCE_PAGE_ID.ordinal(), sourcePageId),
+ Pair.create(METRIC_KEY_PAGE_ID.ordinal(), pageId),
+ Pair.create(METRIC_KEY_USER_TRIGGERED.ordinal(), userTriggered ? 1 : 0),
+ Pair.create(METRIC_KEY_DEVICE_COUNT_IN_SHARING.ordinal(), deviceCountInSharing),
+ Pair.create(METRIC_KEY_CANDIDATE_DEVICE_COUNT.ordinal(), candidateDeviceCount)
+ };
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java
index ddb0b425d71..88e2322ec1f 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java
@@ -16,17 +16,91 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
import com.android.settings.SettingsActivity;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
-public class AudioStreamConfirmDialogActivity extends SettingsActivity {
+public class AudioStreamConfirmDialogActivity extends SettingsActivity
+ implements LocalBluetoothProfileManager.ServiceListener {
+ private static final String TAG = "AudioStreamConfirmDialogActivity";
+ @Nullable private LocalBluetoothProfileManager mProfileManager;
+ @Nullable private Bundle mSavedState;
+ @Nullable private Intent mIntent;
+
+ @Override
+ protected boolean isToolbarEnabled() {
+ return false;
+ }
@Override
protected void onCreate(Bundle savedState) {
+ var localBluetoothManager = Utils.getLocalBluetoothManager(this);
+ mProfileManager =
+ localBluetoothManager == null ? null : localBluetoothManager.getProfileManager();
super.onCreate(savedState);
}
+ @Override
+ protected void createUiFromIntent(@Nullable Bundle savedState, Intent intent) {
+ if (AudioSharingUtils.isFeatureEnabled()
+ && !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+ Log.d(TAG, "createUiFromIntent() : supported but not ready, skip createUiFromIntent");
+ mSavedState = savedState;
+ mIntent = intent;
+ return;
+ }
+
+ Log.d(
+ TAG,
+ "createUiFromIntent() : not supported or already connected, starting"
+ + " createUiFromIntent");
+ super.createUiFromIntent(savedState, intent);
+ }
+
+ @Override
+ public void onStart() {
+ if (AudioSharingUtils.isFeatureEnabled()
+ && !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+ Log.d(TAG, "onStart() : supported but not ready, listen to service ready");
+ if (mProfileManager != null) {
+ mProfileManager.addServiceListener(this);
+ }
+ }
+ super.onStart();
+ }
+
+ @Override
+ public void onStop() {
+ if (mProfileManager != null) {
+ mProfileManager.removeServiceListener(this);
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onServiceConnected() {
+ if (AudioSharingUtils.isFeatureEnabled()
+ && AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+ if (mProfileManager != null) {
+ mProfileManager.removeServiceListener(this);
+ }
+ if (mIntent != null) {
+ Log.d(TAG, "onServiceConnected() : service ready, starting createUiFromIntent");
+ super.createUiFromIntent(mSavedState, mIntent);
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected() {}
+
@Override
protected boolean isValidFragment(String fragmentName) {
return AudioStreamConfirmDialog.class.getName().equals(fragmentName);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
index 5b28abb422f..0bb6286171c 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
@@ -66,10 +66,10 @@ import java.util.stream.Collectors;
public final class DatabaseUtils {
private static final String TAG = "DatabaseUtils";
private static final String SHARED_PREFS_FILE = "battery_usage_shared_prefs";
+ private static final boolean EXPLICIT_CLEAR_MEMORY_ENABLED = false;
/** Clear memory threshold for device booting phase. */
private static final long CLEAR_MEMORY_THRESHOLD_MS = Duration.ofMinutes(5).toMillis();
-
private static final long CLEAR_MEMORY_DELAYED_MS = Duration.ofSeconds(2).toMillis();
private static final long INVALID_TIMESTAMP = 0L;
@@ -975,7 +975,8 @@ public final class DatabaseUtils {
}
private static void clearMemory() {
- if (SystemClock.uptimeMillis() > CLEAR_MEMORY_THRESHOLD_MS) {
+ if (!EXPLICIT_CLEAR_MEMORY_ENABLED
+ || SystemClock.uptimeMillis() > CLEAR_MEMORY_THRESHOLD_MS) {
return;
}
final Handler mainHandler = new Handler(Looper.getMainLooper());
diff --git a/src/com/android/settings/network/ConnectivityRepository.kt b/src/com/android/settings/network/ConnectivityRepository.kt
new file mode 100644
index 00000000000..3f9b61c394d
--- /dev/null
+++ b/src/com/android/settings/network/ConnectivityRepository.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+
+class ConnectivityRepository(context: Context) {
+ private val connectivityManager = context.getSystemService(ConnectivityManager::class.java)!!
+
+ fun networkCapabilitiesFlow(): Flow = callbackFlow {
+ val callback = object : NetworkCallback() {
+ override fun onCapabilitiesChanged(
+ network: Network,
+ networkCapabilities: NetworkCapabilities,
+ ) {
+ trySend(networkCapabilities)
+ Log.d(TAG, "onCapabilitiesChanged: $networkCapabilities")
+ }
+
+ override fun onLost(network: Network) {
+ trySend(NetworkCapabilities())
+ Log.d(TAG, "onLost")
+ }
+ }
+ trySend(getNetworkCapabilities())
+ connectivityManager.registerDefaultNetworkCallback(callback)
+
+ awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+ }.conflate().flowOn(Dispatchers.Default)
+
+ private fun getNetworkCapabilities(): NetworkCapabilities =
+ connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
+ ?: NetworkCapabilities()
+
+ private companion object {
+ private const val TAG = "ConnectivityRepository"
+ }
+}
diff --git a/src/com/android/settings/network/InternetPreferenceControllerV2.kt b/src/com/android/settings/network/InternetPreferenceControllerV2.kt
index f9d56189476..351aca83526 100644
--- a/src/com/android/settings/network/InternetPreferenceControllerV2.kt
+++ b/src/com/android/settings/network/InternetPreferenceControllerV2.kt
@@ -22,7 +22,6 @@ import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import com.android.settings.R
import com.android.settings.core.BasePreferenceController
-import com.android.settings.wifi.WifiSummaryRepository
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
class InternetPreferenceControllerV2(context: Context, preferenceKey: String) :
@@ -40,7 +39,7 @@ class InternetPreferenceControllerV2(context: Context, preferenceKey: String) :
}
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
- WifiSummaryRepository(mContext).summaryFlow()
+ InternetPreferenceRepository(mContext).summaryFlow()
.collectLatestWithLifecycle(viewLifecycleOwner) {
preference?.summary = it
}
diff --git a/src/com/android/settings/network/InternetPreferenceRepository.kt b/src/com/android/settings/network/InternetPreferenceRepository.kt
new file mode 100644
index 00000000000..30a98d7cb1d
--- /dev/null
+++ b/src/com/android/settings/network/InternetPreferenceRepository.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.network
+
+import android.content.Context
+import android.net.NetworkCapabilities
+import android.net.wifi.WifiManager
+import android.provider.Settings
+import android.util.Log
+import com.android.settings.R
+import com.android.settings.wifi.WifiSummaryRepository
+import com.android.settings.wifi.repository.WifiRepository
+import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onEach
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class InternetPreferenceRepository(
+ private val context: Context,
+ private val connectivityRepository: ConnectivityRepository = ConnectivityRepository(context),
+ private val wifiSummaryRepository: WifiSummaryRepository = WifiSummaryRepository(context),
+ private val wifiRepository: WifiRepository = WifiRepository(context),
+ private val airplaneModeOnFlow: Flow =
+ context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON),
+) {
+
+ fun summaryFlow(): Flow = connectivityRepository.networkCapabilitiesFlow()
+ .flatMapLatest { capabilities -> capabilities.summaryFlow() }
+ .onEach { Log.d(TAG, "summaryFlow: $it") }
+ .conflate()
+ .flowOn(Dispatchers.Default)
+
+ private fun NetworkCapabilities.summaryFlow(): Flow {
+ if (hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
+ hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ ) {
+ for (transportType in transportTypes) {
+ if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
+ return wifiSummaryRepository.summaryFlow()
+ }
+ }
+ }
+ return defaultSummaryFlow()
+ }
+
+ private fun defaultSummaryFlow(): Flow = combine(
+ airplaneModeOnFlow,
+ wifiRepository.wifiStateFlow(),
+ ) { airplaneModeOn: Boolean, wifiState: Int ->
+ context.getString(
+ if (airplaneModeOn && wifiState != WifiManager.WIFI_STATE_ENABLED) {
+ R.string.condition_airplane_title
+ } else {
+ R.string.networks_available
+ }
+ )
+ }
+
+ private companion object {
+ private const val TAG = "InternetPreferenceRepo"
+ }
+}
diff --git a/src/com/android/settings/network/apn/ApnSettings.java b/src/com/android/settings/network/apn/ApnSettings.java
index 5249eb27f22..2debba1d99a 100644
--- a/src/com/android/settings/network/apn/ApnSettings.java
+++ b/src/com/android/settings/network/apn/ApnSettings.java
@@ -99,6 +99,8 @@ public class ApnSettings extends RestrictedSettingsFragment
private UserManager mUserManager;
private int mSubId;
private PreferredApnRepository mPreferredApnRepository;
+ @Nullable
+ private String mPreferredApnKey;
private String mMvnoType;
private String mMvnoMatchData;
@@ -175,6 +177,7 @@ public class ApnSettings extends RestrictedSettingsFragment
});
mPreferredApnRepository.collectPreferredApn(viewLifecycleOwner, (preferredApn) -> {
+ mPreferredApnKey = preferredApn;
final PreferenceGroup apnPreferenceList = findPreference(APN_LIST);
for (int i = 0; i < apnPreferenceList.getPreferenceCount(); i++) {
ApnPreference apnPreference = (ApnPreference) apnPreferenceList.getPreference(i);
@@ -259,6 +262,7 @@ public class ApnSettings extends RestrictedSettingsFragment
((type == null) || type.contains(ApnSetting.TYPE_DEFAULT_STRING));
pref.setDefaultSelectable(defaultSelectable);
if (defaultSelectable) {
+ pref.setIsChecked(key.equals(mPreferredApnKey));
apnList.add(pref);
} else {
mmsApnList.add(pref);
diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java
new file mode 100644
index 00000000000..c6ecaa0a56d
--- /dev/null
+++ b/src/com/android/settings/notification/modes/IconUtil.java
@@ -0,0 +1,69 @@
+/*
+ * 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.notification.modes;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.OvalShape;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+
+import com.android.settings.R;
+import com.android.settingslib.Utils;
+
+class IconUtil {
+
+ static Drawable applyTint(@NonNull Context context, @NonNull Drawable icon) {
+ icon = icon.mutate();
+ icon.setTintList(
+ Utils.getColorAttr(context, android.R.attr.colorControlNormal));
+ return icon;
+ }
+
+ /**
+ * Returns a variant of the supplied {@code icon} to be used in the icon picker. The inner icon
+ * is 36x36dp and it's contained into a circle of diameter 54dp.
+ */
+ static Drawable makeIconCircle(@NonNull Context context, @NonNull Drawable icon) {
+ ShapeDrawable background = new ShapeDrawable(new OvalShape());
+ background.getPaint().setColor(Utils.getColorAttrDefaultColor(context,
+ com.android.internal.R.attr.materialColorSecondaryContainer));
+ icon.setTint(Utils.getColorAttrDefaultColor(context,
+ com.android.internal.R.attr.materialColorOnSecondaryContainer));
+
+ LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, icon });
+
+ int circleDiameter = context.getResources().getDimensionPixelSize(
+ R.dimen.zen_mode_icon_list_circle_diameter);
+ int iconSize = context.getResources().getDimensionPixelSize(
+ R.dimen.zen_mode_icon_list_icon_size);
+ int iconPadding = (circleDiameter - iconSize) / 2;
+ layerDrawable.setBounds(0, 0, circleDiameter, circleDiameter);
+ layerDrawable.setLayerInset(1, iconPadding, iconPadding, iconPadding, iconPadding);
+
+ return layerDrawable;
+ }
+
+ static Drawable makeIconCircle(@NonNull Context context, @DrawableRes int iconResId) {
+ return makeIconCircle(context, checkNotNull(context.getDrawable(iconResId)));
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenMode.java b/src/com/android/settings/notification/modes/ZenMode.java
index 1be7e5fda1e..aca959f0f51 100644
--- a/src/com/android/settings/notification/modes/ZenMode.java
+++ b/src/com/android/settings/notification/modes/ZenMode.java
@@ -204,6 +204,14 @@ class ZenMode {
: new ZenDeviceEffects.Builder().build();
}
+ public boolean canEditName() {
+ return !isManualDnd();
+ }
+
+ public boolean canEditIcon() {
+ return !isManualDnd();
+ }
+
public boolean canBeDeleted() {
return !mIsManualDnd;
}
diff --git a/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java
new file mode 100644
index 00000000000..5695fbcbc47
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java
@@ -0,0 +1,64 @@
+/*
+ * 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.notification.modes;
+
+import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.widget.ActionButtonsPreference;
+
+class ZenModeActionsPreferenceController extends AbstractZenModePreferenceController {
+
+ private ActionButtonsPreference mPreference;
+
+ ZenModeActionsPreferenceController(@NonNull Context context, @NonNull String key,
+ @Nullable ZenModesBackend backend) {
+ super(context, key, backend);
+ }
+
+ @Override
+ void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ ActionButtonsPreference buttonsPreference = (ActionButtonsPreference) preference;
+
+ // TODO: b/346278854 - Add rename action (with setButton1Enabled(zenMode.canEditName())
+ buttonsPreference.setButton1Text(R.string.zen_mode_action_change_name);
+ buttonsPreference.setButton1Icon(R.drawable.ic_mode_edit);
+ buttonsPreference.setButton1Enabled(false);
+
+ buttonsPreference.setButton2Text(R.string.zen_mode_action_change_icon);
+ buttonsPreference.setButton2Icon(R.drawable.ic_zen_mode_action_change_icon);
+ buttonsPreference.setButton2Enabled(zenMode.canEditIcon());
+ buttonsPreference.setButton2OnClickListener(v -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(MODE_ID, zenMode.getId());
+ new SubSettingLauncher(mContext)
+ .setDestination(ZenModeIconPickerFragment.class.getName())
+ // TODO: b/332937635 - Update metrics category
+ .setSourceMetricsCategory(0)
+ .setArguments(bundle)
+ .launch();
+ });
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java
new file mode 100644
index 00000000000..8517af16975
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java
@@ -0,0 +1,56 @@
+/*
+ * 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.notification.modes;
+
+import android.content.Context;
+import android.service.notification.ZenModeConfig;
+
+import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+import androidx.preference.TwoStatePreference;
+
+/**
+ * Preference controller controlling whether a time schedule-based mode ends at the next alarm.
+ */
+class ZenModeExitAtAlarmPreferenceController extends
+ AbstractZenModePreferenceController implements Preference.OnPreferenceChangeListener {
+ private ZenModeConfig.ScheduleInfo mSchedule;
+
+ ZenModeExitAtAlarmPreferenceController(Context context,
+ String key, ZenModesBackend backend) {
+ super(context, key, backend);
+ }
+
+ @Override
+ public void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId());
+ ((TwoStatePreference) preference).setChecked(mSchedule.exitAtAlarm);
+ }
+
+ @Override
+ public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+ final boolean exitAtAlarm = (Boolean) newValue;
+ if (mSchedule.exitAtAlarm != exitAtAlarm) {
+ mSchedule.exitAtAlarm = exitAtAlarm;
+ return saveMode(mode -> {
+ mode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(mSchedule));
+ return mode;
+ });
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index 7084f51a922..87165b85d72 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -38,6 +38,7 @@ public class ZenModeFragment extends ZenModeFragmentBase {
List prefControllers = new ArrayList<>();
prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend));
prefControllers.add(new ZenModeButtonPreferenceController(context, "activate", mBackend));
+ prefControllers.add(new ZenModeActionsPreferenceController(context, "actions", mBackend));
prefControllers.add(new ZenModePeopleLinkPreferenceController(
context, "zen_mode_people", mBackend));
prefControllers.add(new ZenModeAppsLinkPreferenceController(
diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
index 5e6cfa5084e..e086524a427 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
@@ -51,12 +51,12 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
if (bundle != null && bundle.containsKey(MODE_ID)) {
String id = bundle.getString(MODE_ID);
if (!reloadMode(id)) {
- Log.d(TAG, "Mode id " + id + " not found");
+ Log.e(TAG, "Mode id " + id + " not found");
toastAndFinish();
return;
}
} else {
- Log.d(TAG, "Mode id required to set mode config settings");
+ Log.e(TAG, "Mode id required to set mode config settings");
toastAndFinish();
return;
}
diff --git a/src/com/android/settings/notification/modes/ZenModeHeaderController.java b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
index ba6e9d9a22e..d8f0a6730fa 100644
--- a/src/com/android/settings/notification/modes/ZenModeHeaderController.java
+++ b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
@@ -63,9 +63,8 @@ class ZenModeHeaderController extends AbstractZenModePreferenceController {
FutureUtil.whenDone(
zenMode.getIcon(mContext, IconLoader.getInstance()),
- icon -> mHeaderController.setIcon(icon)
- .setLabel(zenMode.getRule().getName())
- .done(false /* rebindActions */),
+ icon -> mHeaderController.setIcon(IconUtil.applyTint(mContext, icon))
+ .done(/* rebindActions= */ false),
mContext.getMainExecutor());
}
}
diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java b/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java
new file mode 100644
index 00000000000..950849e0056
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java
@@ -0,0 +1,49 @@
+/*
+ * 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.notification.modes;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+
+import com.android.settings.R;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+public class ZenModeIconPickerFragment extends ZenModeFragmentBase {
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.modes_icon_picker;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ // TODO: b/332937635 - make this the correct metrics category
+ return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
+ }
+
+ @Override
+ protected List createPreferenceControllers(Context context) {
+ return ImmutableList.of(
+ new ZenModeIconPickerIconPreferenceController(context, "current_icon", this,
+ mBackend),
+ new ZenModeIconPickerListPreferenceController(context, "icon_list", this,
+ mBackend));
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java
new file mode 100644
index 00000000000..9eaaa973305
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java
@@ -0,0 +1,59 @@
+/*
+ * 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.notification.modes;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.widget.LayoutPreference;
+
+class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenceController {
+
+ private final DashboardFragment mFragment;
+ private EntityHeaderController mHeaderController;
+
+ ZenModeIconPickerIconPreferenceController(@NonNull Context context, @NonNull String key,
+ @NonNull DashboardFragment fragment, @Nullable ZenModesBackend backend) {
+ super(context, key, backend);
+ mFragment = fragment;
+ }
+
+ @Override
+ void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ preference.setSelectable(false);
+
+ if (mHeaderController == null) {
+ final LayoutPreference pref = (LayoutPreference) preference;
+ mHeaderController = EntityHeaderController.newInstance(
+ mFragment.getActivity(),
+ mFragment,
+ pref.findViewById(R.id.entity_header));
+ }
+
+ FutureUtil.whenDone(
+ zenMode.getIcon(mContext, IconLoader.getInstance()),
+ icon -> mHeaderController.setIcon(IconUtil.applyTint(mContext, icon))
+ .done(/* rebindActions= */ false),
+ mContext.getMainExecutor());
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java
new file mode 100644
index 00000000000..b07c26f9e18
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java
@@ -0,0 +1,170 @@
+/*
+ * 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.notification.modes;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.widget.LayoutPreference;
+
+import com.google.common.collect.ImmutableList;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenceController {
+
+ private final DashboardFragment mFragment;
+ private IconAdapter mAdapter;
+
+ ZenModeIconPickerListPreferenceController(@NonNull Context context, @NonNull String key,
+ @NonNull DashboardFragment fragment, @Nullable ZenModesBackend backend) {
+ super(context, key, backend);
+ mFragment = fragment;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ LayoutPreference pref = screen.findPreference(getPreferenceKey());
+ if (pref == null) {
+ return;
+ }
+
+ if (mAdapter == null) {
+ // TODO: b/333901673 - This is just an example; replace with correct list.
+ List exampleIcons =
+ Arrays.stream(android.R.drawable.class.getFields())
+ .filter(
+ f -> Modifier.isStatic(f.getModifiers())
+ && f.getName().startsWith("ic_"))
+ .sorted(Comparator.comparing(Field::getName))
+ .limit(20)
+ .map(f -> {
+ try {
+ return new IconInfo(f.getInt(null), f.getName());
+ } catch (IllegalAccessException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .toList();
+ mAdapter = new IconAdapter(exampleIcons);
+ }
+ RecyclerView recyclerView = pref.findViewById(R.id.icon_list);
+ recyclerView.setLayoutManager(new AutoFitGridLayoutManager(mContext));
+ recyclerView.setAdapter(mAdapter);
+ recyclerView.setHasFixedSize(true);
+ }
+
+ @VisibleForTesting
+ void onIconSelected(@DrawableRes int resId) {
+ saveMode(mode -> {
+ mode.getRule().setIconResId(resId);
+ return mode;
+ });
+ mFragment.finish();
+ }
+
+ @Override
+ void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ // Nothing to do, the current icon is shown in a different preference.
+ }
+
+ private record IconInfo(@DrawableRes int resId, String description) { }
+
+ private class IconHolder extends RecyclerView.ViewHolder {
+
+ private final ImageView mImageView;
+
+ IconHolder(@NonNull View itemView) {
+ super(itemView);
+ mImageView = itemView.findViewById(R.id.icon_image_view);
+ }
+
+ void bindIcon(IconInfo icon) {
+ mImageView.setImageDrawable(
+ IconUtil.makeIconCircle(itemView.getContext(), icon.resId()));
+ itemView.setContentDescription(icon.description());
+ itemView.setOnClickListener(v -> onIconSelected(icon.resId()));
+ }
+ }
+
+ private class IconAdapter extends RecyclerView.Adapter {
+
+ private final ImmutableList mIconResources;
+
+ private IconAdapter(List iconOptions) {
+ mIconResources = ImmutableList.copyOf(iconOptions);
+ }
+
+ @NonNull
+ @Override
+ public IconHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View itemView = LayoutInflater.from(parent.getContext()).inflate(
+ R.layout.modes_icon_list_item, parent, false);
+ return new IconHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull IconHolder holder, int position) {
+ holder.bindIcon(mIconResources.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mIconResources.size();
+ }
+ }
+
+ private static class AutoFitGridLayoutManager extends GridLayoutManager {
+ private final float mColumnWidth;
+
+ AutoFitGridLayoutManager(Context context) {
+ super(context, /* spanCount= */ 1);
+ this.mColumnWidth = context
+ .getResources()
+ .getDimensionPixelSize(R.dimen.zen_mode_icon_list_item_size);
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ final int totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
+ final int spanCount = Math.max(1, (int) (totalSpace / mColumnWidth));
+ setSpanCount(spanCount);
+ super.onLayoutChildren(recycler, state);
+ }
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeListPreference.java b/src/com/android/settings/notification/modes/ZenModeListPreference.java
index a106bddf964..c3daa614343 100644
--- a/src/com/android/settings/notification/modes/ZenModeListPreference.java
+++ b/src/com/android/settings/notification/modes/ZenModeListPreference.java
@@ -23,7 +23,6 @@ import android.os.Bundle;
import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.RestrictedPreference;
-import com.android.settingslib.Utils;
/**
* Preference representing a single mode item on the modes aggregator page. Clicking on this
@@ -59,11 +58,7 @@ class ZenModeListPreference extends RestrictedPreference {
FutureUtil.whenDone(
mZenMode.getIcon(mContext, IconLoader.getInstance()),
- icon -> {
- icon.setTintList(
- Utils.getColorAttr(mContext, android.R.attr.colorControlNormal));
- setIcon(icon);
- },
+ icon -> setIcon(IconUtil.applyTint(mContext, icon)),
mContext.getMainExecutor());
}
}
diff --git a/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java b/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java
new file mode 100644
index 00000000000..4d58097b1dc
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java
@@ -0,0 +1,54 @@
+/*
+ * 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.notification.modes;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+
+import com.android.settings.R;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Settings page to set a schedule for a mode that turns on automatically based on specific days
+ * of the week and times of day.
+ */
+public class ZenModeSetScheduleFragment extends ZenModeFragmentBase {
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.modes_set_schedule;
+ }
+
+ @Override
+ protected List createPreferenceControllers(Context context) {
+ List controllers = new ArrayList<>();
+ controllers.add(
+ new ZenModeSetSchedulePreferenceController(mContext, this, "schedule", mBackend));
+ controllers.add(
+ new ZenModeExitAtAlarmPreferenceController(mContext, "exit_at_alarm", mBackend));
+ return controllers;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ // TODO: b/332937635 - make this the correct metrics category
+ return SettingsEnums.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE;
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
new file mode 100644
index 00000000000..a6008ccd768
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
@@ -0,0 +1,274 @@
+/*
+ * 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.notification.modes;
+
+import android.app.Flags;
+import android.content.Context;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenModeConfig;
+import android.text.format.DateFormat;
+import android.util.ArraySet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.fragment.app.Fragment;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settingslib.widget.LayoutPreference;
+
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.function.Function;
+
+/**
+ * Preference controller for setting the start and end time and days of the week associated with
+ * an automatic zen mode.
+ */
+class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceController {
+ // per-instance to ensure we're always using the current locale
+ // E = day of the week; "EEEEE" is the shortest version; "EEEE" is the full name
+ private final SimpleDateFormat mShortDayFormat = new SimpleDateFormat("EEEEE");
+ private final SimpleDateFormat mLongDayFormat = new SimpleDateFormat("EEEE");
+
+ private static final String TAG = "ZenModeSetSchedulePreferenceController";
+ private Fragment mParent;
+ private ZenModeConfig.ScheduleInfo mSchedule;
+
+ ZenModeSetSchedulePreferenceController(Context context, Fragment parent, String key,
+ ZenModesBackend backend) {
+ super(context, key, backend);
+ mParent = parent;
+ }
+
+ @Override
+ public void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId());
+ LayoutPreference layoutPref = (LayoutPreference) preference;
+
+ TextView start = layoutPref.findViewById(R.id.start_time);
+ start.setText(timeString(mSchedule.startHour, mSchedule.startMinute));
+ start.setOnClickListener(
+ timePickerLauncher(mSchedule.startHour, mSchedule.startMinute, mStartSetter));
+
+ TextView end = layoutPref.findViewById(R.id.end_time);
+ end.setText(timeString(mSchedule.endHour, mSchedule.endMinute));
+ end.setOnClickListener(
+ timePickerLauncher(mSchedule.endHour, mSchedule.endMinute, mEndSetter));
+
+ TextView durationView = layoutPref.findViewById(R.id.schedule_duration);
+ durationView.setText(getScheduleDurationDescription(mSchedule));
+
+ ViewGroup daysContainer = layoutPref.findViewById(R.id.days_of_week_container);
+ setupDayToggles(daysContainer, mSchedule, Calendar.getInstance());
+ }
+
+ private String timeString(int hour, int minute) {
+ final Calendar c = Calendar.getInstance();
+ c.set(Calendar.HOUR_OF_DAY, hour);
+ c.set(Calendar.MINUTE, minute);
+ return DateFormat.getTimeFormat(mContext).format(c.getTime());
+ }
+
+ private boolean isValidTime(int hour, int minute) {
+ return ZenModeConfig.isValidHour(hour) && ZenModeConfig.isValidMinute(minute);
+ }
+
+ private String getScheduleDurationDescription(ZenModeConfig.ScheduleInfo schedule) {
+ final int startMin = 60 * schedule.startHour + schedule.startMinute;
+ final int endMin = 60 * schedule.endHour + schedule.endMinute;
+ final boolean nextDay = startMin >= endMin;
+
+ Duration scheduleDuration;
+ if (nextDay) {
+ // add one day's worth of minutes (24h x 60min) to end minute for end time calculation
+ int endMinNextDay = endMin + (24 * 60);
+ scheduleDuration = Duration.ofMinutes(endMinNextDay - startMin);
+ } else {
+ scheduleDuration = Duration.ofMinutes(endMin - startMin);
+ }
+
+ int hours = scheduleDuration.toHoursPart();
+ int minutes = scheduleDuration.minusHours(hours).toMinutesPart();
+ return mContext.getString(R.string.zen_mode_schedule_duration, hours, minutes);
+ }
+
+ @VisibleForTesting
+ protected Function updateScheduleMode(ZenModeConfig.ScheduleInfo schedule) {
+ return (zenMode) -> {
+ zenMode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(schedule));
+ if (Flags.modesApi() && Flags.modesUi()) {
+ zenMode.getRule().setTriggerDescription(
+ SystemZenRules.getTriggerDescriptionForScheduleTime(mContext, schedule));
+ }
+ return zenMode;
+ };
+ }
+
+ private ZenModeTimePickerFragment.TimeSetter mStartSetter = (hour, minute) -> {
+ if (!isValidTime(hour, minute)) {
+ return;
+ }
+ if (hour == mSchedule.startHour && minute == mSchedule.startMinute) {
+ return;
+ }
+ mSchedule.startHour = hour;
+ mSchedule.startMinute = minute;
+ saveMode(updateScheduleMode(mSchedule));
+ };
+
+ private ZenModeTimePickerFragment.TimeSetter mEndSetter = (hour, minute) -> {
+ if (!isValidTime(hour, minute)) {
+ return;
+ }
+ if (hour == mSchedule.endHour && minute == mSchedule.endMinute) {
+ return;
+ }
+ mSchedule.endHour = hour;
+ mSchedule.endMinute = minute;
+ saveMode(updateScheduleMode(mSchedule));
+ };
+
+ private View.OnClickListener timePickerLauncher(int hour, int minute,
+ ZenModeTimePickerFragment.TimeSetter timeSetter) {
+ return v -> {
+ final ZenModeTimePickerFragment frag = new ZenModeTimePickerFragment(mContext, hour,
+ minute, timeSetter);
+ frag.show(mParent.getParentFragmentManager(), TAG);
+ };
+ }
+
+ protected static int[] getDaysOfWeekForLocale(Calendar c) {
+ int[] daysOfWeek = new int[7];
+ int currentDay = c.getFirstDayOfWeek();
+ for (int i = 0; i < daysOfWeek.length; i++) {
+ if (currentDay > 7) currentDay = 1;
+ daysOfWeek[i] = currentDay;
+ currentDay++;
+ }
+ return daysOfWeek;
+ }
+
+ @VisibleForTesting
+ protected void setupDayToggles(ViewGroup dayContainer, ZenModeConfig.ScheduleInfo schedule,
+ Calendar c) {
+ int[] daysOfWeek = getDaysOfWeekForLocale(c);
+
+ // Index in daysOfWeek is associated with the [idx]'th object in the list of days in the
+ // layout. Note that because the order of the days of the week may differ per locale, this
+ // is not necessarily the same as the actual value of the day number at that index.
+ for (int i = 0; i < daysOfWeek.length; i++) {
+ ToggleButton dayToggle = dayContainer.findViewById(resIdForDayIndex(i));
+ if (dayToggle == null) {
+ continue;
+ }
+
+ final int day = daysOfWeek[i];
+ c.set(Calendar.DAY_OF_WEEK, day);
+
+ // find current setting for this day
+ boolean dayEnabled = false;
+ if (schedule.days != null) {
+ for (int idx = 0; idx < schedule.days.length; idx++) {
+ if (schedule.days[idx] == day) {
+ dayEnabled = true;
+ break;
+ }
+ }
+ }
+
+ // On/off is indicated by visuals, and both states share the shortest (one-character)
+ // day label.
+ dayToggle.setTextOn(mShortDayFormat.format(c.getTime()));
+ dayToggle.setTextOff(mShortDayFormat.format(c.getTime()));
+ dayToggle.setContentDescription(mLongDayFormat.format(c.getTime()));
+
+ dayToggle.setChecked(dayEnabled);
+ dayToggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (updateScheduleDays(schedule, day, isChecked)) {
+ saveMode(updateScheduleMode(schedule));
+ }
+ });
+
+ // If display and text settings cause the text to be larger than its containing box,
+ // don't show scrollbars.
+ dayToggle.setVerticalScrollBarEnabled(false);
+ dayToggle.setHorizontalScrollBarEnabled(false);
+ }
+ }
+
+ // Updates the set of enabled days in provided schedule to either turn on or off the given day.
+ // The format of days in ZenModeConfig.ScheduleInfo is an array of days, where inclusion means
+ // the schedule is set to run on that day. Returns whether anything was changed.
+ @VisibleForTesting
+ protected static boolean updateScheduleDays(ZenModeConfig.ScheduleInfo schedule, int day,
+ boolean set) {
+ // Build a set representing the days that are currently set in mSchedule.
+ ArraySet daySet = new ArraySet();
+ if (schedule.days != null) {
+ for (int i = 0; i < schedule.days.length; i++) {
+ daySet.add(schedule.days[i]);
+ }
+ }
+
+ if (daySet.contains(day) != set) {
+ if (set) {
+ daySet.add(day);
+ } else {
+ daySet.remove(day);
+ }
+
+ // rebuild days array for mSchedule
+ final int[] out = new int[daySet.size()];
+ for (int i = 0; i < daySet.size(); i++) {
+ out[i] = daySet.valueAt(i);
+ }
+ Arrays.sort(out);
+ schedule.days = out;
+ return true;
+ }
+ // If the setting is the same as it was before, no need to update anything.
+ return false;
+ }
+
+ protected static int resIdForDayIndex(int idx) {
+ switch (idx) {
+ case 0:
+ return R.id.day0;
+ case 1:
+ return R.id.day1;
+ case 2:
+ return R.id.day2;
+ case 3:
+ return R.id.day3;
+ case 4:
+ return R.id.day4;
+ case 5:
+ return R.id.day5;
+ case 6:
+ return R.id.day6;
+ default:
+ return 0; // unknown
+ }
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
index a3bc508cfbb..14d5d59a19d 100644
--- a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
@@ -16,6 +16,7 @@
package com.android.settings.notification.modes;
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
@@ -32,13 +33,13 @@ import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.PrimarySwitchPreference;
/**
- * Preference controller for the link
+ * Preference controller for the link to an individual mode's configuration page.
*/
-public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController {
+class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController {
@VisibleForTesting
protected static final String AUTOMATIC_TRIGGER_PREF_KEY = "zen_automatic_trigger_settings";
- public ZenModeSetTriggerLinkPreferenceController(Context context, String key,
+ ZenModeSetTriggerLinkPreferenceController(Context context, String key,
ZenModesBackend backend) {
super(context, key, backend);
}
@@ -66,6 +67,16 @@ public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePr
// TODO: b/341961712 - direct preference to app-owned intent if available
switch (zenMode.getRule().getType()) {
+ case TYPE_SCHEDULE_TIME:
+ switchPref.setTitle(R.string.zen_mode_set_schedule_link);
+ switchPref.setSummary(zenMode.getRule().getTriggerDescription());
+ switchPref.setIntent(new SubSettingLauncher(mContext)
+ .setDestination(ZenModeSetScheduleFragment.class.getName())
+ // TODO: b/332937635 - set correct metrics category
+ .setSourceMetricsCategory(0)
+ .setArguments(bundle)
+ .toIntent());
+ break;
case TYPE_SCHEDULE_CALENDAR:
switchPref.setTitle(R.string.zen_mode_set_calendar_link);
switchPref.setSummary(zenMode.getRule().getTriggerDescription());
diff --git a/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java b/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java
new file mode 100644
index 00000000000..d8e1b38875b
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java
@@ -0,0 +1,76 @@
+/*
+ * 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.notification.modes;
+
+import android.app.Dialog;
+import android.app.TimePickerDialog;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.format.DateFormat;
+import android.widget.TimePicker;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/**
+ * Dialog that shows when a user selects a (start or end) time to edit for a schedule-based mode.
+ */
+public class ZenModeTimePickerFragment extends InstrumentedDialogFragment implements
+ TimePickerDialog.OnTimeSetListener {
+ private final Context mContext;
+ private final TimeSetter mTimeSetter;
+ private final int mHour;
+ private final int mMinute;
+
+ public ZenModeTimePickerFragment(Context context, int hour, int minute,
+ @NonNull TimeSetter timeSetter) {
+ super();
+ mContext = context;
+ mHour = hour;
+ mMinute = minute;
+ mTimeSetter = timeSetter;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new TimePickerDialog(mContext, this, mHour, mMinute,
+ DateFormat.is24HourFormat(mContext));
+ }
+
+ /**
+ * Calls the provided TimeSetter's setTime() method when a time is set on the TimePicker.
+ */
+ public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
+ mTimeSetter.setTime(hourOfDay, minute);
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ // TODO: b/332937635 - set correct metrics category (or decide to keep this one?)
+ return SettingsEnums.DIALOG_ZEN_TIMEPICKER;
+ }
+
+ /**
+ * Interface for a method to pass into the TimePickerFragment that specifies what to do when the
+ * time is updated.
+ */
+ public interface TimeSetter {
+ void setTime(int hour, int minute);
+ }
+}
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
index 30fd619d9df..7f362c32904 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
@@ -23,6 +23,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_W
import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
+import static com.android.systemui.biometrics.Utils.toBitmap;
+
import android.app.Activity;
import android.app.KeyguardManager;
import android.app.RemoteLockscreenValidationSession;
@@ -35,6 +37,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.UserProperties;
import android.content.res.Configuration;
+import android.graphics.Bitmap;
import android.graphics.Color;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricPrompt;
@@ -215,9 +218,10 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity {
&& android.multiuser.Flags.usePrivateSpaceIconInBiometricPrompt()
&& hasSetBiometricDialogAdvanced(mContext, getLaunchedFromUid())
) {
- int iconResId = intent.getIntExtra(CUSTOM_BIOMETRIC_PROMPT_LOGO_RES_ID_KEY, 0);
+ final int iconResId = intent.getIntExtra(CUSTOM_BIOMETRIC_PROMPT_LOGO_RES_ID_KEY, 0);
if (iconResId != 0) {
- promptInfo.setLogoRes(iconResId);
+ final Bitmap iconBitmap = toBitmap(mContext.getDrawable(iconResId));
+ promptInfo.setLogo(iconResId, iconBitmap);
}
String logoDescription = intent.getStringExtra(
CUSTOM_BIOMETRIC_PROMPT_LOGO_DESCRIPTION_KEY);
diff --git a/src/com/android/settings/privatespace/PrivateSpaceCreationFragment.java b/src/com/android/settings/privatespace/PrivateSpaceCreationFragment.java
index eb8864467b2..ce85d7238cc 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceCreationFragment.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceCreationFragment.java
@@ -157,22 +157,26 @@ public class PrivateSpaceCreationFragment extends InstrumentedFragment {
/** Start new activity in private profile to add an account to private profile */
private void startAccountLogin() {
- Intent intent = new Intent(getContext(), PrivateProfileContextHelperActivity.class);
- intent.putExtra(EXTRA_ACTION_TYPE, ACCOUNT_LOGIN_ACTION);
- mMetricsFeatureProvider.action(
- getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_ACCOUNT_LOGIN_START);
- getActivity().startActivityForResult(intent, ACCOUNT_LOGIN_ACTION);
+ if (isAdded() && getContext() != null && getActivity() != null) {
+ Intent intent = new Intent(getContext(), PrivateProfileContextHelperActivity.class);
+ intent.putExtra(EXTRA_ACTION_TYPE, ACCOUNT_LOGIN_ACTION);
+ mMetricsFeatureProvider.action(
+ getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_ACCOUNT_LOGIN_START);
+ getActivity().startActivityForResult(intent, ACCOUNT_LOGIN_ACTION);
+ }
}
private void registerReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PROFILE_ACCESSIBLE);
- getActivity().registerReceiver(mProfileAccessReceiver, filter);
+ if (getContext() != null) {
+ getContext().registerReceiver(mProfileAccessReceiver, filter);
+ }
}
private void unRegisterReceiver() {
- if (mProfileAccessReceiver != null) {
- getActivity().unregisterReceiver(mProfileAccessReceiver);
+ if (mProfileAccessReceiver != null && isAdded() && getContext() != null) {
+ getContext().unregisterReceiver(mProfileAccessReceiver);
}
}
}
diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java
index 1f0d824d087..b48c71727d0 100644
--- a/src/com/android/settings/users/UserDetailsSettings.java
+++ b/src/com/android/settings/users/UserDetailsSettings.java
@@ -127,7 +127,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
public void onResume() {
super.onResume();
mSwitchUserPref.setEnabled(canSwitchUserNow());
- if (mGuestUserAutoCreated) {
+ if (mUserInfo.isGuest() && mGuestUserAutoCreated) {
mRemoveUserPref.setEnabled((mUserInfo.flags & UserInfo.FLAG_INITIALIZED) != 0);
}
}
diff --git a/src/com/android/settings/wifi/repository/WifiRepository.kt b/src/com/android/settings/wifi/repository/WifiRepository.kt
new file mode 100644
index 00000000000..77f0b1b47cf
--- /dev/null
+++ b/src/com/android/settings/wifi/repository/WifiRepository.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.wifi.repository
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.wifi.WifiManager
+import android.util.Log
+import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+class WifiRepository(
+ private val context: Context,
+ private val wifiStateChangedActionFlow: Flow =
+ context.broadcastReceiverFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)),
+) {
+
+ fun wifiStateFlow() = wifiStateChangedActionFlow
+ .map { intent ->
+ intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN)
+ }
+ .onEach { Log.d(TAG, "wifiStateFlow: $it") }
+
+ private companion object {
+ private const val TAG = "WifiRepository"
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java
index a35ef45f518..d28ab3b928b 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java
@@ -27,9 +27,12 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.input.InputManager;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.FeatureFlagUtils;
import android.view.InputDevice;
@@ -39,13 +42,23 @@ import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater;
+import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.dock.DockUpdater;
import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater;
import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.flags.Flags;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.search.SearchIndexableRaw;
+
+import com.google.common.collect.ImmutableList;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
@@ -57,11 +70,16 @@ import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplicationPackageManager;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowApplicationPackageManager.class, ShadowBluetoothAdapter.class})
+@Config(shadows = {ShadowApplicationPackageManager.class, ShadowBluetoothUtils.class,
+ ShadowBluetoothAdapter.class})
public class ConnectedDeviceGroupControllerTest {
private static final String PREFERENCE_KEY_1 = "pref_key_1";
+ private static final String DEVICE_NAME = "device";
@Mock
private DashboardFragment mDashboardFragment;
@@ -79,6 +97,14 @@ public class ConnectedDeviceGroupControllerTest {
private PreferenceManager mPreferenceManager;
@Mock
private InputManager mInputManager;
+ @Mock
+ private CachedBluetoothDeviceManager mCachedDeviceManager;
+ @Mock
+ private LocalBluetoothManager mLocalBluetoothManager;
+ @Mock
+ private CachedBluetoothDevice mCachedDevice;
+ @Mock
+ private BluetoothDevice mDevice;
private ShadowApplicationPackageManager mPackageManager;
private PreferenceGroup mPreferenceGroup;
@@ -86,6 +112,9 @@ public class ConnectedDeviceGroupControllerTest {
private Preference mPreference;
private ConnectedDeviceGroupController mConnectedDeviceGroupController;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -102,11 +131,20 @@ public class ConnectedDeviceGroupControllerTest {
when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager);
when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{});
+ ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
+ mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+ when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
+
mConnectedDeviceGroupController = new ConnectedDeviceGroupController(mContext);
mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater);
mConnectedDeviceGroupController.mPreferenceGroup = mPreferenceGroup;
+ when(mCachedDevice.getName()).thenReturn(DEVICE_NAME);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
+ ImmutableList.of(mCachedDevice));
+
FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES,
true);
}
@@ -267,4 +305,27 @@ public class ConnectedDeviceGroupControllerTest {
mConnectedDeviceGroupController.onStart();
mConnectedDeviceGroupController.onStop();
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_BONDED_BLUETOOTH_DEVICE_SEARCHABLE)
+ public void updateDynamicRawDataToIndex_deviceNotBonded_deviceIsNotSearchable() {
+ when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+ List searchData = new ArrayList<>();
+
+ mConnectedDeviceGroupController.updateDynamicRawDataToIndex(searchData);
+
+ assertThat(searchData).isEmpty();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_BONDED_BLUETOOTH_DEVICE_SEARCHABLE)
+ public void updateDynamicRawDataToIndex_deviceBonded_deviceIsSearchable() {
+ when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ List searchData = new ArrayList<>();
+
+ mConnectedDeviceGroupController.updateDynamicRawDataToIndex(searchData);
+
+ assertThat(searchData).isNotEmpty();
+ assertThat(searchData.get(0).key).contains(DEVICE_NAME);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceControllerTest.java
index 1a9d09e4148..19221a65abe 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceControllerTest.java
@@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -34,6 +35,7 @@ import static org.robolectric.Shadows.shadowOf;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.os.Looper;
@@ -94,28 +96,28 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
@Mock private VolumeControlProfile mVolumeControl;
@Mock private TwoStatePreference mPreference;
+ @Mock private BluetoothLeBroadcastMetadata mMetadata;
private AudioSharingCompatibilityPreferenceController mController;
- private ShadowBluetoothAdapter mShadowBluetoothAdapter;
- private LocalBluetoothManager mLocalBluetoothManager;
private FakeFeatureFactory mFeatureFactory;
private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner;
@Before
public void setUp() {
- mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- mShadowBluetoothAdapter.setEnabled(true);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ ShadowBluetoothAdapter shadowBluetoothAdapter =
+ Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
- mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+ LocalBluetoothManager localBluetoothManager = Utils.getLocalBtManager(mContext);
mFeatureFactory = FakeFeatureFactory.setupForTest();
- when(mLocalBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
- when(mLocalBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
+ when(localBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
+ when(localBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
@@ -133,7 +135,7 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
verify(mBroadcast)
.registerServiceCallBack(
any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
- verify(mBtProfileManager, times(0)).addServiceListener(mController);
+ verify(mBtProfileManager, never()).addServiceListener(mController);
}
@Test
@@ -141,7 +143,7 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
when(mBroadcast.isProfileReady()).thenReturn(false);
mController.onStart(mLifecycleOwner);
- verify(mBroadcast, times(0))
+ verify(mBroadcast, never())
.registerServiceCallBack(
any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
verify(mBtProfileManager).addServiceListener(mController);
@@ -151,7 +153,7 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
public void onStart_flagOff_doNothing() {
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
mController.onStart(mLifecycleOwner);
- verify(mBroadcast, times(0))
+ verify(mBroadcast, never())
.registerServiceCallBack(
any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
}
@@ -170,9 +172,9 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
mController.setCallbacksRegistered(true);
mController.onStop(mLifecycleOwner);
- verify(mBroadcast, times(0))
+ verify(mBroadcast, never())
.unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
- verify(mBtProfileManager, times(0)).removeServiceListener(mController);
+ verify(mBtProfileManager, never()).removeServiceListener(mController);
}
@Test
@@ -224,11 +226,10 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
mController.displayPreference(mScreen);
shadowOf(Looper.getMainLooper()).idle();
verify(mPreference).setEnabled(false);
- verify(mPreference)
- .setSummary(
- eq(mContext.getString(
- R.string
- .audio_sharing_stream_compatibility_disabled_description)));
+ String expected =
+ mContext.getString(
+ R.string.audio_sharing_stream_compatibility_disabled_description);
+ verify(mPreference).setSummary(eq(expected));
}
@Test
@@ -237,10 +238,9 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
mController.displayPreference(mScreen);
shadowOf(Looper.getMainLooper()).idle();
verify(mPreference).setEnabled(true);
- verify(mPreference)
- .setSummary(
- eq(mContext.getString(
- R.string.audio_sharing_stream_compatibility_description)));
+ String expected =
+ mContext.getString(R.string.audio_sharing_stream_compatibility_description);
+ verify(mPreference).setSummary(eq(expected));
}
@Test
@@ -272,8 +272,73 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
public void setCheckedToCurrentValue_returnsFalse() {
when(mBroadcast.getImproveCompatibility()).thenReturn(true);
boolean setChecked = mController.setChecked(true);
- verify(mBroadcast, times(0)).setImproveCompatibility(anyBoolean());
+ verify(mBroadcast, never()).setImproveCompatibility(anyBoolean());
verifyNoInteractions(mFeatureFactory.metricsFeatureProvider);
assertThat(setChecked).isFalse();
}
+
+ @Test
+ public void testBluetoothLeBroadcastCallbacks_refreshPreference() {
+ when(mBroadcast.isEnabled(any())).thenReturn(false);
+ mController.displayPreference(mScreen);
+ shadowOf(Looper.getMainLooper()).idle();
+ verify(mPreference).setEnabled(true);
+ String expected =
+ mContext.getString(R.string.audio_sharing_stream_compatibility_description);
+ verify(mPreference).setSummary(eq(expected));
+
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ mController.mBroadcastCallback.onBroadcastStarted(/* reason= */ 1, /* broadcastId= */ 1);
+ shadowOf(Looper.getMainLooper()).idle();
+ verify(mPreference).setEnabled(false);
+ expected =
+ mContext.getString(
+ R.string.audio_sharing_stream_compatibility_disabled_description);
+ verify(mPreference).setSummary(eq(expected));
+
+ when(mBroadcast.isEnabled(any())).thenReturn(false);
+ mController.mBroadcastCallback.onBroadcastStopped(/* reason= */ 1, /* broadcastId= */ 1);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // Verify one extra setEnabled/setSummary is called other than the first call in
+ // displayPreference.
+ verify(mPreference, times(2)).setEnabled(true);
+ expected = mContext.getString(R.string.audio_sharing_stream_compatibility_description);
+ verify(mPreference, times(2)).setSummary(eq(expected));
+ }
+
+ @Test
+ public void testBluetoothLeBroadcastCallbacks_doNothing() {
+ when(mBroadcast.isEnabled(any())).thenReturn(false);
+ mController.displayPreference(mScreen);
+ shadowOf(Looper.getMainLooper()).idle();
+ verify(mPreference).setEnabled(true);
+ String expected =
+ mContext.getString(R.string.audio_sharing_stream_compatibility_description);
+ verify(mPreference).setSummary(eq(expected));
+
+ // Verify no extra setEnabled/setSummary is called other than call in displayPreference.
+ mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, mMetadata);
+ verify(mPreference).setEnabled(anyBoolean());
+ verify(mPreference).setSummary(any());
+ mController.mBroadcastCallback.onBroadcastUpdated(/* reason= */ 1, /* broadcastId= */ 1);
+ verify(mPreference).setEnabled(anyBoolean());
+ verify(mPreference).setSummary(any());
+ mController.mBroadcastCallback.onPlaybackStarted(/* reason= */ 1, /* broadcastId= */ 1);
+ verify(mPreference).setEnabled(anyBoolean());
+ verify(mPreference).setSummary(any());
+ mController.mBroadcastCallback.onPlaybackStopped(/* reason= */ 1, /* broadcastId= */ 1);
+ verify(mPreference).setEnabled(anyBoolean());
+ verify(mPreference).setSummary(any());
+ mController.mBroadcastCallback.onBroadcastStartFailed(/* reason= */ 1);
+ verify(mPreference).setEnabled(anyBoolean());
+ verify(mPreference).setSummary(any());
+ mController.mBroadcastCallback.onBroadcastStopFailed(/* reason= */ 1);
+ verify(mPreference).setEnabled(anyBoolean());
+ verify(mPreference).setSummary(any());
+ mController.mBroadcastCallback.onBroadcastUpdateFailed(
+ /* reason= */ 1, /* broadcastId= */ 1);
+ verify(mPreference).setEnabled(anyBoolean());
+ verify(mPreference).setSummary(any());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingConfirmDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingConfirmDialogFragmentTest.java
new file mode 100644
index 00000000000..e5facc1d62d
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingConfirmDialogFragmentTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.View;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+ shadows = {
+ ShadowAlertDialogCompat.class,
+ ShadowBluetoothAdapter.class,
+ })
+public class AudioSharingConfirmDialogFragmentTest {
+ @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private Fragment mParent;
+ private AudioSharingConfirmDialogFragment mFragment;
+
+ @Before
+ public void setUp() {
+ cleanUpDialogs();
+ ShadowBluetoothAdapter shadowBluetoothAdapter =
+ Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFragment = new AudioSharingConfirmDialogFragment();
+ mParent = new Fragment();
+ FragmentController.setupFragment(
+ mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+ }
+
+ @After
+ public void tearDown() {
+ cleanUpDialogs();
+ }
+
+ @Test
+ public void getMetricsCategory_correctValue() {
+ assertThat(mFragment.getMetricsCategory())
+ .isEqualTo(SettingsEnums.DIALOG_AUDIO_SHARING_CONFIRMATION);
+ }
+
+ @Test
+ public void onCreateDialog_flagOff_dialogNotExist() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+ AudioSharingConfirmDialogFragment.show(mParent);
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+ }
+
+ @Test
+ public void onCreateDialog_flagOn_showDialog() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+ AudioSharingConfirmDialogFragment.show(mParent);
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ assertThat(dialog.isShowing()).isTrue();
+ }
+
+ @Test
+ public void onCreateDialog_clickOk_dialogDismiss() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+ AudioSharingConfirmDialogFragment.show(mParent);
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(android.R.id.button1);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
+ shadowMainLooper().idle();
+ assertThat(dialog.isShowing()).isFalse();
+ }
+
+ private void cleanUpDialogs() {
+ AlertDialog latestAlertDialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ if (latestAlertDialog != null) {
+ latestAlertDialog.dismiss();
+ ShadowAlertDialogCompat.reset();
+ }
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java
index c1afeaa7806..8e4915cc8ff 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java
@@ -18,22 +18,45 @@ package com.android.settings.connecteddevice.audiosharing;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController;
+import com.android.settings.testutils.shadow.ShadowFragment;
+import com.android.settings.widget.SettingsMainSwitchBar;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowFragment.class})
public class AudioSharingDashboardFragmentTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock private SettingsActivity mActivity;
+ @Mock private SettingsMainSwitchBar mSwitchBar;
+ @Mock private AudioSharingDeviceVolumeGroupController mVolumeGroupController;
+ @Mock private AudioSharingCallAudioPreferenceController mCallAudioController;
+ @Mock private AudioSharingPlaySoundPreferenceController mPlaySoundController;
+ @Mock private AudioStreamsCategoryController mStreamsCategoryController;
+ private final Context mContext = ApplicationProvider.getApplicationContext();
private AudioSharingDashboardFragment mFragment;
@Before
@@ -59,7 +82,42 @@ public class AudioSharingDashboardFragmentTest {
@Test
public void getHelpResource_returnsCorrectResource() {
- assertThat(mFragment.getHelpResource())
- .isEqualTo(R.string.help_url_audio_sharing);
+ assertThat(mFragment.getHelpResource()).isEqualTo(R.string.help_url_audio_sharing);
+ }
+
+ @Test
+ public void onActivityCreated_showSwitchBar() {
+ doReturn(mSwitchBar).when(mActivity).getSwitchBar();
+ mFragment = spy(new AudioSharingDashboardFragment());
+ doReturn(mActivity).when(mFragment).getActivity();
+ doReturn(mContext).when(mFragment).getContext();
+ mFragment.onAttach(mContext);
+ mFragment.onActivityCreated(new Bundle());
+ verify(mSwitchBar).show();
+ }
+
+ @Test
+ public void onAudioSharingStateChanged_updateVisibilityForControllers() {
+ mFragment.setControllers(
+ mVolumeGroupController,
+ mCallAudioController,
+ mPlaySoundController,
+ mStreamsCategoryController);
+ mFragment.onAudioSharingStateChanged();
+ verify(mVolumeGroupController).updateVisibility();
+ verify(mCallAudioController).updateVisibility();
+ verify(mPlaySoundController).updateVisibility();
+ verify(mStreamsCategoryController).updateVisibility();
+ }
+
+ @Test
+ public void onAudioSharingProfilesConnected_registerCallbacksForVolumeGroupController() {
+ mFragment.setControllers(
+ mVolumeGroupController,
+ mCallAudioController,
+ mPlaySoundController,
+ mStreamsCategoryController);
+ mFragment.onAudioSharingProfilesConnected();
+ verify(mVolumeGroupController).onAudioSharingProfilesConnected();
}
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItemTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItemTest.java
index 1bae3d170f8..b23882d3953 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItemTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItemTest.java
@@ -63,4 +63,19 @@ public class AudioSharingDeviceItemTest {
public void creator_newArray() {
assertThat(AudioSharingDeviceItem.CREATOR.newArray(2)).hasLength(2);
}
+
+ @Test
+ public void creator_createFromParcel() {
+ AudioSharingDeviceItem item =
+ new AudioSharingDeviceItem(TEST_NAME, TEST_GROUP_ID, TEST_IS_ACTIVE);
+ Parcel parcel = Parcel.obtain();
+ item.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ AudioSharingDeviceItem itemFromParcel =
+ AudioSharingDeviceItem.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ assertThat(itemFromParcel.getName()).isEqualTo(TEST_NAME);
+ assertThat(itemFromParcel.getGroupId()).isEqualTo(TEST_GROUP_ID);
+ assertThat(itemFromParcel.isActive()).isEqualTo(TEST_IS_ACTIVE);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
index 4336e771c96..c63a1a971a4 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
@@ -18,11 +18,17 @@ package com.android.settings.connecteddevice.audiosharing;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.Pair;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
@@ -34,6 +40,7 @@ import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settingslib.flags.Flags;
@@ -72,30 +79,50 @@ public class AudioSharingDialogFragmentTest {
new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 2, /* isActive= */ false);
private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
new AudioSharingDeviceItem(TEST_DEVICE_NAME3, /* groupId= */ 3, /* isActive= */ false);
+ private static final AudioSharingDialogFragment.DialogEventListener EMPTY_EVENT_LISTENER =
+ new AudioSharingDialogFragment.DialogEventListener() {
+ @Override
+ public void onItemClick(AudioSharingDeviceItem item) {}
+
+ @Override
+ public void onCancelClick() {}
+ };
+ private static final Pair TEST_EVENT_DATA = Pair.create(1, 1);
+ private static final Pair[] TEST_EVENT_DATA_LIST =
+ new Pair[] {TEST_EVENT_DATA};
private Fragment mParent;
private AudioSharingDialogFragment mFragment;
- private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
ShadowAlertDialogCompat.reset();
- mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- mShadowBluetoothAdapter.setEnabled(true);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ ShadowBluetoothAdapter shadowBluetoothAdapter =
+ Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mFragment = new AudioSharingDialogFragment();
mParent = new Fragment();
FragmentController.setupFragment(
mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
}
+ @Test
+ public void getMetricsCategory_correctValue() {
+ assertThat(mFragment.getMetricsCategory())
+ .isEqualTo(SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE);
+ }
+
@Test
public void onCreateDialog_flagOff_dialogNotExist() {
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+ AudioSharingDialogFragment.show(
+ mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
@@ -105,14 +132,20 @@ public class AudioSharingDialogFragmentTest {
@Test
public void onCreateDialog_flagOn_noConnectedDevice() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+ AudioSharingDialogFragment.show(
+ mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
TextView description = dialog.findViewById(R.id.description_text);
+ assertThat(description).isNotNull();
ImageView image = dialog.findViewById(R.id.description_image);
+ assertThat(image).isNotNull();
Button shareBtn = dialog.findViewById(R.id.positive_btn);
+ assertThat(shareBtn).isNotNull();
Button cancelBtn = dialog.findViewById(R.id.negative_btn);
+ assertThat(cancelBtn).isNotNull();
assertThat(dialog.isShowing()).isTrue();
assertThat(description.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(description.getText().toString())
@@ -125,13 +158,22 @@ public class AudioSharingDialogFragmentTest {
@Test
public void onCreateDialog_noConnectedDevice_dialogDismiss() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+ AudioSharingDialogFragment.show(
+ mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(android.R.id.button2).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(android.R.id.button2);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
shadowMainLooper().idle();
assertThat(dialog.isShowing()).isFalse();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
@Test
@@ -139,15 +181,21 @@ public class AudioSharingDialogFragmentTest {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
ArrayList list = new ArrayList<>();
list.add(TEST_DEVICE_ITEM1);
- mFragment.show(mParent, list, (item) -> {});
+ AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
TextView title = dialog.findViewById(R.id.title_text);
+ assertThat(title).isNotNull();
TextView description = dialog.findViewById(R.id.description_text);
+ assertThat(description).isNotNull();
ImageView image = dialog.findViewById(R.id.description_image);
+ assertThat(image).isNotNull();
Button shareBtn = dialog.findViewById(R.id.positive_btn);
+ assertThat(shareBtn).isNotNull();
Button cancelBtn = dialog.findViewById(R.id.negative_btn);
+ assertThat(cancelBtn).isNotNull();
assertThat(dialog.isShowing()).isTrue();
assertThat(title.getText().toString())
.isEqualTo(
@@ -166,12 +214,22 @@ public class AudioSharingDialogFragmentTest {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
ArrayList list = new ArrayList<>();
list.add(TEST_DEVICE_ITEM1);
- mFragment.show(mParent, list, (item) -> {});
+ AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(R.id.negative_btn).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(R.id.negative_btn);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
+ shadowMainLooper().idle();
+
assertThat(dialog.isShowing()).isFalse();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
@Test
@@ -180,13 +238,35 @@ public class AudioSharingDialogFragmentTest {
ArrayList list = new ArrayList<>();
list.add(TEST_DEVICE_ITEM1);
AtomicBoolean isShareBtnClicked = new AtomicBoolean(false);
- mFragment.show(mParent, list, (item) -> isShareBtnClicked.set(true));
+ AudioSharingDialogFragment.show(
+ mParent,
+ list,
+ new AudioSharingDialogFragment.DialogEventListener() {
+ @Override
+ public void onItemClick(AudioSharingDeviceItem item) {
+ isShareBtnClicked.set(true);
+ }
+
+ @Override
+ public void onCancelClick() {}
+ },
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(R.id.positive_btn).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(R.id.positive_btn);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
+ shadowMainLooper().idle();
+
assertThat(dialog.isShowing()).isFalse();
assertThat(isShareBtnClicked.get()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
@Test
@@ -196,15 +276,21 @@ public class AudioSharingDialogFragmentTest {
list.add(TEST_DEVICE_ITEM1);
list.add(TEST_DEVICE_ITEM2);
list.add(TEST_DEVICE_ITEM3);
- mFragment.show(mParent, list, (item) -> {});
+ AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
TextView description = dialog.findViewById(R.id.description_text);
+ assertThat(description).isNotNull();
ImageView image = dialog.findViewById(R.id.description_image);
+ assertThat(image).isNotNull();
Button shareBtn = dialog.findViewById(R.id.positive_btn);
+ assertThat(shareBtn).isNotNull();
Button cancelBtn = dialog.findViewById(R.id.negative_btn);
+ assertThat(cancelBtn).isNotNull();
RecyclerView recyclerView = dialog.findViewById(R.id.device_btn_list);
+ assertThat(recyclerView).isNotNull();
assertThat(dialog.isShowing()).isTrue();
assertThat(description.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(description.getText().toString())
@@ -223,11 +309,35 @@ public class AudioSharingDialogFragmentTest {
list.add(TEST_DEVICE_ITEM1);
list.add(TEST_DEVICE_ITEM2);
list.add(TEST_DEVICE_ITEM3);
- mFragment.show(mParent, list, (item) -> {});
+ AtomicBoolean isCancelBtnClicked = new AtomicBoolean(false);
+ AudioSharingDialogFragment.show(
+ mParent,
+ list,
+ new AudioSharingDialogFragment.DialogEventListener() {
+ @Override
+ public void onItemClick(AudioSharingDeviceItem item) {}
+
+ @Override
+ public void onCancelClick() {
+ isCancelBtnClicked.set(true);
+ }
+ },
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(R.id.negative_btn).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(R.id.negative_btn);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
+ shadowMainLooper().idle();
+
assertThat(dialog.isShowing()).isFalse();
+ assertThat(isCancelBtnClicked.get()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java
index 570af1f3c19..633bc06aa30 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java
@@ -24,6 +24,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
+import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -32,6 +33,7 @@ import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.os.Looper;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.Pair;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
@@ -39,6 +41,7 @@ import androidx.fragment.app.FragmentActivity;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -51,6 +54,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.flags.Flags;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.common.truth.Correspondence;
import org.junit.Before;
@@ -87,6 +91,7 @@ public class AudioSharingDialogHandlerTest {
Correspondence.from(
(Fragment fragment, String tag) ->
fragment instanceof DialogFragment
+ && ((DialogFragment) fragment).getTag() != null
&& ((DialogFragment) fragment).getTag().equals(tag),
"is equal to");
@@ -107,20 +112,22 @@ public class AudioSharingDialogHandlerTest {
private Fragment mParentFragment;
@Mock private BluetoothLeBroadcastReceiveState mState;
private Context mContext;
- private ShadowBluetoothAdapter mShadowBluetoothAdapter;
private AudioSharingDialogHandler mHandler;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setup() {
mContext = ApplicationProvider.getApplicationContext();
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
mLocalBtManager = Utils.getLocalBtManager(mContext);
- mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- mShadowBluetoothAdapter.setEnabled(true);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ ShadowBluetoothAdapter shadowBluetoothAdapter =
+ Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
when(mLocalBtManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
@@ -183,9 +190,33 @@ public class AudioSharingDialogHandlerTest {
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ true);
shadowOf(Looper.getMainLooper()).idle();
- assertThat(mParentFragment.getChildFragmentManager().getFragments())
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
.comparingElementsUsing(TAG_EQUALS)
.containsExactly(AudioSharingStopDialogFragment.tag());
+
+ AudioSharingStopDialogFragment fragment =
+ (AudioSharingStopDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_STOP_AUDIO_SHARING),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 0));
}
@Test
@@ -211,9 +242,33 @@ public class AudioSharingDialogHandlerTest {
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
shadowOf(Looper.getMainLooper()).idle();
- assertThat(mParentFragment.getChildFragmentManager().getFragments())
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
.comparingElementsUsing(TAG_EQUALS)
.containsExactly(AudioSharingJoinDialogFragment.tag());
+
+ AudioSharingJoinDialogFragment fragment =
+ (AudioSharingJoinDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_START_AUDIO_SHARING),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 0),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 2));
}
@Test
@@ -227,9 +282,33 @@ public class AudioSharingDialogHandlerTest {
when(mAssistant.getAllSources(mDevice3)).thenReturn(ImmutableList.of(mState));
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
shadowOf(Looper.getMainLooper()).idle();
- assertThat(mParentFragment.getChildFragmentManager().getFragments())
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
.comparingElementsUsing(TAG_EQUALS)
.containsExactly(AudioSharingJoinDialogFragment.tag());
+
+ AudioSharingJoinDialogFragment fragment =
+ (AudioSharingJoinDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 1));
}
@Test
@@ -245,9 +324,33 @@ public class AudioSharingDialogHandlerTest {
when(mAssistant.getAllSources(mDevice4)).thenReturn(ImmutableList.of(mState));
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
shadowOf(Looper.getMainLooper()).idle();
- assertThat(mParentFragment.getChildFragmentManager().getFragments())
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
.comparingElementsUsing(TAG_EQUALS)
.containsExactly(AudioSharingDisconnectDialogFragment.tag());
+
+ AudioSharingDisconnectDialogFragment fragment =
+ (AudioSharingDisconnectDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 2),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 1));
}
@Test
@@ -273,9 +376,33 @@ public class AudioSharingDialogHandlerTest {
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ false);
shadowOf(Looper.getMainLooper()).idle();
- assertThat(mParentFragment.getChildFragmentManager().getFragments())
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
.comparingElementsUsing(TAG_EQUALS)
.containsExactly(AudioSharingStopDialogFragment.tag());
+
+ AudioSharingStopDialogFragment fragment =
+ (AudioSharingStopDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_STOP_AUDIO_SHARING),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 0));
}
@Test
@@ -301,9 +428,33 @@ public class AudioSharingDialogHandlerTest {
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
shadowOf(Looper.getMainLooper()).idle();
- assertThat(mParentFragment.getChildFragmentManager().getFragments())
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
.comparingElementsUsing(TAG_EQUALS)
.containsExactly(AudioSharingJoinDialogFragment.tag());
+
+ AudioSharingJoinDialogFragment fragment =
+ (AudioSharingJoinDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_START_AUDIO_SHARING),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 0),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 2));
}
@Test
@@ -317,9 +468,33 @@ public class AudioSharingDialogHandlerTest {
when(mAssistant.getAllSources(mDevice3)).thenReturn(ImmutableList.of(mState));
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
shadowOf(Looper.getMainLooper()).idle();
- assertThat(mParentFragment.getChildFragmentManager().getFragments())
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
.comparingElementsUsing(TAG_EQUALS)
.containsExactly(AudioSharingJoinDialogFragment.tag());
+
+ AudioSharingJoinDialogFragment fragment =
+ (AudioSharingJoinDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 1));
}
@Test
@@ -334,9 +509,33 @@ public class AudioSharingDialogHandlerTest {
when(mAssistant.getAllSources(mDevice4)).thenReturn(ImmutableList.of(mState));
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
shadowOf(Looper.getMainLooper()).idle();
- assertThat(mParentFragment.getChildFragmentManager().getFragments())
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
.comparingElementsUsing(TAG_EQUALS)
.containsExactly(AudioSharingDisconnectDialogFragment.tag());
+
+ AudioSharingDisconnectDialogFragment fragment =
+ (AudioSharingDisconnectDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 2),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 1));
}
@Test
@@ -357,6 +556,11 @@ public class AudioSharingDialogHandlerTest {
mHandler.closeOpeningDialogsForLeaDevice(mCachedDevice1);
shadowOf(Looper.getMainLooper()).idle();
assertThat(mParentFragment.getChildFragmentManager().getFragments()).isEmpty();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ mContext,
+ SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
+ SettingsEnums.DIALOG_START_AUDIO_SHARING);
}
@Test
@@ -377,6 +581,11 @@ public class AudioSharingDialogHandlerTest {
mHandler.closeOpeningDialogsForNonLeaDevice(mCachedDevice2);
shadowOf(Looper.getMainLooper()).idle();
assertThat(mParentFragment.getChildFragmentManager().getFragments()).isEmpty();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ mContext,
+ SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
+ SettingsEnums.DIALOG_STOP_AUDIO_SHARING);
}
private void setUpBroadcast(boolean isBroadcasting) {
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
index 348efbe931b..481c78d2917 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
@@ -18,13 +18,21 @@ package com.android.settings.connecteddevice.audiosharing;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.Pair;
+import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AlertDialog;
@@ -33,6 +41,7 @@ import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -78,15 +87,19 @@ public class AudioSharingDisconnectDialogFragmentTest {
new AudioSharingDeviceItem(TEST_DEVICE_NAME2, TEST_GROUP_ID2, /* isActive= */ false);
private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
new AudioSharingDeviceItem(TEST_DEVICE_NAME3, TEST_GROUP_ID3, /* isActive= */ false);
+ private static final AudioSharingDisconnectDialogFragment.DialogEventListener
+ EMPTY_EVENT_LISTENER = (AudioSharingDeviceItem item) -> {};
+ private static final Pair TEST_EVENT_DATA = Pair.create(1, 1);
+ private static final Pair[] TEST_EVENT_DATA_LIST =
+ new Pair[] {TEST_EVENT_DATA};
@Mock private BluetoothDevice mDevice1;
@Mock private BluetoothDevice mDevice3;
-
@Mock private CachedBluetoothDevice mCachedDevice1;
@Mock private CachedBluetoothDevice mCachedDevice3;
+ private FakeFeatureFactory mFeatureFactory;
private Fragment mParent;
private AudioSharingDisconnectDialogFragment mFragment;
- private ShadowBluetoothAdapter mShadowBluetoothAdapter;
private ArrayList mDeviceItems = new ArrayList<>();
@Before
@@ -96,12 +109,14 @@ public class AudioSharingDisconnectDialogFragmentTest {
latestAlertDialog.dismiss();
ShadowAlertDialogCompat.reset();
}
- mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- mShadowBluetoothAdapter.setEnabled(true);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ ShadowBluetoothAdapter shadowBluetoothAdapter =
+ Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
when(mDevice1.getAnonymizedAddress()).thenReturn(TEST_ADDRESS1);
when(mDevice3.getAnonymizedAddress()).thenReturn(TEST_ADDRESS3);
when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
@@ -116,13 +131,20 @@ public class AudioSharingDisconnectDialogFragmentTest {
mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
}
+ @Test
+ public void getMetricsCategory_correctValue() {
+ assertThat(mFragment.getMetricsCategory())
+ .isEqualTo(SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE);
+ }
+
@Test
public void onCreateDialog_flagOff_dialogNotExist() {
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
mDeviceItems = new ArrayList<>();
mDeviceItems.add(TEST_DEVICE_ITEM1);
mDeviceItems.add(TEST_DEVICE_ITEM2);
- mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+ AudioSharingDisconnectDialogFragment.show(
+ mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
@@ -135,12 +157,15 @@ public class AudioSharingDisconnectDialogFragmentTest {
mDeviceItems = new ArrayList<>();
mDeviceItems.add(TEST_DEVICE_ITEM1);
mDeviceItems.add(TEST_DEVICE_ITEM2);
- mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+ AudioSharingDisconnectDialogFragment.show(
+ mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
RecyclerView view = dialog.findViewById(R.id.device_btn_list);
+ assertThat(view).isNotNull();
assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
}
@@ -150,12 +175,14 @@ public class AudioSharingDisconnectDialogFragmentTest {
mDeviceItems = new ArrayList<>();
mDeviceItems.add(TEST_DEVICE_ITEM1);
mDeviceItems.add(TEST_DEVICE_ITEM2);
- mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+ AudioSharingDisconnectDialogFragment.show(
+ mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
- AtomicBoolean isItemBtnClicked = new AtomicBoolean(false);
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
RecyclerView view = dialog.findViewById(R.id.device_btn_list);
+ assertThat(view).isNotNull();
assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
Button btn1 =
view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
@@ -173,37 +200,71 @@ public class AudioSharingDisconnectDialogFragmentTest {
TEST_DEVICE_NAME2));
// Update dialog content for device with same group
- mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> isItemBtnClicked.set(true));
+ AtomicBoolean isItemBtnClicked = new AtomicBoolean(false);
+ AudioSharingDisconnectDialogFragment.show(
+ mParent,
+ mDeviceItems,
+ mCachedDevice3,
+ (AudioSharingDeviceItem item) -> isItemBtnClicked.set(true),
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog.isShowing()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider, times(0))
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
+ eq(SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
+
btn1 = view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
btn1.performClick();
+ shadowMainLooper().idle();
+ assertThat(dialog.isShowing()).isFalse();
assertThat(isItemBtnClicked.get()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
@Test
- public void onCreateDialog_dialogIsShowingForNewGroup_updateDialog() {
+ public void onCreateDialog_dialogIsShowingForNewGroup_showNewDialog() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
mDeviceItems = new ArrayList<>();
mDeviceItems.add(TEST_DEVICE_ITEM1);
mDeviceItems.add(TEST_DEVICE_ITEM2);
- mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+ AudioSharingDisconnectDialogFragment.show(
+ mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
RecyclerView view = dialog.findViewById(R.id.device_btn_list);
+ assertThat(view).isNotNull();
assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
// Show new dialog for device with new group
ArrayList newDeviceItems = new ArrayList<>();
newDeviceItems.add(TEST_DEVICE_ITEM2);
newDeviceItems.add(TEST_DEVICE_ITEM3);
- mFragment.show(mParent, newDeviceItems, mCachedDevice1, (item) -> {});
+ AudioSharingDisconnectDialogFragment.show(
+ mParent,
+ newDeviceItems,
+ mCachedDevice1,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog.isShowing()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
+ eq(SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
+
view = dialog.findViewById(R.id.device_btn_list);
+ assertThat(view).isNotNull();
assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
Button btn1 =
view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
@@ -227,12 +288,27 @@ public class AudioSharingDisconnectDialogFragmentTest {
mDeviceItems = new ArrayList<>();
mDeviceItems.add(TEST_DEVICE_ITEM1);
mDeviceItems.add(TEST_DEVICE_ITEM2);
- mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+ AudioSharingDisconnectDialogFragment.show(
+ mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
+ shadowMainLooper().idle();
+ AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+ assertThat(dialog).isNotNull();
+ assertThat(dialog.isShowing()).isTrue();
+ View btnView = dialog.findViewById(R.id.negative_btn);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
shadowMainLooper().idle();
- AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- assertThat(dialog.isShowing()).isTrue();
- dialog.findViewById(R.id.negative_btn).performClick();
assertThat(dialog.isShowing()).isFalse();
+ verify(mFeatureFactory.metricsFeatureProvider, times(0))
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
+ eq(SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java
index 2d55d97d4e1..c7b21ade886 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java
@@ -18,13 +18,19 @@ package com.android.settings.connecteddevice.audiosharing;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.Pair;
+import android.view.View;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
@@ -32,6 +38,7 @@ import androidx.fragment.app.FragmentActivity;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
@@ -82,6 +89,9 @@ public class AudioSharingJoinDialogFragmentTest {
@Override
public void onCancelClick() {}
};
+ private static final Pair TEST_EVENT_DATA = Pair.create(1, 1);
+ private static final Pair[] TEST_EVENT_DATA_LIST =
+ new Pair[] {TEST_EVENT_DATA};
@Mock private CachedBluetoothDevice mCachedDevice1;
@Mock private CachedBluetoothDevice mCachedDevice2;
@@ -90,7 +100,7 @@ public class AudioSharingJoinDialogFragmentTest {
@Mock private LocalBluetoothLeBroadcast mBroadcast;
private Fragment mParent;
private AudioSharingJoinDialogFragment mFragment;
- private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+ private FakeFeatureFactory mFeatureFactory;
@Before
public void setUp() {
@@ -99,12 +109,14 @@ public class AudioSharingJoinDialogFragmentTest {
latestAlertDialog.dismiss();
ShadowAlertDialogCompat.reset();
}
- mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- mShadowBluetoothAdapter.setEnabled(true);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ ShadowBluetoothAdapter shadowBluetoothAdapter =
+ Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
when(mCachedDevice2.getName()).thenReturn(TEST_DEVICE_NAME2);
mFragment = new AudioSharingJoinDialogFragment();
@@ -137,7 +149,12 @@ public class AudioSharingJoinDialogFragmentTest {
@Test
public void onCreateDialog_flagOff_dialogNotExist() {
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
+ AudioSharingJoinDialogFragment.show(
+ mParent,
+ new ArrayList<>(),
+ mCachedDevice2,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNull();
@@ -146,7 +163,12 @@ public class AudioSharingJoinDialogFragmentTest {
@Test
public void onCreateDialog_flagOn_dialogShowTextForSingleDevice() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
+ AudioSharingJoinDialogFragment.show(
+ mParent,
+ new ArrayList<>(),
+ mCachedDevice2,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
@@ -160,7 +182,8 @@ public class AudioSharingJoinDialogFragmentTest {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
ArrayList list = new ArrayList<>();
list.add(TEST_DEVICE_ITEM1);
- mFragment.show(mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER);
+ AudioSharingJoinDialogFragment.show(
+ mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
@@ -179,7 +202,8 @@ public class AudioSharingJoinDialogFragmentTest {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
ArrayList list = new ArrayList<>();
list.add(TEST_DEVICE_ITEM1);
- mFragment.show(mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER);
+ AudioSharingJoinDialogFragment.show(
+ mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
@@ -188,7 +212,8 @@ public class AudioSharingJoinDialogFragmentTest {
// Update the content
ArrayList list2 = new ArrayList<>();
list2.add(TEST_DEVICE_ITEM2);
- mFragment.show(mParent, list2, mCachedDevice1, EMPTY_EVENT_LISTENER);
+ AudioSharingJoinDialogFragment.show(
+ mParent, list2, mCachedDevice1, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
@@ -205,11 +230,25 @@ public class AudioSharingJoinDialogFragmentTest {
@Test
public void onCreateDialog_clickCancel_dialogDismiss() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
+ AudioSharingJoinDialogFragment.show(
+ mParent,
+ new ArrayList<>(),
+ mCachedDevice2,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(R.id.negative_btn).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(R.id.negative_btn);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
+ shadowMainLooper().idle();
assertThat(dialog.isShowing()).isFalse();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
@Test
@@ -228,12 +267,22 @@ public class AudioSharingJoinDialogFragmentTest {
@Override
public void onCancelClick() {}
- });
+ },
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(R.id.positive_btn).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(R.id.positive_btn);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
+ shadowMainLooper().idle();
assertThat(dialog.isShowing()).isFalse();
assertThat(isShareBtnClicked.get()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
@Test
@@ -252,11 +301,21 @@ public class AudioSharingJoinDialogFragmentTest {
public void onCancelClick() {
isCancelBtnClicked.set(true);
}
- });
+ },
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(R.id.negative_btn).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(R.id.negative_btn);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
+ shadowMainLooper().idle();
assertThat(dialog.isShowing()).isFalse();
assertThat(isCancelBtnClicked.get()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java
index b8bee1a65c8..046a4ce8f67 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java
@@ -25,12 +25,15 @@ import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.os.Looper;
@@ -84,47 +87,67 @@ public class AudioSharingPreferenceControllerTest {
@Mock private BluetoothEventManager mBtEventManager;
@Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
@Mock private LocalBluetoothLeBroadcast mBroadcast;
+ @Mock private BluetoothLeBroadcastMetadata mMetadata;
private AudioSharingPreferenceController mController;
- private ShadowBluetoothAdapter mShadowBluetoothAdapter;
- private LocalBluetoothManager mLocalBluetoothManager;
private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner;
- private Preference mPreference;
+ @Spy private Preference mPreference;
@Before
public void setUp() {
- mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- mShadowBluetoothAdapter.setEnabled(true);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ ShadowBluetoothAdapter shadowBluetoothAdapter =
+ Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
- mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
- when(mLocalBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
- when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
+ LocalBluetoothManager localBluetoothManager = Utils.getLocalBtManager(mContext);
+ when(localBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
+ when(localBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
mController = new AudioSharingPreferenceController(mContext, PREF_KEY);
- mPreference = new Preference(mContext);
+ mPreference = spy(new Preference(mContext));
when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
}
@Test
- public void onStart_registerCallback() {
+ public void onStart_flagOn_registerCallback() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
mController.onStart(mLifecycleOwner);
verify(mBtEventManager).registerCallback(mController);
verify(mBroadcast).registerServiceCallBack(any(), any(BluetoothLeBroadcast.Callback.class));
}
@Test
- public void onStop_unregisterCallback() {
+ public void onStart_flagOff_skipRegisterCallback() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+ mController.onStart(mLifecycleOwner);
+ verify(mBtEventManager, never()).registerCallback(mController);
+ verify(mBroadcast, never())
+ .registerServiceCallBack(any(), any(BluetoothLeBroadcast.Callback.class));
+ }
+
+ @Test
+ public void onStop_flagOn_unregisterCallback() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
mController.onStop(mLifecycleOwner);
verify(mBtEventManager).unregisterCallback(mController);
verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
}
+ @Test
+ public void onStop_flagOff_skipUnregisterCallback() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+ mController.onStop(mLifecycleOwner);
+ verify(mBtEventManager, never()).unregisterCallback(mController);
+ verify(mBroadcast, never())
+ .unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+ }
+
@Test
public void getAvailabilityStatus_flagOn() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
@@ -166,4 +189,42 @@ public class AudioSharingPreferenceControllerTest {
assertThat(mPreference.getSummary().toString())
.isEqualTo(mContext.getString(R.string.audio_sharing_summary_off));
}
+
+ @Test
+ public void testBluetoothLeBroadcastCallbacks_refreshSummary() {
+ mController.displayPreference(mScreen);
+
+ when(mBroadcast.isEnabled(any())).thenReturn(true);
+ mController.mBroadcastCallback.onBroadcastStarted(/* reason= */ 1, /* broadcastId= */ 1);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(mPreference.getSummary().toString())
+ .isEqualTo(mContext.getString(R.string.audio_sharing_summary_on));
+
+ when(mBroadcast.isEnabled(any())).thenReturn(false);
+ mController.mBroadcastCallback.onBroadcastStopped(/* reason= */ 1, /* broadcastId= */ 1);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(mPreference.getSummary().toString())
+ .isEqualTo(mContext.getString(R.string.audio_sharing_summary_off));
+ }
+
+ @Test
+ public void testBluetoothLeBroadcastCallbacks_doNothing() {
+ mController.displayPreference(mScreen);
+
+ mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, mMetadata);
+ verify(mPreference, never()).setSummary(any());
+ mController.mBroadcastCallback.onBroadcastUpdated(/* reason= */ 1, /* broadcastId= */ 1);
+ verify(mPreference, never()).setSummary(any());
+ mController.mBroadcastCallback.onPlaybackStarted(/* reason= */ 1, /* broadcastId= */ 1);
+ verify(mPreference, never()).setSummary(any());
+ mController.mBroadcastCallback.onPlaybackStopped(/* reason= */ 1, /* broadcastId= */ 1);
+ verify(mPreference, never()).setSummary(any());
+ mController.mBroadcastCallback.onBroadcastStartFailed(/* reason= */ 1);
+ verify(mPreference, never()).setSummary(any());
+ mController.mBroadcastCallback.onBroadcastStopFailed(/* reason= */ 1);
+ verify(mPreference, never()).setSummary(any());
+ mController.mBroadcastCallback.onBroadcastUpdateFailed(
+ /* reason= */ 1, /* broadcastId= */ 1);
+ verify(mPreference, never()).setSummary(any());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
index 84d7a317164..7d46a18a4f2 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
@@ -18,13 +18,21 @@ package com.android.settings.connecteddevice.audiosharing;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.Pair;
+import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
@@ -32,6 +40,7 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.android.settings.R;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -76,14 +85,19 @@ public class AudioSharingStopDialogFragmentTest {
private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
new AudioSharingDeviceItem(
TEST_DEVICE_NAME3, TEST_DEVICE_GROUP_ID3, /* isActive= */ false);
+ private static final AudioSharingStopDialogFragment.DialogEventListener EMPTY_EVENT_LISTENER =
+ () -> {};
+ private static final Pair TEST_EVENT_DATA = Pair.create(1, 1);
+ private static final Pair[] TEST_EVENT_DATA_LIST =
+ new Pair[] {TEST_EVENT_DATA};
@Mock private CachedBluetoothDevice mCachedDevice1;
@Mock private CachedBluetoothDevice mCachedDevice2;
@Mock private BluetoothDevice mDevice1;
@Mock private BluetoothDevice mDevice2;
+ private FakeFeatureFactory mFeatureFactory;
private Fragment mParent;
private AudioSharingStopDialogFragment mFragment;
- private ShadowBluetoothAdapter mShadowBluetoothAdapter;
@Before
public void setUp() {
@@ -92,12 +106,14 @@ public class AudioSharingStopDialogFragmentTest {
latestAlertDialog.dismiss();
ShadowAlertDialogCompat.reset();
}
- mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- mShadowBluetoothAdapter.setEnabled(true);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+ ShadowBluetoothAdapter shadowBluetoothAdapter =
+ Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
- mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
BluetoothStatusCodes.FEATURE_SUPPORTED);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
when(mCachedDevice1.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
@@ -110,10 +126,21 @@ public class AudioSharingStopDialogFragmentTest {
mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
}
+ @Test
+ public void getMetricsCategory_correctValue() {
+ assertThat(mFragment.getMetricsCategory())
+ .isEqualTo(SettingsEnums.DIALOG_STOP_AUDIO_SHARING);
+ }
+
@Test
public void onCreateDialog_flagOff_dialogNotExist() {
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
+ AudioSharingStopDialogFragment.show(
+ mParent,
+ ImmutableList.of(),
+ mCachedDevice1,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNull();
@@ -122,12 +149,18 @@ public class AudioSharingStopDialogFragmentTest {
@Test
public void onCreateDialog_oneDeviceInSharing_showDialogWithCorrectMessage() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, ImmutableList.of(TEST_DEVICE_ITEM2), mCachedDevice1, () -> {});
+ AudioSharingStopDialogFragment.show(
+ mParent,
+ ImmutableList.of(TEST_DEVICE_ITEM2),
+ mCachedDevice1,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
TextView view = dialog.findViewById(R.id.description_text);
+ assertThat(view).isNotNull();
assertThat(view.getText().toString())
.isEqualTo(
mParent.getString(
@@ -137,16 +170,18 @@ public class AudioSharingStopDialogFragmentTest {
@Test
public void onCreateDialog_twoDeviceInSharing_showDialogWithCorrectMessage() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(
+ AudioSharingStopDialogFragment.show(
mParent,
ImmutableList.of(TEST_DEVICE_ITEM2, TEST_DEVICE_ITEM3),
mCachedDevice1,
- () -> {});
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
TextView view = dialog.findViewById(R.id.description_text);
+ assertThat(view).isNotNull();
assertThat(view.getText().toString())
.isEqualTo(
mParent.getString(
@@ -158,57 +193,99 @@ public class AudioSharingStopDialogFragmentTest {
@Test
public void onCreateDialog_dialogIsShowingForSameDevice_updateDialog() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
+ AudioSharingStopDialogFragment.show(
+ mParent,
+ ImmutableList.of(),
+ mCachedDevice1,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
TextView view = dialog.findViewById(R.id.description_text);
+ assertThat(view).isNotNull();
assertThat(view.getText().toString())
.isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
// Update the content
AtomicBoolean isStopBtnClicked = new AtomicBoolean(false);
- mFragment.show(
- mParent, ImmutableList.of(), mCachedDevice1, () -> isStopBtnClicked.set(true));
+ AudioSharingStopDialogFragment.show(
+ mParent,
+ ImmutableList.of(),
+ mCachedDevice1,
+ () -> isStopBtnClicked.set(true),
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider, times(0))
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
+ eq(SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
- dialog.findViewById(android.R.id.button1).performClick();
+ View btnView = dialog.findViewById(android.R.id.button1);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
shadowMainLooper().idle();
assertThat(dialog.isShowing()).isFalse();
assertThat(isStopBtnClicked.get()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
@Test
public void onCreateDialog_dialogIsShowingForNewDevice_showNewDialog() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
+ AudioSharingStopDialogFragment.show(
+ mParent,
+ ImmutableList.of(),
+ mCachedDevice1,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
TextView view = dialog.findViewById(R.id.description_text);
+ assertThat(view).isNotNull();
assertThat(view.getText().toString())
.isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
TextView title = dialog.findViewById(R.id.title_text);
+ assertThat(title).isNotNull();
assertThat(title.getText().toString())
.isEqualTo(
mParent.getString(
R.string.audio_sharing_stop_dialog_title, TEST_DEVICE_NAME1));
// Show new dialog
- mFragment.show(mParent, ImmutableList.of(), mCachedDevice2, () -> {});
+ AudioSharingStopDialogFragment.show(
+ mParent,
+ ImmutableList.of(),
+ mCachedDevice2,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
assertThat(dialog).isNotNull();
assertThat(dialog.isShowing()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
+ eq(SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
+
view = dialog.findViewById(R.id.description_text);
+ assertThat(view).isNotNull();
assertThat(view.getText().toString())
.isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
title = dialog.findViewById(R.id.title_text);
+ assertThat(title).isNotNull();
assertThat(title.getText().toString())
.isEqualTo(
mParent.getString(
@@ -218,25 +295,60 @@ public class AudioSharingStopDialogFragmentTest {
@Test
public void onCreateDialog_clickCancel_dialogDismiss() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
+ AudioSharingStopDialogFragment.show(
+ mParent,
+ ImmutableList.of(),
+ mCachedDevice1,
+ EMPTY_EVENT_LISTENER,
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(android.R.id.button2).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(android.R.id.button2);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
shadowMainLooper().idle();
assertThat(dialog.isShowing()).isFalse();
+ verify(mFeatureFactory.metricsFeatureProvider, times(0))
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
+ eq(SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
@Test
public void onCreateDialog_clickShare_callbackTriggered() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
AtomicBoolean isStopBtnClicked = new AtomicBoolean(false);
- mFragment.show(
- mParent, ImmutableList.of(), mCachedDevice1, () -> isStopBtnClicked.set(true));
+ AudioSharingStopDialogFragment.show(
+ mParent,
+ ImmutableList.of(),
+ mCachedDevice1,
+ () -> isStopBtnClicked.set(true),
+ TEST_EVENT_DATA_LIST);
shadowMainLooper().idle();
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
- dialog.findViewById(android.R.id.button1).performClick();
+ assertThat(dialog).isNotNull();
+ View btnView = dialog.findViewById(android.R.id.button1);
+ assertThat(btnView).isNotNull();
+ btnView.performClick();
shadowMainLooper().idle();
assertThat(dialog.isShowing()).isFalse();
assertThat(isStopBtnClicked.get()).isTrue();
+ verify(mFeatureFactory.metricsFeatureProvider, times(0))
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
+ eq(SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(
+ any(Context.class),
+ eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
+ eq(TEST_EVENT_DATA));
}
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java
index 0ead2d5d807..8f85feb89fd 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java
@@ -23,6 +23,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -30,6 +31,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
+import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcast;
@@ -43,12 +45,17 @@ import android.content.IntentFilter;
import android.os.Looper;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.FeatureFlagUtils;
+import android.util.Pair;
import android.widget.CompoundButton;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleOwner;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
import com.android.settings.testutils.shadow.ShadowThreadUtils;
@@ -65,6 +72,8 @@ import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.flags.Flags;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.truth.Correspondence;
import org.junit.Before;
import org.junit.Rule;
@@ -77,7 +86,9 @@ import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+import java.util.List;
import java.util.concurrent.Executor;
@RunWith(RobolectricTestRunner.class)
@@ -88,6 +99,18 @@ import java.util.concurrent.Executor;
ShadowThreadUtils.class,
})
public class AudioSharingSwitchBarControllerTest {
+ private static final String TEST_DEVICE_NAME1 = "test1";
+ private static final String TEST_DEVICE_NAME2 = "test2";
+ private static final int TEST_DEVICE_GROUP_ID1 = 1;
+ private static final int TEST_DEVICE_GROUP_ID2 = 2;
+ private static final Correspondence TAG_EQUALS =
+ Correspondence.from(
+ (Fragment fragment, String tag) ->
+ fragment instanceof DialogFragment
+ && ((DialogFragment) fragment).getTag() != null
+ && ((DialogFragment) fragment).getTag().equals(tag),
+ "is equal to");
+
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -99,17 +122,19 @@ public class AudioSharingSwitchBarControllerTest {
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
@Mock private VolumeControlProfile mVolumeControl;
@Mock private CompoundButton mBtnView;
- @Mock private CachedBluetoothDevice mCachedDevice;
- @Mock private BluetoothDevice mDevice;
+ @Mock private CachedBluetoothDevice mCachedDevice1;
+ @Mock private CachedBluetoothDevice mCachedDevice2;
+ @Mock private BluetoothDevice mDevice1;
+ @Mock private BluetoothDevice mDevice2;
private SettingsMainSwitchBar mSwitchBar;
private AudioSharingSwitchBarController mController;
- private AudioSharingSwitchBarController.OnAudioSharingStateChangedListener mListener;
+ private FakeFeatureFactory mFeatureFactory;
private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner;
private boolean mOnAudioSharingStateChanged;
private boolean mOnAudioSharingServiceConnected;
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
- private LocalBluetoothManager mLocalBluetoothManager;
+ private Fragment mParentFragment;
@Before
public void setUp() {
@@ -122,13 +147,20 @@ public class AudioSharingSwitchBarControllerTest {
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
- mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
- when(mLocalBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
- when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
- when(mDeviceManager.findDevice(mDevice)).thenReturn(mCachedDevice);
- when(mCachedDevice.getDevice()).thenReturn(mDevice);
- when(mCachedDevice.getGroupId()).thenReturn(1);
- when(mCachedDevice.getName()).thenReturn("test");
+ LocalBluetoothManager localBluetoothManager = Utils.getLocalBtManager(mContext);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ when(localBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
+ when(localBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
+ when(mDeviceManager.findDevice(mDevice1)).thenReturn(mCachedDevice1);
+ when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
+ when(mCachedDevice1.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
+ when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
+ when(mCachedDevice1.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(false);
+ when(mDeviceManager.findDevice(mDevice2)).thenReturn(mCachedDevice2);
+ when(mCachedDevice2.getDevice()).thenReturn(mDevice2);
+ when(mCachedDevice2.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID2);
+ when(mCachedDevice2.getName()).thenReturn(TEST_DEVICE_NAME2);
+ when(mCachedDevice2.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(true);
when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
@@ -153,7 +185,7 @@ public class AudioSharingSwitchBarControllerTest {
mSwitchBar.setDisabledByAdmin(mock(RestrictedLockUtils.EnforcedAdmin.class));
mOnAudioSharingStateChanged = false;
mOnAudioSharingServiceConnected = false;
- mListener =
+ AudioSharingSwitchBarController.OnAudioSharingStateChangedListener listener =
new AudioSharingSwitchBarController.OnAudioSharingStateChangedListener() {
@Override
public void onAudioSharingStateChanged() {
@@ -165,7 +197,14 @@ public class AudioSharingSwitchBarControllerTest {
mOnAudioSharingServiceConnected = true;
}
};
- mController = new AudioSharingSwitchBarController(mContext, mSwitchBar, mListener);
+ mController = new AudioSharingSwitchBarController(mContext, mSwitchBar, listener);
+ mParentFragment = new Fragment();
+ FragmentController.setupFragment(
+ mParentFragment,
+ FragmentActivity.class,
+ 0 /* containerViewId */,
+ null /* bundle */);
+ mController.init(mParentFragment);
}
@Test
@@ -356,7 +395,7 @@ public class AudioSharingSwitchBarControllerTest {
when(mBtnView.isEnabled()).thenReturn(true);
when(mAssistant.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED}))
- .thenReturn(ImmutableList.of(mDevice));
+ .thenReturn(ImmutableList.of(mDevice1));
doNothing().when(mBroadcast).startPrivateBroadcast();
mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
verify(mBroadcast).startPrivateBroadcast();
@@ -380,4 +419,50 @@ public class AudioSharingSwitchBarControllerTest {
mController.onCheckedChanged(mBtnView, /* isChecked= */ false);
verify(mBroadcast).stopBroadcast(1);
}
+
+ @Test
+ public void onPlaybackStarted_showJoinAudioSharingDialog() {
+ FeatureFlagUtils.setEnabled(
+ mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true);
+ when(mBtnView.isEnabled()).thenReturn(true);
+ when(mAssistant.getDevicesMatchingConnectionStates(
+ new int[] {BluetoothProfile.STATE_CONNECTED}))
+ .thenReturn(ImmutableList.of(mDevice2, mDevice1));
+ doNothing().when(mBroadcast).startPrivateBroadcast();
+ mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
+ verify(mBroadcast).startPrivateBroadcast();
+ mController.mBroadcastCallback.onPlaybackStarted(0, 0);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ verify(mFeatureFactory.metricsFeatureProvider)
+ .action(any(Context.class), eq(SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING));
+
+ List childFragments = mParentFragment.getChildFragmentManager().getFragments();
+ assertThat(childFragments)
+ .comparingElementsUsing(TAG_EQUALS)
+ .containsExactly(AudioSharingDialogFragment.tag());
+
+ AudioSharingDialogFragment fragment =
+ (AudioSharingDialogFragment) Iterables.getOnlyElement(childFragments);
+ Pair[] eventData = fragment.getEventData();
+ assertThat(eventData)
+ .asList()
+ .containsExactly(
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
+ SettingsEnums.AUDIO_SHARING_SETTINGS),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
+ SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
+ .ordinal(),
+ 1),
+ Pair.create(
+ AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
+ .ordinal(),
+ 1));
+ }
}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java
new file mode 100644
index 00000000000..c1c4d61727f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.notification.modes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.service.notification.ZenModeConfig;
+
+import androidx.preference.TwoStatePreference;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Calendar;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeExitAtAlarmPreferenceControllerTest {
+ private Context mContext;
+ @Mock
+ private ZenModesBackend mBackend;
+
+ private ZenModeExitAtAlarmPreferenceController mPrefController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = ApplicationProvider.getApplicationContext();
+ mPrefController = new ZenModeExitAtAlarmPreferenceController(mContext, "exit_at_alarm",
+ mBackend);
+ }
+
+ @Test
+ public void testUpdateState() {
+ TwoStatePreference preference = mock(TwoStatePreference.class);
+
+ // previously: don't exit at alarm
+ ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
+ scheduleInfo.days = new int[] { Calendar.MONDAY };
+ scheduleInfo.startHour = 1;
+ scheduleInfo.endHour = 2;
+ scheduleInfo.exitAtAlarm = false;
+
+ ZenMode mode = new ZenMode("id",
+ new AutomaticZenRule.Builder("name",
+ ZenModeConfig.toScheduleConditionId(scheduleInfo)).build(),
+ true); // is active
+
+ // need to call updateZenMode for the first call
+ mPrefController.updateZenMode(preference, mode);
+ verify(preference).setChecked(false);
+
+ // Now update state after changing exitAtAlarm
+ scheduleInfo.exitAtAlarm = true;
+ mode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(scheduleInfo));
+
+ // now can just call updateState
+ mPrefController.updateState(preference, mode);
+ verify(preference).setChecked(true);
+ }
+
+ @Test
+ public void testOnPreferenceChange() {
+ TwoStatePreference preference = mock(TwoStatePreference.class);
+
+ // previously: exit at alarm
+ ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
+ scheduleInfo.days = new int[] { Calendar.MONDAY };
+ scheduleInfo.startHour = 1;
+ scheduleInfo.endHour = 2;
+ scheduleInfo.exitAtAlarm = true;
+
+ ZenMode mode = new ZenMode("id",
+ new AutomaticZenRule.Builder("name",
+ ZenModeConfig.toScheduleConditionId(scheduleInfo)).build(),
+ true); // is active
+ mPrefController.updateZenMode(preference, mode);
+
+ // turn off exit at alarm
+ mPrefController.onPreferenceChange(preference, false);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class);
+ verify(mBackend).updateMode(captor.capture());
+ ZenModeConfig.ScheduleInfo newSchedule = ZenModeConfig.tryParseScheduleConditionId(
+ captor.getValue().getRule().getConditionId());
+ assertThat(newSchedule.exitAtAlarm).isFalse();
+
+ // other properties remain the same
+ assertThat(newSchedule.startHour).isEqualTo(1);
+ assertThat(newSchedule.endHour).isEqualTo(2);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java
new file mode 100644
index 00000000000..c0fbe15a6c2
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.notification.modes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.preference.PreferenceScreen;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.widget.LayoutPreference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeIconPickerListPreferenceControllerTest {
+
+ private static final ZenMode ZEN_MODE = new ZenMode(
+ "mode_id",
+ new AutomaticZenRule.Builder("mode name", Uri.parse("mode")).build(),
+ /* isActive= */ false);
+
+ private ZenModesBackend mBackend;
+ private ZenModeIconPickerListPreferenceController mController;
+ private PreferenceScreen mPreferenceScreen;
+ private RecyclerView mRecyclerView;
+
+ @Before
+ public void setUp() {
+ Context context = RuntimeEnvironment.getApplication();
+ mBackend = mock(ZenModesBackend.class);
+
+ DashboardFragment fragment = mock(DashboardFragment.class);
+ mController = new ZenModeIconPickerListPreferenceController(
+ RuntimeEnvironment.getApplication(), "icon_list", fragment, mBackend);
+
+ mRecyclerView = new RecyclerView(context);
+ mRecyclerView.setId(R.id.icon_list);
+ LayoutPreference layoutPreference = new LayoutPreference(context, mRecyclerView);
+ mPreferenceScreen = mock(PreferenceScreen.class);
+ when(mPreferenceScreen.findPreference(eq("icon_list"))).thenReturn(layoutPreference);
+ }
+
+ @Test
+ public void displayPreference_loadsIcons() {
+ mController.displayPreference(mPreferenceScreen);
+
+ assertThat(mRecyclerView.getAdapter()).isNotNull();
+ assertThat(mRecyclerView.getAdapter().getItemCount()).isEqualTo(20);
+ }
+
+ @Test
+ public void selectIcon_updatesMode() {
+ mController.setZenMode(ZEN_MODE);
+
+ mController.onIconSelected(R.drawable.ic_android);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class);
+ verify(mBackend).updateMode(captor.capture());
+ assertThat(captor.getValue().getRule().getIconResId()).isEqualTo(R.drawable.ic_android);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java
new file mode 100644
index 00000000000..7cf327c983e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.notification.modes;
+
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AutomaticZenRule;
+import android.app.Flags;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.ZenModeConfig;
+import android.view.ViewGroup;
+import android.widget.ToggleButton;
+
+import androidx.fragment.app.Fragment;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+
+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.RobolectricTestRunner;
+
+import java.util.Calendar;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeSetSchedulePreferenceControllerTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
+ @Mock
+ private ZenModesBackend mBackend;
+ private Context mContext;
+
+ @Mock
+ private Fragment mParent;
+ @Mock
+ private Calendar mCalendar;
+ @Mock
+ private ViewGroup mDaysContainer;
+ @Mock
+ private ToggleButton mDay0, mDay1, mDay2, mDay3, mDay4, mDay5, mDay6;
+
+ private ZenModeSetSchedulePreferenceController mPrefController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = ApplicationProvider.getApplicationContext();
+ mPrefController = new ZenModeSetSchedulePreferenceController(mContext, mParent, "schedule",
+ mBackend);
+ setupMockDayContainer();
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void updateScheduleRule_updatesConditionAndTriggerDescription() {
+ ZenMode mode = new ZenMode("id",
+ new AutomaticZenRule.Builder("name", Uri.parse("condition")).build(),
+ true); // is active
+
+ ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
+ scheduleInfo.days = new int[] { Calendar.MONDAY };
+ scheduleInfo.startHour = 1;
+ scheduleInfo.endHour = 2;
+ ZenMode out = mPrefController.updateScheduleMode(scheduleInfo).apply(mode);
+
+ assertThat(out.getRule().getConditionId())
+ .isEqualTo(ZenModeConfig.toScheduleConditionId(scheduleInfo));
+ assertThat(out.getRule().getTriggerDescription()).isNotEmpty();
+ }
+
+ @Test
+ public void testUpdateScheduleDays() {
+ // Confirm that adding/subtracting/etc days works as expected
+ // starting from null: no days set
+ ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo();
+
+ // Unset a day that's already unset: nothing should change
+ assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
+ Calendar.TUESDAY, false)).isFalse();
+ // not explicitly checking whether schedule.days is still null here, as we don't necessarily
+ // want to require nullness as distinct from an empty list of days.
+
+ // set a few new days
+ assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
+ Calendar.MONDAY, true)).isTrue();
+ assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
+ Calendar.FRIDAY, true)).isTrue();
+ assertThat(schedule.days).hasLength(2);
+ assertThat(schedule.days).asList().containsExactly(Calendar.MONDAY, Calendar.FRIDAY);
+
+ // remove an existing day to make sure that works
+ assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
+ Calendar.MONDAY, false)).isTrue();
+ assertThat(schedule.days).hasLength(1);
+ assertThat(schedule.days).asList().containsExactly(Calendar.FRIDAY);
+ }
+
+ @Test
+ public void testSetupDayToggles_daysOfWeekOrder() {
+ // Confirm that days are correctly associated with the actual day of the week independent
+ // of when the first day of the week is for the given calendar.
+ ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo();
+ schedule.days = new int[] { Calendar.SUNDAY, Calendar.TUESDAY, Calendar.FRIDAY };
+ schedule.startHour = 1;
+ schedule.endHour = 5;
+
+ // Start mCalendar on Wednesday, arbitrarily
+ when(mCalendar.getFirstDayOfWeek()).thenReturn(Calendar.WEDNESDAY);
+
+ // Setup the day toggles
+ mPrefController.setupDayToggles(mDaysContainer, schedule, mCalendar);
+
+ // we should see toggle 0 associated with the first day of the week, etc.
+ // in this week order, schedule turns on friday (2), sunday (4), tuesday (6) so those
+ // should be checked while everything else should not be checked.
+ verify(mDay0).setChecked(false); // weds
+ verify(mDay1).setChecked(false); // thurs
+ verify(mDay2).setChecked(true); // fri
+ verify(mDay3).setChecked(false); // sat
+ verify(mDay4).setChecked(true); // sun
+ verify(mDay5).setChecked(false); // mon
+ verify(mDay6).setChecked(true); // tues
+ }
+
+ private void setupMockDayContainer() {
+ // associate each index (regardless of associated day of the week) with the appropriate
+ // res id in the days container
+ when(mDaysContainer.findViewById(R.id.day0)).thenReturn(mDay0);
+ when(mDaysContainer.findViewById(R.id.day1)).thenReturn(mDay1);
+ when(mDaysContainer.findViewById(R.id.day2)).thenReturn(mDay2);
+ when(mDaysContainer.findViewById(R.id.day3)).thenReturn(mDay3);
+ when(mDaysContainer.findViewById(R.id.day4)).thenReturn(mDay4);
+ when(mDaysContainer.findViewById(R.id.day5)).thenReturn(mDay5);
+ when(mDaysContainer.findViewById(R.id.day6)).thenReturn(mDay6);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
index 7dcec1cfeed..91de4ea8348 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
@@ -17,6 +17,7 @@
package com.android.settings.notification.modes;
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
@@ -53,6 +54,8 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
+import java.util.Calendar;
+
@RunWith(RobolectricTestRunner.class)
public class ZenModeSetTriggerLinkPreferenceControllerTest {
@Rule
@@ -167,4 +170,29 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest {
captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo(
ZenModeSetCalendarFragment.class.getName());
}
+
+ @Test
+ public void testRuleLink_schedule() {
+ ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
+ scheduleInfo.days = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.THURSDAY };
+ scheduleInfo.startHour = 1;
+ scheduleInfo.endHour = 15;
+ ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name",
+ ZenModeConfig.toScheduleConditionId(scheduleInfo))
+ .setType(TYPE_SCHEDULE_TIME)
+ .setTriggerDescription("some schedule")
+ .build(),
+ true); // is active
+ mPrefController.updateZenMode(mPrefCategory, mode);
+
+ verify(mPreference).setTitle(R.string.zen_mode_set_schedule_link);
+ verify(mPreference).setSummary(mode.getRule().getTriggerDescription());
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class);
+ verify(mPreference).setIntent(captor.capture());
+ // Destination as written into the intent by SubSettingLauncher
+ assertThat(
+ captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo(
+ ZenModeSetScheduleFragment.class.getName());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockTypeDialogFragmentTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockTypeDialogFragmentTest.java
index 68f8ed736d1..24418bfeb41 100644
--- a/tests/robotests/src/com/android/settings/password/ChooseLockTypeDialogFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/password/ChooseLockTypeDialogFragmentTest.java
@@ -37,7 +37,6 @@ import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@@ -47,7 +46,6 @@ import org.robolectric.shadows.androidx.fragment.FragmentController;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowAlertDialogCompat.class, ShadowLockPatternUtils.class})
-@Ignore("b/342667939")
public class ChooseLockTypeDialogFragmentTest {
private Context mContext;
diff --git a/tests/spa_unit/src/com/android/settings/network/ConnectivityRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/ConnectivityRepositoryTest.kt
new file mode 100644
index 00000000000..170b84d8884
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/ConnectivityRepositoryTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.android.settingslib.spa.testutils.toListWithTimeout
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class ConnectivityRepositoryTest {
+
+ private var networkCallback: NetworkCallback? = null
+
+ private val mockConnectivityManager = mock {
+ on { registerDefaultNetworkCallback(any()) } doAnswer {
+ networkCallback = it.arguments[0] as NetworkCallback
+ }
+ }
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { getSystemService(ConnectivityManager::class.java) } doReturn mockConnectivityManager
+ }
+
+ private val connectivityRepository = ConnectivityRepository(context)
+
+ @Test
+ fun networkCapabilitiesFlow_activeNetworkIsNull_noCrash() = runBlocking {
+ mockConnectivityManager.stub {
+ on { activeNetwork } doReturn null
+ on { getNetworkCapabilities(null) } doReturn null
+ }
+
+ val networkCapabilities =
+ connectivityRepository.networkCapabilitiesFlow().firstWithTimeoutOrNull()!!
+
+ assertThat(networkCapabilities.transportTypes).isEmpty()
+ }
+
+ @Test
+ fun networkCapabilitiesFlow_getInitialValue() = runBlocking {
+ val expectedNetworkCapabilities = NetworkCapabilities.Builder().apply {
+ addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ }.build()
+ mockConnectivityManager.stub {
+ on { getNetworkCapabilities(null) } doReturn expectedNetworkCapabilities
+ }
+
+ val actualNetworkCapabilities =
+ connectivityRepository.networkCapabilitiesFlow().firstWithTimeoutOrNull()!!
+
+ assertThat(actualNetworkCapabilities).isSameInstanceAs(expectedNetworkCapabilities)
+ }
+
+ @Test
+ fun networkCapabilitiesFlow_getUpdatedValue() = runBlocking {
+ val expectedNetworkCapabilities = NetworkCapabilities.Builder().apply {
+ addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ }.build()
+
+ val deferredList = async {
+ connectivityRepository.networkCapabilitiesFlow().toListWithTimeout()
+ }
+ delay(100)
+ networkCallback?.onCapabilitiesChanged(mock(), expectedNetworkCapabilities)
+
+ assertThat(deferredList.await().last()).isSameInstanceAs(expectedNetworkCapabilities)
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/InternetPreferenceRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/InternetPreferenceRepositoryTest.kt
new file mode 100644
index 00000000000..4cd65e74505
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/InternetPreferenceRepositoryTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.network
+
+import android.content.Context
+import android.net.NetworkCapabilities
+import android.net.wifi.WifiManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settings.wifi.WifiSummaryRepository
+import com.android.settings.wifi.repository.WifiRepository
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class InternetPreferenceRepositoryTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val mockConnectivityRepository = mock()
+ private val mockWifiSummaryRepository = mock()
+ private val mockWifiRepository = mock()
+ private val airplaneModeOnFlow = MutableStateFlow(false)
+
+ private val repository = InternetPreferenceRepository(
+ context = context,
+ connectivityRepository = mockConnectivityRepository,
+ wifiSummaryRepository = mockWifiSummaryRepository,
+ wifiRepository = mockWifiRepository,
+ airplaneModeOnFlow = airplaneModeOnFlow,
+ )
+
+ @Test
+ fun summaryFlow_wifi() = runBlocking {
+ val wifiNetworkCapabilities = NetworkCapabilities.Builder().apply {
+ addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ }.build()
+ mockConnectivityRepository.stub {
+ on { networkCapabilitiesFlow() } doReturn flowOf(wifiNetworkCapabilities)
+ }
+ mockWifiSummaryRepository.stub {
+ on { summaryFlow() } doReturn flowOf(SUMMARY)
+ }
+
+ val summary = repository.summaryFlow().firstWithTimeoutOrNull()
+
+ assertThat(summary).isEqualTo(SUMMARY)
+ }
+
+ @Test
+ fun summaryFlow_airplaneModeOnAndWifiOn() = runBlocking {
+ mockConnectivityRepository.stub {
+ on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
+ }
+ airplaneModeOnFlow.value = true
+ mockWifiRepository.stub {
+ on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_ENABLED)
+ }
+
+ val summary = repository.summaryFlow().firstWithTimeoutOrNull()
+
+ assertThat(summary).isEqualTo(context.getString(R.string.networks_available))
+ }
+
+ @Test
+ fun summaryFlow_airplaneModeOnAndWifiOff() = runBlocking {
+ mockConnectivityRepository.stub {
+ on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
+ }
+ airplaneModeOnFlow.value = true
+ mockWifiRepository.stub {
+ on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_DISABLED)
+ }
+
+ val summary = repository.summaryFlow().firstWithTimeoutOrNull()
+
+ assertThat(summary).isEqualTo(context.getString(R.string.condition_airplane_title))
+ }
+
+ @Test
+ fun summaryFlow_airplaneModeOff() = runBlocking {
+ mockConnectivityRepository.stub {
+ on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
+ }
+ airplaneModeOnFlow.value = false
+ mockWifiRepository.stub {
+ on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_DISABLED)
+ }
+
+ val summary = repository.summaryFlow().firstWithTimeoutOrNull()
+
+ assertThat(summary).isEqualTo(context.getString(R.string.networks_available))
+ }
+
+ private companion object {
+ const val SUMMARY = "Summary"
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt
new file mode 100644
index 00000000000..dae3617c37e
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.wifi.repository
+
+import android.content.Context
+import android.content.Intent
+import android.net.wifi.WifiManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WifiRepositoryTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val mockWifiStateChangedActionFlow = flowOf(Intent().apply {
+ putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_ENABLED)
+ })
+
+ private val repository = WifiRepository(context, mockWifiStateChangedActionFlow)
+
+ @Test
+ fun wifiStateFlow() = runBlocking {
+ val wifiState = repository.wifiStateFlow().firstWithTimeoutOrNull()
+
+ assertThat(wifiState).isEqualTo(WifiManager.WIFI_STATE_ENABLED)
+ }
+}