diff --git a/Android.bp b/Android.bp
index 8b903ba91be..0a58ee8ea7c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -94,8 +94,10 @@ android_library {
"MediaDrmSettingsFlagsLib",
"Settings-change-ids",
"SettingsLib",
- "SettingsLibDataStore",
"SettingsLibActivityEmbedding",
+ "SettingsLibDataStore",
+ "SettingsLibMetadata",
+ "SettingsLibPreference",
"aconfig_settings_flags_lib",
"accessibility_settings_flags_lib",
"contextualcards",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3f5cd84de19..fd40e905fda 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1297,7 +1297,7 @@
@@ -2813,6 +2813,9 @@
+
@@ -2823,9 +2826,13 @@
+ android:exported="false"
+ android:theme="@style/GlifTheme.Light"
+ android:taskAffinity="com.android.settings.root" />
+
+
@@ -2842,7 +2853,6 @@
-
keyboard, haptics, vibrate,
- Volume, vibration, Do Not Disturb
+ Volume and vibration
+
+ Volume, vibration, Do Not Disturb
Media volume
@@ -13366,9 +13368,9 @@
On
- Audio %1$d%% left, %2$d%% right
+ Audio %1$s left, %2$s right
- Audio %1$d%% right, %2$d%% left
+ Audio %1$s right, %2$s left
Your device name is visible to apps you installed. It may also be seen by other people when you connect to Bluetooth devices, connect to a Wi-Fi network or set up a Wi-Fi hotspot.
@@ -13577,6 +13579,8 @@
Can\u0027t play this audio stream on %1$s.
Listening now
+
+ Paused by host
Stop listening
diff --git a/res/xml/top_level_settings.xml b/res/xml/top_level_settings.xml
index 1ec968a415c..44fe7fcc3d3 100644
--- a/res/xml/top_level_settings.xml
+++ b/res/xml/top_level_settings.xml
@@ -104,8 +104,9 @@
android:key="top_level_sound"
android:order="-90"
android:title="@string/sound_settings"
- android:summary="@string/sound_dashboard_summary"
- settings:highlightableMenuKey="@string/menu_key_sound"/>
+ android:summary="@string/sound_dashboard_summary_with_dnd"
+ settings:highlightableMenuKey="@string/menu_key_sound"
+ settings:controller="com.android.settings.sound.TopLevelSoundPreferenceController"/>
+ android:summary="@string/sound_dashboard_summary_with_dnd"
+ settings:highlightableMenuKey="@string/menu_key_sound"
+ settings:controller="com.android.settings.sound.TopLevelSoundPreferenceController"/>
getSearchIndexableRawData(Context context);
+
+ /**
+ * Returns synonyms of the Accessibility component that is used for search.
+ *
+ * @param context the context that is used for grabbing resources
+ * @param componentName the ComponentName of the accessibility feature
+ * @return a comma separated synonyms e.g. "wifi, wi-fi, network connection"
+ */
+ @NonNull
+ String getSynonymsForComponent(@NonNull Context context, @NonNull ComponentName componentName);
}
diff --git a/src/com/android/settings/accessibility/AccessibilitySearchFeatureProviderImpl.java b/src/com/android/settings/accessibility/AccessibilitySearchFeatureProviderImpl.java
index c358af11d06..94594a1a292 100644
--- a/src/com/android/settings/accessibility/AccessibilitySearchFeatureProviderImpl.java
+++ b/src/com/android/settings/accessibility/AccessibilitySearchFeatureProviderImpl.java
@@ -16,8 +16,12 @@
package com.android.settings.accessibility;
+import android.content.ComponentName;
import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.settingslib.search.SearchIndexableRaw;
import java.util.List;
@@ -27,8 +31,16 @@ import java.util.List;
*/
public class AccessibilitySearchFeatureProviderImpl implements AccessibilitySearchFeatureProvider {
+ @Nullable
@Override
public List getSearchIndexableRawData(Context context) {
return null;
}
+
+ @NonNull
+ @Override
+ public String getSynonymsForComponent(@NonNull Context context,
+ @NonNull ComponentName componentName) {
+ return "";
+ }
}
diff --git a/src/com/android/settings/accessibility/AccessibilityServicePreference.java b/src/com/android/settings/accessibility/AccessibilityServicePreference.java
index c1dfae80fb7..8a22d820af9 100644
--- a/src/com/android/settings/accessibility/AccessibilityServicePreference.java
+++ b/src/com/android/settings/accessibility/AccessibilityServicePreference.java
@@ -26,6 +26,7 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.android.settings.R;
@@ -95,6 +96,11 @@ public class AccessibilityServicePreference extends RestrictedPreference {
super.performClick();
}
+ @NonNull
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
private Drawable getA11yServiceIcon() {
ResolveInfo resolveInfo = mA11yServiceInfo.getResolveInfo();
Drawable serviceIcon;
diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java
index 8de49365060..db8f9379afe 100644
--- a/src/com/android/settings/accessibility/AccessibilitySettings.java
+++ b/src/com/android/settings/accessibility/AccessibilitySettings.java
@@ -473,7 +473,7 @@ public class AccessibilitySettings extends DashboardFragment implements
* @param installedShortcutList A list of installed {@link AccessibilityShortcutInfo}s.
* @param installedServiceList A list of installed {@link AccessibilityServiceInfo}s.
*/
- private List getInstalledAccessibilityPreferences(Context context,
+ private static List getInstalledAccessibilityPreferences(Context context,
List installedShortcutList,
List installedServiceList) {
final RestrictedPreferenceHelper preferenceHelper = new RestrictedPreferenceHelper(context);
@@ -623,6 +623,51 @@ public class AccessibilitySettings extends DashboardFragment implements
.getAccessibilitySearchFeatureProvider().getSearchIndexableRawData(
context);
}
+
+ @Override
+ public List getDynamicRawDataToIndex(Context context,
+ boolean enabled) {
+ List dynamicRawData = super.getDynamicRawDataToIndex(
+ context, enabled);
+ if (dynamicRawData == null) {
+ dynamicRawData = new ArrayList<>();
+ }
+ if (!Flags.fixA11ySettingsSearch()) {
+ return dynamicRawData;
+ }
+
+ AccessibilityManager a11yManager = context.getSystemService(
+ AccessibilityManager.class);
+ AccessibilitySearchFeatureProvider a11ySearchFeatureProvider =
+ FeatureFactory.getFeatureFactory()
+ .getAccessibilitySearchFeatureProvider();
+ List installedA11yFeaturesPref =
+ AccessibilitySettings.getInstalledAccessibilityPreferences(
+ context,
+ a11yManager.getInstalledAccessibilityShortcutListAsUser(
+ context, UserHandle.myUserId()),
+ a11yManager.getInstalledAccessibilityServiceList()
+ );
+ for (RestrictedPreference pref : installedA11yFeaturesPref) {
+ SearchIndexableRaw indexableRaw = new SearchIndexableRaw(context);
+ indexableRaw.key = pref.getKey();
+ indexableRaw.title = pref.getTitle().toString();
+ @NonNull String synonyms = "";
+ if (pref instanceof AccessibilityServicePreference) {
+ synonyms = a11ySearchFeatureProvider.getSynonymsForComponent(
+ context,
+ ((AccessibilityServicePreference) pref).getComponentName());
+ } else if (pref instanceof AccessibilityActivityPreference) {
+ synonyms = a11ySearchFeatureProvider.getSynonymsForComponent(
+ context,
+ ((AccessibilityActivityPreference) pref).getComponentName());
+ }
+ indexableRaw.keywords = synonyms;
+ dynamicRawData.add(indexableRaw);
+ }
+
+ return dynamicRawData;
+ }
};
@Override
diff --git a/src/com/android/settings/accessibility/BalanceSeekBar.java b/src/com/android/settings/accessibility/BalanceSeekBar.java
index 7441d6fe9e2..8f8f767cebf 100644
--- a/src/com/android/settings/accessibility/BalanceSeekBar.java
+++ b/src/com/android/settings/accessibility/BalanceSeekBar.java
@@ -36,6 +36,7 @@ import android.widget.SeekBar;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
+import com.android.settings.Utils;
/**
* A custom seekbar for the balance setting.
@@ -178,10 +179,12 @@ public class BalanceSeekBar extends SeekBar {
== LAYOUT_DIRECTION_RTL;
final int rightPercent = (int) (100 * (progress / max));
final int leftPercent = 100 - rightPercent;
+ final String rightPercentString = Utils.formatPercentage(rightPercent);
+ final String leftPercentString = Utils.formatPercentage(leftPercent);
if (rightPercent > leftPercent || (rightPercent == leftPercent && isLayoutRtl)) {
- return context.getString(resIdRightFirst, rightPercent, leftPercent);
+ return context.getString(resIdRightFirst, rightPercentString, leftPercentString);
} else {
- return context.getString(resIdLeftFirst, leftPercent, rightPercent);
+ return context.getString(resIdLeftFirst, leftPercentString, rightPercentString);
}
}
}
diff --git a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
index 72a0f08c30f..b38c42c7208 100644
--- a/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
+++ b/src/com/android/settings/activityembedding/ActivityEmbeddingRulesController.java
@@ -40,9 +40,8 @@ import com.android.settings.SettingsActivity;
import com.android.settings.SubSettings;
import com.android.settings.biometrics.face.FaceEnrollIntroduction;
import com.android.settings.biometrics.face.FaceEnrollIntroductionInternal;
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollActivityClassProvider;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
-import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
-import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal;
import com.android.settings.core.FeatureFlags;
import com.android.settings.homepage.DeepLinkHomepageActivity;
import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
@@ -255,8 +254,12 @@ public class ActivityEmbeddingRulesController {
.buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
addActivityFilter(activityFilters, searchIntent);
}
- addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
- addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
+ final FingerprintEnrollActivityClassProvider fpClassProvider = FeatureFactory
+ .getFeatureFactory()
+ .getFingerprintFeatureProvider()
+ .getEnrollActivityClassProvider();
+ addActivityFilter(activityFilters, fpClassProvider.getDefault());
+ addActivityFilter(activityFilters, fpClassProvider.getInternal());
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
addActivityFilter(activityFilters, FaceEnrollIntroductionInternal.class);
addActivityFilter(activityFilters, FaceEnrollIntroduction.class);
diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java
index 09b2dba89b3..2a457f50e9b 100644
--- a/src/com/android/settings/biometrics/BiometricUtils.java
+++ b/src/com/android/settings/biometrics/BiometricUtils.java
@@ -44,10 +44,9 @@ import com.android.internal.widget.VerifyCredentialResponse;
import com.android.settings.R;
import com.android.settings.SetupWizardUtils;
import com.android.settings.biometrics.face.FaceEnrollIntroduction;
+import com.android.settings.biometrics.fingerprint.FingerprintEnroll;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor;
-import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor;
-import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.password.ChooseLockGeneric;
import com.android.settings.password.ChooseLockSettingsHelper;
@@ -262,13 +261,13 @@ public class BiometricUtils {
/**
* @param context caller's context
* @param activityIntent The intent that started the caller's activity
- * @return Intent for starting FingerprintEnrollIntroduction
+ * @return Intent for starting FingerprintEnroll
*/
public static Intent getFingerprintIntroIntent(@NonNull Context context,
@NonNull Intent activityIntent) {
final boolean isSuw = WizardManagerHelper.isAnySetupWizard(activityIntent);
final Intent intent = new Intent(context, isSuw
- ? SetupFingerprintEnrollIntroduction.class : FingerprintEnrollIntroduction.class);
+ ? FingerprintEnroll.SetupActivity.class : FingerprintEnroll.class);
if (isSuw) {
WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent);
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnroll.kt b/src/com/android/settings/biometrics/fingerprint/FingerprintEnroll.kt
new file mode 100644
index 00000000000..3d782697541
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnroll.kt
@@ -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.biometrics.fingerprint
+
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import androidx.appcompat.app.AppCompatActivity
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+
+/**
+ * Default class for handling fingerprint enrollment, designed to launch a subsequent activity and
+ * forward the result, then finish itself.
+ */
+open class FingerprintEnroll: AppCompatActivity() {
+
+ /** Inner class representing enrolling fingerprint enrollment in SetupWizard environment */
+ class SetupActivity : FingerprintEnroll() {
+ override val nextActivityClass: Class<*>
+ get() = enrollActivityProvider.setup
+ }
+
+ /** Inner class representing enrolling fingerprint enrollment from FingerprintSettings */
+ class InternalActivity : FingerprintEnroll() {
+ override val nextActivityClass: Class<*>
+ get() = enrollActivityProvider.internal
+ }
+
+ /**
+ * The class of the next activity to launch. This is open to allow subclasses to provide their
+ * own behavior. Defaults to the default activity class provided by the
+ * enrollActivityClassProvider.
+ */
+ open val nextActivityClass: Class<*>
+ get() = enrollActivityProvider.default
+
+ protected val enrollActivityProvider: FingerprintEnrollActivityClassProvider
+ get() = featureFactory.fingerprintFeatureProvider.enrollActivityClassProvider
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ /**
+ * Logs the next activity to be launched, creates an intent for that activity,
+ * adds flags to forward the result, includes any existing extras from the current intent,
+ * starts the new activity and then finishes the current one
+ */
+ Log.d("FingerprintEnroll", "forward to $nextActivityClass")
+ val nextIntent = Intent(this, nextActivityClass)
+ nextIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ nextIntent.putExtras(intent)
+ startActivity(nextIntent)
+ finish()
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollActivityClassProvider.kt b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollActivityClassProvider.kt
new file mode 100644
index 00000000000..853a3df01b8
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollActivityClassProvider.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.biometrics.fingerprint
+
+import android.app.Activity
+
+open class FingerprintEnrollActivityClassProvider {
+
+ open val default: Class
+ get() = FingerprintEnrollIntroduction::class.java
+ open val setup: Class
+ get() = SetupFingerprintEnrollIntroduction::class.java
+ open val internal: Class
+ get() = FingerprintEnrollIntroductionInternal::class.java
+
+ companion object {
+ @JvmStatic
+ val instance = FingerprintEnrollActivityClassProvider()
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
index c1e34a579a8..baa88b5655a 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
@@ -33,7 +33,6 @@ public interface FingerprintFeatureProvider {
*/
SfpsEnrollmentFeature getSfpsEnrollmentFeature();
-
/**
* Gets calibrator for udfps pre-enroll
* @param appContext application context
@@ -52,4 +51,13 @@ public interface FingerprintFeatureProvider {
* @return the feature implementation
*/
SfpsRestToUnlockFeature getSfpsRestToUnlockFeature(@NonNull Context context);
+
+ /**
+ * Gets the provider for current fingerprint enrollment activity classes
+ * @return the provider
+ */
+ @NonNull
+ default FingerprintEnrollActivityClassProvider getEnrollActivityClassProvider() {
+ return FingerprintEnrollActivityClassProvider.getInstance();
+ }
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index 125691fbf1c..20d453f2ea8 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -1142,7 +1142,7 @@ public class FingerprintSettings extends SubSettings {
private void addFirstFingerprint(@Nullable Long gkPwHandle) {
Intent intent = new Intent();
intent.setClassName(SETTINGS_PACKAGE_NAME,
- FingerprintEnrollIntroductionInternal.class.getName());
+ FingerprintEnroll.InternalActivity.class.getName());
intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true);
intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE);
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
index 241eaea0b28..d9289d6f107 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/settings/fragment/FingerprintSettingsV2Fragment.kt
@@ -43,7 +43,7 @@ import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY
import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
-import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
+import com.android.settings.biometrics.fingerprint.FingerprintEnroll.InternalActivity
import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl
import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel
@@ -514,7 +514,7 @@ class FingerprintSettingsV2Fragment :
val intent = Intent()
intent.setClassName(
SETTINGS_PACKAGE_NAME,
- FingerprintEnrollIntroductionInternal::class.java.name,
+ InternalActivity::class.java.name,
)
intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true)
intent.putExtra(
diff --git a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
index 87e2e8b4962..5987e5a2079 100644
--- a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
+++ b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
@@ -22,4 +22,7 @@ import kotlinx.coroutines.flow.Flow
data class DeviceSettingLayout(val rows: List)
/** Represent a row in the layout. */
-data class DeviceSettingLayoutRow(val settingIds: Flow>)
+data class DeviceSettingLayoutRow(val columns: Flow>)
+
+/** Represent a column in a row. */
+data class DeviceSettingLayoutColumn(val settingId: Int, val highlighted: Boolean)
diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
index f2a569d2245..a5997e7bc83 100644
--- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
+++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
@@ -20,12 +20,23 @@ import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.media.AudioManager
import android.os.Bundle
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
@@ -43,7 +54,6 @@ import com.android.settings.core.SubSettingLauncher
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice
-import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -91,10 +101,16 @@ class DeviceDetailsFragmentFormatterImpl(
) : DeviceDetailsFragmentFormatter {
private val repository =
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
- context, bluetoothAdapter, fragment.lifecycleScope)
+ context,
+ bluetoothAdapter,
+ fragment.lifecycleScope,
+ )
private val spatialAudioInteractor =
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
- context, context.getSystemService(AudioManager::class.java), fragment.lifecycleScope)
+ context,
+ context.getSystemService(AudioManager::class.java),
+ fragment.lifecycleScope,
+ )
private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider(
fragment,
@@ -104,7 +120,8 @@ class DeviceDetailsFragmentFormatterImpl(
spatialAudioInteractor,
cachedDevice,
backgroundCoroutineContext,
- ))
+ ),
+ )
.get(BluetoothDeviceDetailsViewModel::class.java)
override fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List? =
@@ -120,7 +137,8 @@ class DeviceDetailsFragmentFormatterImpl(
viewModel
.getItems(fragmentType)
?.filterIsInstance()
- ?.first()?.invisibleProfiles
+ ?.first()
+ ?.invisibleProfiles
}
/** Updates bluetooth device details fragment layout. */
@@ -144,7 +162,8 @@ class DeviceDetailsFragmentFormatterImpl(
val settingId = items[row].settingId
if (settingIdToXmlPreferences.containsKey(settingId)) {
fragment.preferenceScreen.addPreference(
- settingIdToXmlPreferences[settingId]!!.apply { order = row })
+ settingIdToXmlPreferences[settingId]!!.apply { order = row }
+ )
} else {
val pref =
ComposePreference(context)
@@ -169,7 +188,8 @@ class DeviceDetailsFragmentFormatterImpl(
emitAll(
viewModel.getDeviceSetting(cachedDevice, item.settingId).map {
it as? DeviceSettingPreferenceModel.HelpPreference
- })
+ }
+ )
} ?: emit(null)
}
@@ -177,22 +197,56 @@ class DeviceDetailsFragmentFormatterImpl(
private fun buildPreference(layout: DeviceSettingLayout, row: Int) {
val contents by
remember(row) {
- layout.rows[row].settingIds.flatMapLatest { settingIds ->
- if (settingIds.isEmpty()) {
+ layout.rows[row].columns.flatMapLatest { columns ->
+ if (columns.isEmpty()) {
flowOf(emptyList())
} else {
combine(
- settingIds.map { settingId ->
- viewModel.getDeviceSetting(cachedDevice, settingId)
- }) {
- it.toList()
+ columns.map { column ->
+ viewModel.getDeviceSetting(cachedDevice, column.settingId)
}
+ ) {
+ it.toList()
+ }
}
}
}
.collectAsStateWithLifecycle(initialValue = listOf())
+ val highlighted by
+ remember(row) {
+ layout.rows[row].columns.map { columns -> columns.any { it.highlighted } }
+ }
+ .collectAsStateWithLifecycle(initialValue = false)
+
val settings = contents
+ AnimatedVisibility(
+ visible = settings.isNotEmpty(),
+ enter = expandVertically(expandFrom = Alignment.Top),
+ exit = shrinkVertically(shrinkTowards = Alignment.Top),
+ ) {
+ Box {
+ Box(
+ modifier =
+ Modifier.matchParentSize()
+ .padding(16.dp, 0.dp, 8.dp, 0.dp)
+ .background(
+ color =
+ if (highlighted) {
+ MaterialTheme.colorScheme.primaryContainer
+ } else {
+ Color.Transparent
+ },
+ shape = RoundedCornerShape(28.dp),
+ ),
+ ) {}
+ buildPreferences(settings)
+ }
+ }
+ }
+
+ @Composable
+ fun buildPreferences(settings: List) {
when (settings.size) {
0 -> {}
1 -> {
@@ -217,11 +271,18 @@ class DeviceDetailsFragmentFormatterImpl(
}
}
else -> {
- if (!settings.all { it is DeviceSettingPreferenceModel.MultiTogglePreference }) {
+ if (
+ !settings.all {
+ it is DeviceSettingPreferenceModel.MultiTogglePreference
+ }
+ ) {
return
}
buildMultiTogglePreference(
- settings.filterIsInstance())
+ settings.filterIsInstance<
+ DeviceSettingPreferenceModel.MultiTogglePreference
+ >()
+ )
}
}
}
@@ -243,11 +304,19 @@ class DeviceDetailsFragmentFormatterImpl(
override val onCheckedChange = { newChecked: Boolean ->
model.onCheckedChange(newChecked)
}
- override val icon = @Composable { deviceSettingIcon(model.icon) }
+ override val icon: (@Composable () -> Unit)?
+ get() {
+ if (model.icon == null) {
+ return null
+ }
+ return { deviceSettingIcon(model.icon) }
+ }
}
if (model.onPrimaryClick != null) {
TwoTargetSwitchPreference(
- switchPrefModel, primaryOnClick = model.onPrimaryClick::invoke)
+ switchPrefModel,
+ primaryOnClick = model.onPrimaryClick::invoke,
+ )
} else {
SwitchPreference(switchPrefModel)
}
@@ -263,8 +332,15 @@ class DeviceDetailsFragmentFormatterImpl(
model.onClick?.invoke()
Unit
}
- override val icon = @Composable { deviceSettingIcon(model.icon) }
- })
+ override val icon: (@Composable () -> Unit)?
+ get() {
+ if (model.icon == null) {
+ return null
+ }
+ return { deviceSettingIcon(model.icon) }
+ }
+ }
+ )
}
@Composable
@@ -281,11 +357,13 @@ class DeviceDetailsFragmentFormatterImpl(
.setDestination(DeviceDetailsMoreSettingsFragment::class.java.name)
.setSourceMetricsCategory(fragment.getMetricsCategory())
.setArguments(
- Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) })
+ Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) }
+ )
.launch()
}
override val icon = @Composable { deviceSettingIcon(null) }
- })
+ }
+ )
}
@Composable
diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
index 1071adce37f..67a0ebc8398 100644
--- a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
+++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
@@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope
import com.android.settings.R
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
@@ -36,7 +37,6 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@@ -51,7 +51,7 @@ class BluetoothDeviceDetailsViewModel(
private val spatialAudioInteractor: SpatialAudioInteractor,
private val cachedDevice: CachedBluetoothDevice,
backgroundCoroutineContext: CoroutineContext,
-) : AndroidViewModel(application){
+) : AndroidViewModel(application) {
private val items =
viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) {
@@ -74,7 +74,7 @@ class BluetoothDeviceDetailsViewModel(
fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice,
- @DeviceSettingId settingId: Int
+ @DeviceSettingId settingId: Int,
): Flow {
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) {
return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId))
@@ -98,16 +98,19 @@ class BluetoothDeviceDetailsViewModel(
checked = switchState?.checked ?: false,
onCheckedChange = { newState ->
updateState?.invoke(
- DeviceSettingStateModel.ActionSwitchPreferenceState(newState))
+ DeviceSettingStateModel.ActionSwitchPreferenceState(newState)
+ )
},
- onPrimaryClick = { intent?.let { application.startActivity(it) } })
+ onPrimaryClick = { intent?.let { application.startActivity(it) } },
+ )
} else {
DeviceSettingPreferenceModel.PlainPreference(
id = id,
title = title,
summary = summary,
icon = icon,
- onClick = { intent?.let { application.startActivity(it) } })
+ onClick = { intent?.let { application.startActivity(it) } },
+ )
}
}
is DeviceSettingModel.FooterPreference ->
@@ -116,9 +119,8 @@ class BluetoothDeviceDetailsViewModel(
DeviceSettingPreferenceModel.HelpPreference(
id = id,
icon = DeviceSettingIcon.ResourceIcon(R.drawable.ic_help),
- onClick = {
- application.startActivity(intent)
- })
+ onClick = { application.startActivity(intent) },
+ )
is DeviceSettingModel.MultiTogglePreference ->
DeviceSettingPreferenceModel.MultiTogglePreference(
id = id,
@@ -129,7 +131,8 @@ class BluetoothDeviceDetailsViewModel(
isAllowedChangingState = isAllowedChangingState,
onSelectedChange = { newState ->
updateState(DeviceSettingStateModel.MultiTogglePreferenceState(newState))
- })
+ },
+ )
is DeviceSettingModel.Unknown -> null
}
}
@@ -145,8 +148,8 @@ class BluetoothDeviceDetailsViewModel(
configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
val positionToSettingIds =
combine(configDeviceSetting) { settings ->
- val positionMapping = mutableMapOf>()
- var multiToggleSettingIds: MutableList? = null
+ val positionMapping = mutableMapOf>()
+ var multiToggleSettingIds: MutableList? = null
for (i in settings.indices) {
val configItem = configItems[i]
val setting = settings[i]
@@ -156,14 +159,31 @@ class BluetoothDeviceDetailsViewModel(
}
if (setting !is DeviceSettingPreferenceModel.MultiTogglePreference) {
multiToggleSettingIds = null
- positionMapping[i] = listOf(configItem.settingId)
+ positionMapping[i] =
+ listOf(
+ DeviceSettingLayoutColumn(
+ configItem.settingId,
+ configItem.highlighted,
+ )
+ )
continue
}
if (multiToggleSettingIds != null) {
- multiToggleSettingIds.add(setting.id)
+ multiToggleSettingIds.add(
+ DeviceSettingLayoutColumn(
+ configItem.settingId,
+ configItem.highlighted,
+ )
+ )
} else {
- multiToggleSettingIds = mutableListOf(setting.id)
+ multiToggleSettingIds =
+ mutableListOf(
+ DeviceSettingLayoutColumn(
+ configItem.settingId,
+ configItem.highlighted,
+ )
+ )
positionMapping[i] = multiToggleSettingIds
}
}
@@ -173,7 +193,8 @@ class BluetoothDeviceDetailsViewModel(
return DeviceSettingLayout(
configItems.indices.map { idx ->
DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() })
- })
+ }
+ )
}
class Factory(
@@ -186,9 +207,12 @@ class BluetoothDeviceDetailsViewModel(
override fun create(modelClass: Class): T {
@Suppress("UNCHECKED_CAST")
return BluetoothDeviceDetailsViewModel(
- application, deviceSettingRepository, spatialAudioInteractor,
+ application,
+ deviceSettingRepository,
+ spatialAudioInteractor,
cachedDevice,
- backgroundCoroutineContext)
+ backgroundCoroutineContext,
+ )
as T
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
index 939dd5c2f92..48acf3256d0 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
+
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
@@ -41,6 +43,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.ActionButtonsPreference;
+import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -73,12 +76,18 @@ public class AudioStreamButtonController extends BasePreferenceController
int sourceId,
BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state);
- if (AudioStreamsHelper.isConnected(state)) {
+ boolean shouldUpdateButton =
+ audioSharingHysteresisModeFix()
+ ? AudioStreamsHelper.hasSourcePresent(state)
+ : AudioStreamsHelper.isConnected(state);
+ if (shouldUpdateButton) {
updateButton();
- mMetricsFeatureProvider.action(
- mContext,
- SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED,
- SOURCE_ORIGIN_REPOSITORY);
+ if (AudioStreamsHelper.isConnected(state)) {
+ mMetricsFeatureProvider.action(
+ mContext,
+ SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED,
+ SOURCE_ORIGIN_REPOSITORY);
+ }
}
}
@@ -146,8 +155,13 @@ public class AudioStreamButtonController extends BasePreferenceController
Log.w(TAG, "updateButton(): preference is null!");
return;
}
+
+ List sources =
+ audioSharingHysteresisModeFix()
+ ? mAudioStreamsHelper.getAllPresentSources()
+ : mAudioStreamsHelper.getAllConnectedSources();
boolean isConnected =
- mAudioStreamsHelper.getAllConnectedSources().stream()
+ sources.stream()
.map(BluetoothLeBroadcastReceiveState::getBroadcastId)
.anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId);
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
index e1a178d87e6..0ee93e7742e 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
@@ -16,6 +16,10 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
+
+import static java.util.stream.Collectors.toList;
+
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -48,6 +52,8 @@ public class AudioStreamHeaderController extends BasePreferenceController
static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY =
R.string.audio_streams_listening_now;
+ static final int AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY = R.string.audio_streams_present_now;
+
@VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = "";
private static final String TAG = "AudioStreamHeaderController";
private static final String KEY = "audio_stream_header";
@@ -80,6 +86,10 @@ public class AudioStreamHeaderController extends BasePreferenceController
updateSummary();
mAudioStreamsHelper.startMediaService(
mContext, mBroadcastId, mBroadcastName);
+ } else if (audioSharingHysteresisModeFix()
+ && AudioStreamsHelper.hasSourcePresent(state)) {
+ // if source present but not connected, only update the summary
+ updateSummary();
}
}
};
@@ -140,8 +150,27 @@ public class AudioStreamHeaderController extends BasePreferenceController
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
+ var connectedSourceList =
+ mAudioStreamsHelper.getAllPresentSources().stream()
+ .filter(
+ state ->
+ (state.getBroadcastId()
+ == mBroadcastId))
+ .collect(toList());
+
var latestSummary =
- mAudioStreamsHelper.getAllConnectedSources().stream()
+ audioSharingHysteresisModeFix()
+ ? connectedSourceList.isEmpty()
+ ? AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY
+ : (connectedSourceList.stream()
+ .anyMatch(
+ AudioStreamsHelper
+ ::isConnected)
+ ? mContext.getString(
+ AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
+ : mContext.getString(
+ AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY))
+ : mAudioStreamsHelper.getAllConnectedSources().stream()
.map(
BluetoothLeBroadcastReceiveState
::getBroadcastId)
@@ -149,9 +178,10 @@ public class AudioStreamHeaderController extends BasePreferenceController
connectedBroadcastId ->
connectedBroadcastId
== mBroadcastId)
- ? mContext.getString(
- AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
- : AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
+ ? mContext.getString(
+ AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
+ : AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
+
ThreadUtils.postOnMainThread(
() -> {
if (mHeaderController != null) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java
index 758984fe432..458cfab55ff 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java
@@ -18,6 +18,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
+import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
+
import android.os.Handler;
import android.os.Looper;
import android.text.SpannableString;
@@ -94,8 +96,12 @@ class AudioStreamStateHandler {
}
preference.setIsConnected(
newState
- == AudioStreamsProgressCategoryController.AudioStreamState
- .SOURCE_ADDED);
+ == AudioStreamsProgressCategoryController
+ .AudioStreamState.SOURCE_ADDED
+ || (audioSharingHysteresisModeFix()
+ && newState
+ == AudioStreamsProgressCategoryController
+ .AudioStreamState.SOURCE_PRESENT));
preference.setOnPreferenceClickListener(getOnClickListener(controller));
});
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
index c219e0b6de3..c0d91626d78 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -19,6 +19,7 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
+import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList;
@@ -63,6 +64,12 @@ public class AudioStreamsHelper {
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+ // Referring to Broadcast Audio Scan Service 1.0
+ // Table 3.9: Broadcast Receive State characteristic format
+ // 0x00000000: 0b0 = Not synchronized to BIS_index[x]
+ // 0xFFFFFFFF: Failed to sync to BIG
+ private static final long BIS_SYNC_NOT_SYNC_TO_BIS = 0x00000000L;
+ private static final long BIS_SYNC_FAILED_SYNC_TO_BIG = 0xFFFFFFFFL;
AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
mBluetoothManager = bluetoothManager;
@@ -144,6 +151,19 @@ public class AudioStreamsHelper {
.toList();
}
+ /** Retrieves a list of all LE broadcast receive states from sinks with source present. */
+ @VisibleForTesting
+ public List getAllPresentSources() {
+ if (mLeBroadcastAssistant == null) {
+ Log.w(TAG, "getAllPresentSources(): LeBroadcastAssistant is null!");
+ return emptyList();
+ }
+ return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
+ .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
+ .filter(AudioStreamsHelper::hasSourcePresent)
+ .toList();
+ }
+
/** Retrieves LocalBluetoothLeBroadcastAssistant. */
@VisibleForTesting
@Nullable
@@ -153,7 +173,18 @@ public class AudioStreamsHelper {
/** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
- return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
+ return state.getBisSyncState().stream()
+ .anyMatch(
+ bitmap ->
+ (bitmap != BIS_SYNC_NOT_SYNC_TO_BIS
+ && bitmap != BIS_SYNC_FAILED_SYNC_TO_BIG));
+ }
+
+ /** Checks the connectivity status based on the provided broadcast receive state. */
+ public static boolean hasSourcePresent(BluetoothLeBroadcastReceiveState state) {
+ // Referring to Broadcast Audio Scan Service 1.0
+ // All zero address means no source on the sink device
+ return !state.getSourceDevice().getAddress().equals("00:00:00:00:00:00");
}
static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
@@ -242,7 +273,8 @@ public class AudioStreamsHelper {
List sourceList =
assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty()
- && sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) {
+ && (audioSharingHysteresisModeFix()
+ || sourceList.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d(
TAG,
"Lead device has connected broadcast source, device = "
@@ -253,7 +285,9 @@ public class AudioStreamsHelper {
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List list =
assistant.getAllSources(device.getDevice());
- if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) {
+ if (!list.isEmpty()
+ && (audioSharingHysteresisModeFix()
+ || list.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d(
TAG,
"Member device has connected broadcast source, device = "
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
index 3370d8dbfd5..b379d4e7314 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
+
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -39,6 +41,9 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA
mCategoryController.handleSourceConnected(state);
} else if (AudioStreamsHelper.isBadCode(state)) {
mCategoryController.handleSourceConnectBadCode(state);
+ } else if (audioSharingHysteresisModeFix() && AudioStreamsHelper.hasSourcePresent(state)) {
+ // Keep this check as the last, source might also present in above states
+ mCategoryController.handleSourcePresent(state);
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index 9bbf135285c..7ab588260d0 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
+
import static java.util.Collections.emptyList;
import android.app.AlertDialog;
@@ -48,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils;
import java.util.Comparator;
+import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -95,9 +98,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final Comparator mComparator =
Comparator.comparing(
p ->
- p.getAudioStreamState()
- == AudioStreamsProgressCategoryController
- .AudioStreamState.SOURCE_ADDED)
+ (p.getAudioStreamState()
+ == AudioStreamsProgressCategoryController
+ .AudioStreamState.SOURCE_ADDED
+ || (audioSharingHysteresisModeFix()
+ && p.getAudioStreamState()
+ == AudioStreamsProgressCategoryController
+ .AudioStreamState
+ .SOURCE_PRESENT)))
.thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
.reversed();
@@ -113,6 +121,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
ADD_SOURCE_BAD_CODE,
// When addSource result in other bad state.
ADD_SOURCE_FAILED,
+ // Source is present on sink.
+ SOURCE_PRESENT,
// Source is added to active sink.
SOURCE_ADDED,
}
@@ -243,10 +253,13 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else {
// A preference with source founded existed either because it's already
- // connected (SOURCE_ADDED). Any other reason is unexpected. We update the
- // preference with this source and won't change it's state.
+ // connected (SOURCE_ADDED) or present (SOURCE_PRESENT). Any other reason
+ // is unexpected. We update the preference with this source and won't
+ // change it's state.
existingPreference.setAudioStreamMetadata(source);
- if (fromState != AudioStreamState.SOURCE_ADDED) {
+ if (fromState != AudioStreamState.SOURCE_ADDED
+ && (!audioSharingHysteresisModeFix()
+ || fromState != AudioStreamState.SOURCE_PRESENT)) {
Log.w(
TAG,
"handleSourceFound(): unexpected state : "
@@ -346,10 +359,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
var preference = entry.getValue();
- // Look for preference has SOURCE_ADDED state, re-check if they are still connected. If
+ // Look for preference has SOURCE_ADDED or SOURCE_PRESENT state, re-check if they are
+ // still connected. If
// not, means the source is removed from the sink, we move back the preference to SYNCED
// state.
- if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
+ if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
+ || (audioSharingHysteresisModeFix()
+ && preference.getAudioStreamState()
+ == AudioStreamState.SOURCE_PRESENT))
&& mAudioStreamsHelper.getAllConnectedSources().stream()
.noneMatch(
connected ->
@@ -383,6 +400,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
if (!AudioStreamsHelper.isConnected(receiveState)) {
return;
}
+
var broadcastIdConnected = receiveState.getBroadcastId();
if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
// mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
@@ -455,6 +473,58 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
});
}
+ // Find preference by receiveState and decide next state.
+ // Expect one preference existed, move to SOURCE_PRESENT
+ void handleSourcePresent(BluetoothLeBroadcastReceiveState receiveState) {
+ if (DEBUG) {
+ Log.d(TAG, "handleSourcePresent()");
+ }
+ if (!AudioStreamsHelper.hasSourcePresent(receiveState)) {
+ return;
+ }
+
+ var broadcastIdConnected = receiveState.getBroadcastId();
+ if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
+ // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
+ // connected source receiveState.
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "handleSourcePresent() : processing mSourceFromQrCode with broadcastId"
+ + " unset");
+ }
+ boolean updated =
+ maybeUpdateId(
+ AudioStreamsHelper.getBroadcastName(receiveState),
+ receiveState.getBroadcastId());
+ if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) {
+ var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID);
+ mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference);
+ }
+ }
+
+ mBroadcastIdToPreferenceMap.compute(
+ broadcastIdConnected,
+ (k, existingPreference) -> {
+ if (existingPreference == null) {
+ // No existing preference for this source even if it's already connected,
+ // add one and set initial state to SOURCE_PRESENT. This could happen
+ // because
+ // we retrieves the connected source during onStart() from
+ // AudioStreamsHelper#getAllPresentSources() even before the source is
+ // founded by scanning.
+ return addNewPreference(receiveState, AudioStreamState.SOURCE_PRESENT);
+ }
+ if (existingPreference.getAudioStreamState() == AudioStreamState.WAIT_FOR_SYNC
+ && existingPreference.getAudioStreamBroadcastId() == UNSET_BROADCAST_ID
+ && mSourceFromQrCode != null) {
+ existingPreference.setAudioStreamMetadata(mSourceFromQrCode);
+ }
+ moveToState(existingPreference, AudioStreamState.SOURCE_PRESENT);
+ return existingPreference;
+ });
+ }
+
// Find preference by metadata and decide next state.
// Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE
void handleSourceAddRequest(
@@ -530,9 +600,23 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Handle QR code scan, display currently connected streams then start scanning
// sequentially
handleSourceFromQrCodeIfExists();
- mAudioStreamsHelper
- .getAllConnectedSources()
- .forEach(this::handleSourceConnected);
+ if (audioSharingHysteresisModeFix()) {
+ // With hysteresis mode, we prioritize showing connected sources first.
+ // If no connected sources are found, we then show present sources.
+ List sources =
+ mAudioStreamsHelper.getAllConnectedSources();
+ if (!sources.isEmpty()) {
+ sources.forEach(this::handleSourceConnected);
+ } else {
+ mAudioStreamsHelper
+ .getAllPresentSources()
+ .forEach(this::handleSourcePresent);
+ }
+ } else {
+ mAudioStreamsHelper
+ .getAllConnectedSources()
+ .forEach(this::handleSourceConnected);
+ }
mLeBroadcastAssistant.startSearchingForSources(emptyList());
mMediaControlHelper.start();
});
@@ -581,6 +665,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
AddSourceWaitForResponseState.getInstance();
case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance();
case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance();
+ case SOURCE_PRESENT -> SourcePresentState.getInstance();
case SOURCE_ADDED -> SourceAddedState.getInstance();
default -> throw new IllegalArgumentException("Unsupported state: " + state);
};
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentState.java
new file mode 100644
index 00000000000..1e724f16f63
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentState.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.dashboard.DashboardFragment;
+
+class SourcePresentState extends AudioStreamStateHandler {
+ @VisibleForTesting
+ static final int AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY = R.string.audio_streams_present_now;
+
+ @Nullable private static SourcePresentState sInstance = null;
+
+ SourcePresentState() {}
+
+ static SourcePresentState getInstance() {
+ if (sInstance == null) {
+ sInstance = new SourcePresentState();
+ }
+ return sInstance;
+ }
+
+ @Override
+ void performAction(
+ AudioStreamPreference preference,
+ AudioStreamsProgressCategoryController controller,
+ AudioStreamsHelper helper) {
+ // nothing to do
+ }
+
+ @Override
+ int getSummary() {
+ return AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY;
+ }
+
+ @Override
+ Preference.OnPreferenceClickListener getOnClickListener(
+ AudioStreamsProgressCategoryController controller) {
+ return preference -> {
+ var p = (AudioStreamPreference) preference;
+ Bundle broadcast = new Bundle();
+ broadcast.putString(
+ AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle());
+ broadcast.putInt(
+ AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
+
+ new SubSettingLauncher(p.getContext())
+ .setTitleRes(R.string.audio_streams_detail_page_title)
+ .setDestination(AudioStreamDetailsFragment.class.getName())
+ .setSourceMetricsCategory(
+ !(controller.getFragment() instanceof DashboardFragment)
+ ? SettingsEnums.PAGE_UNKNOWN
+ : ((DashboardFragment) controller.getFragment())
+ .getMetricsCategory())
+ .setArguments(broadcast)
+ .launch();
+ return true;
+ };
+ }
+
+ @Override
+ AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
+ return AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT;
+ }
+}
diff --git a/src/com/android/settings/core/InstrumentedPreferenceFragment.java b/src/com/android/settings/core/InstrumentedPreferenceFragment.java
index 4d871d4c3ff..9b03e9b16e2 100644
--- a/src/com/android/settings/core/InstrumentedPreferenceFragment.java
+++ b/src/com/android/settings/core/InstrumentedPreferenceFragment.java
@@ -37,7 +37,6 @@ import com.android.settingslib.core.instrumentation.Instrumentable;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.instrumentation.SettingsJankMonitor;
import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin;
-import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment;
/**
* Instrumented fragment that logs visibility state.
diff --git a/src/com/android/settings/core/ObservablePreferenceFragment.java b/src/com/android/settings/core/ObservablePreferenceFragment.java
new file mode 100644
index 00000000000..997317dbc1a
--- /dev/null
+++ b/src/com/android/settings/core/ObservablePreferenceFragment.java
@@ -0,0 +1,137 @@
+/*
+ * 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.core;
+
+
+import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
+import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+
+import android.annotation.CallSuper;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.preference.PreferenceFragment;
+
+/**
+ * Preference fragment that has hooks to observe fragment lifecycle events.
+ */
+public abstract class ObservablePreferenceFragment extends PreferenceFragment
+ implements LifecycleOwner {
+
+ private final Lifecycle mLifecycle = new Lifecycle(this);
+
+ public Lifecycle getSettingsLifecycle() {
+ return mLifecycle;
+ }
+
+ @CallSuper
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mLifecycle.onAttach(context);
+ }
+
+ @CallSuper
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mLifecycle.onCreate(savedInstanceState);
+ mLifecycle.handleLifecycleEvent(ON_CREATE);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
+ mLifecycle.setPreferenceScreen(preferenceScreen);
+ super.setPreferenceScreen(preferenceScreen);
+ }
+
+ @CallSuper
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mLifecycle.onSaveInstanceState(outState);
+ }
+
+ @CallSuper
+ @Override
+ public void onStart() {
+ mLifecycle.handleLifecycleEvent(ON_START);
+ super.onStart();
+ }
+
+ @CallSuper
+ @Override
+ public void onResume() {
+ mLifecycle.handleLifecycleEvent(ON_RESUME);
+ super.onResume();
+ }
+
+ @CallSuper
+ @Override
+ public void onPause() {
+ mLifecycle.handleLifecycleEvent(ON_PAUSE);
+ super.onPause();
+ }
+
+ @CallSuper
+ @Override
+ public void onStop() {
+ mLifecycle.handleLifecycleEvent(ON_STOP);
+ super.onStop();
+ }
+
+ @CallSuper
+ @Override
+ public void onDestroy() {
+ mLifecycle.handleLifecycleEvent(ON_DESTROY);
+ super.onDestroy();
+ }
+
+ @CallSuper
+ @Override
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
+ mLifecycle.onCreateOptionsMenu(menu, inflater);
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @CallSuper
+ @Override
+ public void onPrepareOptionsMenu(final Menu menu) {
+ mLifecycle.onPrepareOptionsMenu(menu);
+ super.onPrepareOptionsMenu(menu);
+ }
+
+ @CallSuper
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem menuItem) {
+ boolean lifecycleHandled = mLifecycle.onOptionsItemSelected(menuItem);
+ if (!lifecycleHandled) {
+ return super.onOptionsItemSelected(menuItem);
+ }
+ return lifecycleHandled;
+ }
+}
diff --git a/src/com/android/settings/network/MobileNetworkListFragment.kt b/src/com/android/settings/network/MobileNetworkListFragment.kt
index bb88330dcfb..d110779b36d 100644
--- a/src/com/android/settings/network/MobileNetworkListFragment.kt
+++ b/src/com/android/settings/network/MobileNetworkListFragment.kt
@@ -27,13 +27,13 @@ import com.android.settings.R
import com.android.settings.SettingsPreferenceFragment
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.flags.Flags
+import com.android.settings.network.telephony.SimRepository
import com.android.settings.network.telephony.euicc.EuiccRepository
import com.android.settings.search.BaseSearchIndexProvider
import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
import com.android.settings.spa.network.NetworkCellularGroupProvider
import com.android.settingslib.search.SearchIndexable
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
-import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv())
@@ -85,10 +85,11 @@ class MobileNetworkListFragment : DashboardFragment() {
val SEARCH_INDEX_DATA_PROVIDER = SearchIndexProvider()
@VisibleForTesting
- class SearchIndexProvider : BaseSearchIndexProvider(R.xml.network_provider_sims_list) {
+ class SearchIndexProvider(
+ private val simRepositoryFactory: (Context) -> SimRepository = ::SimRepository
+ ) : BaseSearchIndexProvider(R.xml.network_provider_sims_list) {
public override fun isPageSearchEnabled(context: Context): Boolean =
- SubscriptionUtil.isSimHardwareVisible(context) &&
- context.userManager.isAdminUser
+ simRepositoryFactory(context).showMobileNetworkPage()
}
}
}
diff --git a/src/com/android/settings/network/MobileNetworkPreferenceController.java b/src/com/android/settings/network/MobileNetworkPreferenceController.java
deleted file mode 100644
index b49613a7a94..00000000000
--- a/src/com/android/settings/network/MobileNetworkPreferenceController.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2016 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 static android.os.UserHandle.myUserId;
-import static android.os.UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS;
-
-import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
-
-import static androidx.lifecycle.Lifecycle.Event;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.telephony.PhoneStateListener;
-import android.telephony.ServiceState;
-import android.telephony.TelephonyCallback;
-import android.telephony.TelephonyManager;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.LifecycleObserver;
-import androidx.lifecycle.OnLifecycleEvent;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
-
-import com.android.settings.core.PreferenceControllerMixin;
-import com.android.settings.network.telephony.MobileNetworkUtils;
-import com.android.settingslib.RestrictedLockUtilsInternal;
-import com.android.settingslib.RestrictedPreference;
-import com.android.settingslib.Utils;
-import com.android.settingslib.core.AbstractPreferenceController;
-
-public class MobileNetworkPreferenceController extends AbstractPreferenceController
- implements PreferenceControllerMixin, LifecycleObserver {
-
- @VisibleForTesting
- static final String KEY_MOBILE_NETWORK_SETTINGS = "mobile_network_settings";
-
- private final boolean mIsSecondaryUser;
- private final TelephonyManager mTelephonyManager;
- private final UserManager mUserManager;
- private Preference mPreference;
- @VisibleForTesting
- MobileNetworkTelephonyCallback mTelephonyCallback;
-
- private BroadcastReceiver mAirplanModeChangedReceiver;
-
- public MobileNetworkPreferenceController(Context context) {
- super(context);
- mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
- mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- mIsSecondaryUser = !mUserManager.isAdminUser();
-
- mAirplanModeChangedReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateState(mPreference);
- }
- };
- }
-
- @Override
- public boolean isAvailable() {
- return !isUserRestricted() && !Utils.isWifiOnly(mContext);
- }
-
- public boolean isUserRestricted() {
- return mIsSecondaryUser ||
- RestrictedLockUtilsInternal.hasBaseUserRestriction(
- mContext,
- DISALLOW_CONFIG_MOBILE_NETWORKS,
- myUserId());
- }
-
- @Override
- public void displayPreference(PreferenceScreen screen) {
- super.displayPreference(screen);
- mPreference = screen.findPreference(getPreferenceKey());
- }
-
- @Override
- public String getPreferenceKey() {
- return KEY_MOBILE_NETWORK_SETTINGS;
- }
-
- class MobileNetworkTelephonyCallback extends TelephonyCallback implements
- TelephonyCallback.ServiceStateListener {
- @Override
- public void onServiceStateChanged(ServiceState serviceState) {
- updateState(mPreference);
- }
- }
-
- @OnLifecycleEvent(Event.ON_START)
- public void onStart() {
- if (isAvailable()) {
- if (mTelephonyCallback == null) {
- mTelephonyCallback = new MobileNetworkTelephonyCallback();
- }
- mTelephonyManager.registerTelephonyCallback(
- mContext.getMainExecutor(), mTelephonyCallback);
- }
- if (mAirplanModeChangedReceiver != null) {
- mContext.registerReceiver(mAirplanModeChangedReceiver,
- new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
- }
- }
-
- @OnLifecycleEvent(Event.ON_STOP)
- public void onStop() {
- if (mTelephonyCallback != null) {
- mTelephonyManager.unregisterTelephonyCallback(mTelephonyCallback);
- }
- if (mAirplanModeChangedReceiver != null) {
- mContext.unregisterReceiver(mAirplanModeChangedReceiver);
- }
- }
-
- @Override
- public void updateState(Preference preference) {
- super.updateState(preference);
-
- if (preference instanceof RestrictedPreference &&
- ((RestrictedPreference) preference).isDisabledByAdmin()) {
- return;
- }
- preference.setEnabled(Settings.Global.getInt(
- mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) == 0);
- }
-
- @Override
- public boolean handlePreferenceTreeClick(Preference preference) {
- if (KEY_MOBILE_NETWORK_SETTINGS.equals(preference.getKey())) {
- final Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);
- intent.setPackage(SETTINGS_PACKAGE_NAME);
- mContext.startActivity(intent);
- return true;
- }
- return false;
- }
-
- @Override
- public CharSequence getSummary() {
- return MobileNetworkUtils.getCurrentCarrierNameForDisplay(mContext);
- }
-}
diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.java b/src/com/android/settings/network/MobileNetworkSummaryController.java
index 9bf6915a527..45d475f8eb8 100644
--- a/src/com/android/settings/network/MobileNetworkSummaryController.java
+++ b/src/com/android/settings/network/MobileNetworkSummaryController.java
@@ -21,7 +21,6 @@ import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import android.content.Context;
import android.content.Intent;
-import android.os.UserManager;
import android.telephony.SubscriptionManager;
import android.telephony.euicc.EuiccManager;
@@ -35,10 +34,10 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.network.telephony.SimRepository;
import com.android.settings.network.telephony.euicc.EuiccRepository;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.RestrictedPreference;
-import com.android.settingslib.Utils;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.mobile.dataservice.MobileNetworkInfoEntity;
@@ -56,7 +55,6 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController
private static final String KEY = "mobile_network_list";
private final MetricsFeatureProvider mMetricsFeatureProvider;
- private UserManager mUserManager;
private RestrictedPreference mPreference;
private MobileNetworkRepository mMobileNetworkRepository;
@@ -85,7 +83,6 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController
LifecycleOwner lifecycleOwner) {
super(context);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
- mUserManager = context.getSystemService(UserManager.class);
mLifecycleOwner = lifecycleOwner;
mMobileNetworkRepository = MobileNetworkRepository.getInstance(context);
mIsAirplaneModeOn = mMobileNetworkRepository.isAirplaneModeOn();
@@ -185,8 +182,7 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController
@Override
public boolean isAvailable() {
- return SubscriptionUtil.isSimHardwareVisible(mContext) &&
- !Utils.isWifiOnly(mContext) && mUserManager.isAdminUser();
+ return new SimRepository(mContext).showMobileNetworkPage();
}
@Override
diff --git a/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.java b/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.java
deleted file mode 100644
index a5c19adcd99..00000000000
--- a/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2018 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.text.BidiFormatter;
-
-import com.android.settings.R;
-import com.android.settings.Utils;
-import com.android.settings.activityembedding.ActivityEmbeddingUtils;
-import com.android.settings.core.BasePreferenceController;
-
-public class TopLevelNetworkEntryPreferenceController extends BasePreferenceController {
-
- private final MobileNetworkPreferenceController mMobileNetworkPreferenceController;
-
- public TopLevelNetworkEntryPreferenceController(Context context, String preferenceKey) {
- super(context, preferenceKey);
- mMobileNetworkPreferenceController = new MobileNetworkPreferenceController(mContext);
- }
-
- @Override
- public int getAvailabilityStatus() {
- // TODO(b/281597506): Update the ActivityEmbeddingUtils.isEmbeddingActivityEnabled
- // while getting the new API.
- return (Utils.isDemoUser(mContext)
- && !ActivityEmbeddingUtils.isEmbeddingActivityEnabled(mContext))
- ? UNSUPPORTED_ON_DEVICE : AVAILABLE;
- }
-
- @Override
- public CharSequence getSummary() {
- if (mMobileNetworkPreferenceController.isAvailable()) {
- return BidiFormatter.getInstance()
- .unicodeWrap(mContext.getString(R.string.network_dashboard_summary_mobile));
- } else {
- return BidiFormatter.getInstance()
- .unicodeWrap(mContext.getString(R.string.network_dashboard_summary_no_mobile));
- }
- }
-}
diff --git a/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.kt b/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.kt
new file mode 100644
index 00000000000..1722f6ae6b9
--- /dev/null
+++ b/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.text.BidiFormatter
+import com.android.settings.R
+import com.android.settings.Utils
+import com.android.settings.activityembedding.ActivityEmbeddingUtils
+import com.android.settings.core.BasePreferenceController
+import com.android.settings.network.telephony.SimRepository
+
+class TopLevelNetworkEntryPreferenceController
+@JvmOverloads
+constructor(
+ context: Context,
+ preferenceKey: String,
+ private val simRepository: SimRepository = SimRepository(context),
+ private val isDemoUser: () -> Boolean = { Utils.isDemoUser(context) },
+ private val isEmbeddingActivityEnabled: () -> Boolean = {
+ ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)
+ },
+) : BasePreferenceController(context, preferenceKey) {
+
+ override fun getAvailabilityStatus(): Int {
+ // TODO(b/281597506): Update the ActivityEmbeddingUtils.isEmbeddingActivityEnabled
+ // while getting the new API.
+ return if (isDemoUser() && !isEmbeddingActivityEnabled()) {
+ UNSUPPORTED_ON_DEVICE
+ } else {
+ AVAILABLE
+ }
+ }
+
+ override fun getSummary(): CharSequence {
+ val summaryResId =
+ if (simRepository.showMobileNetworkPage()) {
+ R.string.network_dashboard_summary_mobile
+ } else {
+ R.string.network_dashboard_summary_no_mobile
+ }
+ return BidiFormatter.getInstance().unicodeWrap(mContext.getString(summaryResId))
+ }
+}
diff --git a/src/com/android/settings/network/telephony/SimRepository.kt b/src/com/android/settings/network/telephony/SimRepository.kt
new file mode 100644
index 00000000000..ed3c8aa303c
--- /dev/null
+++ b/src/com/android/settings/network/telephony/SimRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.telephony
+
+import android.content.Context
+import android.content.pm.PackageManager
+import com.android.settingslib.spaprivileged.framework.common.userManager
+
+class SimRepository(context: Context) {
+ private val packageManager = context.packageManager
+ private val userManager = context.userManager
+
+ /** Gets whether we show mobile network settings page to the current user. */
+ fun showMobileNetworkPage(): Boolean =
+ packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) && userManager.isAdminUser
+}
diff --git a/src/com/android/settings/notification/zen/ZenModeBehaviorFooterPreferenceController.java b/src/com/android/settings/notification/zen/ZenModeBehaviorFooterPreferenceController.java
index 9332c9b0c2b..82f0816fc86 100644
--- a/src/com/android/settings/notification/zen/ZenModeBehaviorFooterPreferenceController.java
+++ b/src/com/android/settings/notification/zen/ZenModeBehaviorFooterPreferenceController.java
@@ -76,7 +76,7 @@ public class ZenModeBehaviorFooterPreferenceController extends AbstractZenModePr
// DND turned on by an automatic rule with deprecated zen mode
for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) {
- if (automaticRule.isAutomaticActive() && isDeprecatedZenMode(
+ if (automaticRule.isActive() && isDeprecatedZenMode(
automaticRule.zenMode)) {
ComponentName component = automaticRule.component;
if (component != null) {
diff --git a/src/com/android/settings/notification/zen/ZenModeSettingsFooterPreferenceController.java b/src/com/android/settings/notification/zen/ZenModeSettingsFooterPreferenceController.java
index 6a574411a25..4781b360419 100644
--- a/src/com/android/settings/notification/zen/ZenModeSettingsFooterPreferenceController.java
+++ b/src/com/android/settings/notification/zen/ZenModeSettingsFooterPreferenceController.java
@@ -153,7 +153,7 @@ public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePr
// DND turned on by an automatic rule
for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) {
- if (automaticRule.isAutomaticActive()) {
+ if (automaticRule.isActive()) {
// set footer if 3rd party rule
if (!mZenModeConfigWrapper.isTimeRule(automaticRule.conditionId)) {
return mContext.getString(R.string.zen_mode_settings_dnd_automatic_rule,
@@ -180,7 +180,7 @@ public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePr
}
for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) {
- if (automaticRule.isAutomaticActive()) {
+ if (automaticRule.isActive()) {
zenRules.add(automaticRule);
}
}
diff --git a/src/com/android/settings/security/OwnerInfoPreferenceController.java b/src/com/android/settings/security/OwnerInfoPreferenceController.java
index 248301671a0..67dbbc14738 100644
--- a/src/com/android/settings/security/OwnerInfoPreferenceController.java
+++ b/src/com/android/settings/security/OwnerInfoPreferenceController.java
@@ -24,6 +24,7 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.core.ObservablePreferenceFragment;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.users.OwnerInfoSettings;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
@@ -31,7 +32,6 @@ import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
-import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment;
import com.android.settingslib.core.lifecycle.events.OnResume;
public class OwnerInfoPreferenceController extends AbstractPreferenceController
diff --git a/src/com/android/settings/slices/SliceBuilderUtils.java b/src/com/android/settings/slices/SliceBuilderUtils.java
index c9d5f23f8d4..f99267ef241 100644
--- a/src/com/android/settings/slices/SliceBuilderUtils.java
+++ b/src/com/android/settings/slices/SliceBuilderUtils.java
@@ -24,6 +24,7 @@ import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY;
import android.annotation.ColorInt;
import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -47,6 +48,7 @@ import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.SubSettings;
import com.android.settings.Utils;
+import com.android.settings.accessibility.AccessibilitySlicePreferenceController;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SliderPreferenceController;
import com.android.settings.core.SubSettingLauncher;
@@ -448,7 +450,17 @@ public class SliceBuilderUtils {
iconResource = R.drawable.ic_settings_accent;
}
try {
- return IconCompat.createWithResource(context, iconResource);
+ // LINT.IfChange(createA11yIcon)
+ if (AccessibilitySlicePreferenceController.class.getName().equals(
+ data.getPreferenceController())) {
+ ComponentName serviceComponent = ComponentName.unflattenFromString(data.getKey());
+ return IconCompat.createWithResource(
+ context.createPackageContext(serviceComponent.getPackageName(), 0),
+ iconResource);
+ // LINT.ThenChange()
+ } else {
+ return IconCompat.createWithResource(context, iconResource);
+ }
} catch (Exception e) {
Log.w(TAG, "Falling back to settings icon because there is an error getting slice icon "
+ data.getUri(), e);
diff --git a/src/com/android/settings/slices/SliceDataConverter.java b/src/com/android/settings/slices/SliceDataConverter.java
index f6828af3df0..983edc0a588 100644
--- a/src/com/android/settings/slices/SliceDataConverter.java
+++ b/src/com/android/settings/slices/SliceDataConverter.java
@@ -274,6 +274,12 @@ class SliceDataConverter {
final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
final String packageName = serviceInfo.packageName;
final ComponentName componentName = new ComponentName(packageName, serviceInfo.name);
+
+ // If we change the flattenedName that is used to be set as a key of the Slice, we
+ // need to make corresponding change in SliceBuilderUtils, since we rely on the
+ // the A11y Service Slice's key to be a ComponentName to get the correct package name
+ // to grab the icon belongs to that package.
+ // LINT.IfChange
final String flattenedName = componentName.flattenToString();
if (!a11yServiceNames.contains(flattenedName)) {
@@ -287,6 +293,7 @@ class SliceDataConverter {
}
sliceDataBuilder.setKey(flattenedName)
+ // LINT.ThenChange(SliceBuilderUtils.java:createA11yIcon)
.setTitle(title)
.setUri(new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
diff --git a/src/com/android/settings/sound/TopLevelSoundPreferenceController.java b/src/com/android/settings/sound/TopLevelSoundPreferenceController.java
new file mode 100644
index 00000000000..ddc33992d8d
--- /dev/null
+++ b/src/com/android/settings/sound/TopLevelSoundPreferenceController.java
@@ -0,0 +1,45 @@
+/*
+ * 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.sound;
+
+import android.app.Flags;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+public class TopLevelSoundPreferenceController extends BasePreferenceController {
+
+ public TopLevelSoundPreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ super.updateState(preference);
+ preference.setSummary(Flags.modesApi() && Flags.modesUi()
+ ? R.string.sound_dashboard_summary
+ : R.string.sound_dashboard_summary_with_dnd);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE;
+ }
+}
diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
index f76bba45388..d736fe5224a 100644
--- a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
+++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
@@ -48,9 +48,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settings.R
import com.android.settings.flags.Flags
import com.android.settings.network.SubscriptionInfoListViewModel
-import com.android.settings.network.SubscriptionUtil
import com.android.settings.network.telephony.DataSubscriptionRepository
import com.android.settings.network.telephony.MobileDataRepository
+import com.android.settings.network.telephony.SimRepository
import com.android.settings.network.telephony.requireSubscriptionManager
import com.android.settings.spa.network.PrimarySimRepository.PrimarySimInfo
import com.android.settings.spa.search.SearchablePage
@@ -66,7 +66,6 @@ import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Category
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
-import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -213,10 +212,7 @@ open class NetworkCellularGroupProvider : SettingsPageProvider, SearchablePage {
const val fileName = "NetworkCellularGroupProvider"
private fun isPageSearchable(context: Context) =
- Flags.isDualSimOnboardingEnabled() &&
- SubscriptionUtil.isSimHardwareVisible(context) &&
- !com.android.settingslib.Utils.isWifiOnly(context) &&
- context.userManager.isAdminUser
+ Flags.isDualSimOnboardingEnabled() && SimRepository(context).showMobileNetworkPage()
}
}
diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java
index 3982dc0c68c..36578a90e25 100644
--- a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java
@@ -42,7 +42,6 @@ import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.Flags;
import androidx.fragment.app.Fragment;
import androidx.test.core.app.ApplicationProvider;
@@ -50,6 +49,7 @@ import androidx.test.core.app.ApplicationProvider;
import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
+import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.XmlTestUtils;
import com.android.settings.testutils.shadow.ShadowAccessibilityManager;
import com.android.settings.testutils.shadow.ShadowApplicationPackageManager;
@@ -78,6 +78,7 @@ import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowLooper;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
@@ -155,6 +156,53 @@ public class AccessibilitySettingsTest {
assertThat(indexableRawList).isNull();
}
+ @DisableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH)
+ @Test
+ public void getDynamicRawDataToIndex_hasInstalledA11yFeatures_flagOff_returnEmpty() {
+ mShadowAccessibilityManager.setInstalledAccessibilityServiceList(
+ List.of(mServiceInfo));
+ mShadowAccessibilityManager.setInstalledAccessibilityShortcutListAsUser(
+ List.of(getMockAccessibilityShortcutInfo()));
+
+ assertThat(AccessibilitySettings.SEARCH_INDEX_DATA_PROVIDER.getDynamicRawDataToIndex(
+ mContext, /* enabled= */ true))
+ .isEmpty();
+ }
+
+ @EnableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH)
+ @Test
+ public void getDynamicRawDataToIndex_hasInstalledA11yFeatures_flagOn_returnRawDataForInstalledA11yFeatures() {
+ mShadowAccessibilityManager.setInstalledAccessibilityServiceList(
+ List.of(mServiceInfo));
+ mShadowAccessibilityManager.setInstalledAccessibilityShortcutListAsUser(
+ List.of(getMockAccessibilityShortcutInfo()));
+ final AccessibilitySearchFeatureProvider featureProvider =
+ FakeFeatureFactory.setupForTest().getAccessibilitySearchFeatureProvider();
+ final String synonyms = "fake keyword1, fake keyword2";
+ when(featureProvider.getSynonymsForComponent(mContext, ACTIVITY_COMPONENT_NAME))
+ .thenReturn("");
+ when(featureProvider.getSynonymsForComponent(mContext, SERVICE_COMPONENT_NAME))
+ .thenReturn(synonyms);
+
+ final List indexableRawDataList =
+ AccessibilitySettings.SEARCH_INDEX_DATA_PROVIDER.getDynamicRawDataToIndex(
+ mContext, /* enabled= */ true);
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
+
+ assertThat(indexableRawDataList).hasSize(2);
+ SearchIndexableRaw a11yActivityIndexableData = indexableRawDataList.get(0);
+ assertThat(a11yActivityIndexableData.key).isEqualTo(
+ ACTIVITY_COMPONENT_NAME.flattenToString());
+ assertThat(a11yActivityIndexableData.title).isEqualTo(DEFAULT_LABEL);
+ assertThat(a11yActivityIndexableData.keywords).isEmpty();
+
+ SearchIndexableRaw a11yServiceIndexableData = indexableRawDataList.get(1);
+ assertThat(a11yServiceIndexableData.key).isEqualTo(
+ SERVICE_COMPONENT_NAME.flattenToString());
+ assertThat(a11yServiceIndexableData.title).isEqualTo(DEFAULT_LABEL);
+ assertThat(a11yServiceIndexableData.keywords).isEqualTo(synonyms);
+ }
+
@Test
public void getServiceSummary_serviceCrash_showsStopped() {
mServiceInfo.crashed = true;
@@ -328,7 +376,7 @@ public class AccessibilitySettingsTest {
}
@Test
- @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ @DisableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
public void onCreate_flagDisabled_haveRegisterToSpecificUrisAndActions() {
setupFragment();
@@ -341,7 +389,7 @@ public class AccessibilitySettingsTest {
}
@Test
- @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
public void onCreate_flagEnabled_haveRegisterToSpecificUrisAndActions() {
setupFragment();
@@ -415,7 +463,7 @@ public class AccessibilitySettingsTest {
}
@Test
- @EnableFlags(com.android.settings.accessibility.Flags.FLAG_CHECK_PREBUNDLED_IS_PREINSTALLED)
+ @EnableFlags(Flags.FLAG_CHECK_PREBUNDLED_IS_PREINSTALLED)
public void testNonPreinstalledApp_IncludedInDownloadedCategory() {
mShadowAccessibilityManager.setInstalledAccessibilityServiceList(
List.of(getMockAccessibilityServiceInfo(
diff --git a/tests/robotests/src/com/android/settings/accessibility/BalanceSeekBarTest.java b/tests/robotests/src/com/android/settings/accessibility/BalanceSeekBarTest.java
index d74794f0363..bbe511d1b98 100644
--- a/tests/robotests/src/com/android/settings/accessibility/BalanceSeekBarTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/BalanceSeekBarTest.java
@@ -34,6 +34,7 @@ import android.util.AttributeSet;
import android.widget.SeekBar;
import com.android.settings.R;
+import com.android.settings.Utils;
import com.android.settings.testutils.shadow.ShadowSystemSettings;
import org.junit.Before;
@@ -162,7 +163,8 @@ public class BalanceSeekBarTest {
mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true);
assertThat(mSeekBar.getStateDescription()).isEqualTo(
- mContext.getString(R.string.audio_seek_bar_state_left_first, 50, 50));
+ mContext.getString(R.string.audio_seek_bar_state_left_first,
+ Utils.formatPercentage(50), Utils.formatPercentage(50)));
}
@Test
@@ -177,7 +179,8 @@ public class BalanceSeekBarTest {
mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true);
assertThat(mSeekBar.getStateDescription()).isEqualTo(
- mContext.getString(R.string.audio_seek_bar_state_right_first, 50, 50));
+ mContext.getString(R.string.audio_seek_bar_state_right_first,
+ Utils.formatPercentage(50), Utils.formatPercentage(50)));
}
@Test
@@ -189,7 +192,8 @@ public class BalanceSeekBarTest {
mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true);
assertThat(mSeekBar.getStateDescription()).isEqualTo(
- mContext.getString(R.string.audio_seek_bar_state_left_first, 75, 25));
+ mContext.getString(R.string.audio_seek_bar_state_left_first,
+ Utils.formatPercentage(75), Utils.formatPercentage(25)));
}
@Test
@@ -201,7 +205,8 @@ public class BalanceSeekBarTest {
mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true);
assertThat(mSeekBar.getStateDescription()).isEqualTo(
- mContext.getString(R.string.audio_seek_bar_state_right_first, 75, 25));
+ mContext.getString(R.string.audio_seek_bar_state_right_first,
+ Utils.formatPercentage(75), Utils.formatPercentage(25)));
}
// method to get the center from BalanceSeekBar for testing setMax().
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollTest.kt b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollTest.kt
new file mode 100644
index 00000000000..07cdffb942c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.biometrics.fingerprint
+
+import android.app.Activity
+import android.content.Intent
+import com.android.settings.overlay.FeatureFactory
+import com.android.settings.testutils.FakeFeatureFactory
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class FingerprintEnrollTest {
+
+ private lateinit var featureFactory: FeatureFactory
+
+ private companion object {
+ const val INTENT_KEY = "testKey"
+ const val INTENT_VALUE = "testValue"
+ val INTENT = Intent().apply {
+ putExtra(INTENT_KEY, INTENT_VALUE)
+ }
+ }
+
+ private val activityProvider = FingerprintEnrollActivityClassProvider()
+
+ @Before
+ fun setUp() {
+ featureFactory = FakeFeatureFactory.setupForTest()
+ `when`(featureFactory.fingerprintFeatureProvider.enrollActivityClassProvider)
+ .thenReturn(activityProvider)
+ }
+
+ private fun setupActivity(activityClass: Class): FingerprintEnroll {
+ return Robolectric.buildActivity(activityClass, INTENT).create().get()
+ }
+
+ @Test
+ fun testFinishAndLaunchDefaultActivity() {
+ // Run
+ val activity = setupActivity(FingerprintEnroll::class.java)
+
+ // Verify
+ verifyLaunchNextActivity(activity, activityProvider.default)
+ }
+
+ @Test
+ fun testFinishAndLaunchSetupActivity() {
+ // Run
+ val activity = setupActivity(FingerprintEnroll.SetupActivity::class.java)
+
+ // Verify
+ verifyLaunchNextActivity(activity, activityProvider.setup)
+ }
+
+ @Test
+ fun testFinishAndLaunchInternalActivity() {
+ // Run
+ val activity = setupActivity(FingerprintEnroll.InternalActivity::class.java)
+
+ // Verify
+ verifyLaunchNextActivity(activity, activityProvider.internal)
+ }
+
+ private fun verifyLaunchNextActivity(
+ currentActivityInstance : FingerprintEnroll,
+ nextActivityClass: Class
+ ) {
+ assertThat(currentActivityInstance.isFinishing).isTrue()
+ val nextActivityIntent = Shadows.shadowOf(currentActivityInstance).nextStartedActivity
+ assertThat(nextActivityIntent.component!!.className).isEqualTo(nextActivityClass.name)
+ assertThat(nextActivityIntent.extras!!.size()).isEqualTo(1)
+ assertThat(nextActivityIntent.getStringExtra(INTENT_KEY)).isEqualTo(INTENT_VALUE)
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt
index 8070b2e5362..51c0c3076ee 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt
+++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt
@@ -124,10 +124,11 @@ class DeviceDetailsFragmentFormatterTest {
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
- "bluetooth_device_header"
+ highlighted = false,
+ preferenceKey = "bluetooth_device_header"
),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
- DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"),
+ DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, highlighted = false, preferenceKey = "action_buttons"),
),
listOf(),
null))
@@ -157,7 +158,7 @@ class DeviceDetailsFragmentFormatterTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
- listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345)))
+ listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false)))
val intent = Intent().apply {
setAction(Intent.ACTION_VIEW)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -206,10 +207,10 @@ class DeviceDetailsFragmentFormatterTest {
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
- "bluetooth_device_header"),
+ highlighted = false, preferenceKey = "bluetooth_device_header"),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
- "keyboard_settings"),
+ highlighted = false, preferenceKey = "keyboard_settings"),
),
listOf(),
null))
@@ -230,12 +231,14 @@ class DeviceDetailsFragmentFormatterTest {
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
- "bluetooth_device_header"),
+ highlighted = false,
+ preferenceKey = "bluetooth_device_header"),
DeviceSettingConfigItemModel.AppProvidedItem(
- DeviceSettingId.DEVICE_SETTING_ID_ANC),
+ DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
- "keyboard_settings"),
+ highlighted = false,
+ preferenceKey = "keyboard_settings"),
),
listOf(),
null))
diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
index 6869c23fa95..c3f938c3c46 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
+++ b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
@@ -246,11 +246,11 @@ class BluetoothDeviceDetailsViewModelTest {
}
private fun getLatestLayout(layout: DeviceSettingLayout): List> {
- var latestLayout = MutableList(layout.rows.size) { emptyList() }
+ val latestLayout = MutableList(layout.rows.size) { emptyList() }
for (i in layout.rows.indices) {
layout.rows[i]
- .settingIds
- .onEach { latestLayout[i] = it }
+ .columns
+ .onEach { latestLayout[i] = it.map { c -> c.settingId } }
.launchIn(testScope.backgroundScope)
}
@@ -278,15 +278,15 @@ class BluetoothDeviceDetailsViewModelTest {
DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title")
private fun buildRemoteSettingItem(settingId: Int) =
- DeviceSettingConfigItemModel.AppProvidedItem(settingId)
+ DeviceSettingConfigItemModel.AppProvidedItem(settingId, false)
private companion object {
val BUILTIN_SETTING_ITEM_1 =
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
- DeviceSettingId.DEVICE_SETTING_ID_HEADER, "bluetooth_device_header")
+ DeviceSettingId.DEVICE_SETTING_ID_HEADER, false, "bluetooth_device_header")
val BUILDIN_SETTING_ITEM_2 =
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
- DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons")
- val SETTING_ITEM_HELP = DeviceSettingConfigItemModel.AppProvidedItem(12345)
+ DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, false, "action_buttons")
+ val SETTING_ITEM_HELP = DeviceSettingConfigItemModel.AppProvidedItem(12345, false)
}
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java
index c6fb361d656..1d39bc9f0db 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -34,6 +36,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.View;
import androidx.lifecycle.LifecycleOwner;
@@ -72,8 +75,8 @@ import java.util.concurrent.Executor;
ShadowAudioStreamsHelper.class,
})
public class AudioStreamButtonControllerTest {
-
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String KEY = "audio_stream_button";
private static final int BROADCAST_ID = 1;
private final Context mContext = ApplicationProvider.getApplicationContext();
@@ -83,6 +86,7 @@ public class AudioStreamButtonControllerTest {
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
@Mock private AudioStreamsRepository mRepository;
@Mock private ActionButtonsPreference mPreference;
+ @Mock private BluetoothDevice mSourceDevice;
private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner;
private FakeFeatureFactory mFeatureFactory;
@@ -90,6 +94,7 @@ public class AudioStreamButtonControllerTest {
@Before
public void setUp() {
+ mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant);
mFeatureFactory = FakeFeatureFactory.setupForTest();
@@ -254,6 +259,33 @@ public class AudioStreamButtonControllerTest {
.setButton1Icon(com.android.settings.R.drawable.ic_settings_close);
}
+ @Test
+ public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ String address = "11:22:33:44:55:66";
+
+ BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
+ when(state.getBroadcastId()).thenReturn(BROADCAST_ID);
+ when(state.getSourceDevice()).thenReturn(mSourceDevice);
+ when(mSourceDevice.getAddress()).thenReturn(address);
+ List bisSyncState = new ArrayList<>();
+ when(state.getBisSyncState()).thenReturn(bisSyncState);
+ when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(List.of(state));
+
+ mController.displayPreference(mScreen);
+ mController.mBroadcastAssistantCallback.onReceiveStateChanged(
+ mock(BluetoothDevice.class), /* sourceId= */ 0, state);
+
+ verify(mFeatureFactory.metricsFeatureProvider, never())
+ .action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED), anyInt());
+
+ // Called twice, once in displayPreference, the other one in callback
+ verify(mPreference, times(2)).setButton1Enabled(true);
+ verify(mPreference, times(2)).setButton1Text(R.string.audio_streams_disconnect);
+ verify(mPreference, times(2))
+ .setButton1Icon(com.android.settings.R.drawable.ic_settings_close);
+ }
+
@Test
public void testCallback_onSourceAddFailed_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java
index 327090da437..5cdc7974846 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java
@@ -18,6 +18,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY;
+import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -31,6 +33,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
@@ -68,8 +71,9 @@ import java.util.concurrent.Executor;
ShadowAudioStreamsHelper.class,
})
public class AudioStreamHeaderControllerTest {
-
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private static final String KEY = "audio_stream_header";
private static final int BROADCAST_ID = 1;
private static final String BROADCAST_NAME = "broadcast name";
@@ -81,12 +85,15 @@ public class AudioStreamHeaderControllerTest {
@Mock private AudioStreamDetailsFragment mFragment;
@Mock private LayoutPreference mPreference;
@Mock private EntityHeaderController mHeaderController;
+ @Mock private BluetoothDevice mBluetoothDevice;
private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner;
private AudioStreamHeaderController mController;
@Before
public void setUp() {
+ mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+
ShadowEntityHeaderController.setUseMock(mHeaderController);
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant);
@@ -168,6 +175,44 @@ public class AudioStreamHeaderControllerTest {
verify(mScreen).addPreference(any());
}
+ @Test
+ public void testDisplayPreference_sourcePresent_setSummary() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ String address = "11:22:33:44:55:66";
+
+ when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
+ when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice);
+ when(mBluetoothDevice.getAddress()).thenReturn(address);
+ List bisSyncState = new ArrayList<>();
+ when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+ when(mAudioStreamsHelper.getAllPresentSources())
+ .thenReturn(List.of(mBroadcastReceiveState));
+
+ mController.displayPreference(mScreen);
+
+ verify(mHeaderController).setLabel(BROADCAST_NAME);
+ verify(mHeaderController).setIcon(any(Drawable.class));
+ verify(mHeaderController)
+ .setSummary(mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY));
+ verify(mHeaderController).done(true);
+ verify(mScreen).addPreference(any());
+ }
+
+ @Test
+ public void testDisplayPreference_sourceNotPresent_setSummary() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+
+ when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(Collections.emptyList());
+
+ mController.displayPreference(mScreen);
+
+ verify(mHeaderController).setLabel(BROADCAST_NAME);
+ verify(mHeaderController).setIcon(any(Drawable.class));
+ verify(mHeaderController).setSummary(AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY);
+ verify(mHeaderController).done(true);
+ verify(mScreen).addPreference(any());
+ }
+
@Test
public void testCallback_onSourceRemoved_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
@@ -212,4 +257,25 @@ public class AudioStreamHeaderControllerTest {
.setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY));
verify(mHeaderController, times(2)).done(true);
}
+
+ @Test
+ public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ String address = "11:22:33:44:55:66";
+
+ when(mAudioStreamsHelper.getAllPresentSources())
+ .thenReturn(List.of(mBroadcastReceiveState));
+ when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
+ when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice);
+ when(mBluetoothDevice.getAddress()).thenReturn(address);
+
+ mController.displayPreference(mScreen);
+ mController.mBroadcastAssistantCallback.onReceiveStateChanged(
+ mock(BluetoothDevice.class), /* sourceId= */ 0, mBroadcastReceiveState);
+
+ // Called twice, once in displayPreference, the other one in callback
+ verify(mHeaderController, times(2))
+ .setSummary(mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY));
+ verify(mHeaderController, times(2)).done(true);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java
index e44dee90e70..bb873d44575 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -30,6 +32,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.text.SpannableString;
import androidx.preference.Preference;
@@ -48,6 +51,8 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class AudioStreamStateHandlerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private static final int SUMMARY_RES = 1;
private static final String SUMMARY = "summary";
private final Context mContext = spy(ApplicationProvider.getApplicationContext());
@@ -58,6 +63,7 @@ public class AudioStreamStateHandlerTest {
@Before
public void setUp() {
+ mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
mHandler = spy(new AudioStreamStateHandler());
}
@@ -101,6 +107,28 @@ public class AudioStreamStateHandlerTest {
verify(mPreference).setOnPreferenceClickListener(eq(null));
}
+ @Test
+ public void testHandleStateChange_setNewState_sourcePresent() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+
+ when(mHandler.getStateEnum())
+ .thenReturn(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
+ when(mPreference.getAudioStreamState())
+ .thenReturn(
+ AudioStreamsProgressCategoryController.AudioStreamState
+ .ADD_SOURCE_BAD_CODE);
+
+ mHandler.handleStateChange(mPreference, mController, mHelper);
+
+ verify(mPreference)
+ .setAudioStreamState(
+ AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
+ verify(mHandler).performAction(any(), any(), any());
+ verify(mPreference).setIsConnected(eq(true));
+ verify(mPreference).setSummary(eq(""));
+ verify(mPreference).setOnPreferenceClickListener(eq(null));
+ }
+
@Test
public void testHandleStateChange_setNewState_newSummary_newListener() {
Preference.OnPreferenceClickListener listener =
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java
index 42667982eda..fca1137e5c7 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelperTest.java
@@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -37,6 +39,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.fragment.app.FragmentActivity;
import androidx.test.core.app.ApplicationProvider;
@@ -74,6 +77,8 @@ import java.util.List;
})
public class AudioStreamsHelperTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private static final int GROUP_ID = 1;
private static final int BROADCAST_ID_1 = 1;
private static final int BROADCAST_ID_2 = 2;
@@ -86,10 +91,12 @@ public class AudioStreamsHelperTest {
@Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private CachedBluetoothDevice mCachedDevice;
@Mock private BluetoothDevice mDevice;
+ @Mock private BluetoothDevice mSourceDevice;
private AudioStreamsHelper mHelper;
@Before
public void setUp() {
+ mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile())
@@ -166,6 +173,7 @@ public class AudioStreamsHelperTest {
@Test
public void removeSource_memberHasConnectedSource() {
+ String address = "11:22:33:44:55:66";
List devices = new ArrayList<>();
var memberDevice = mock(BluetoothDevice.class);
devices.add(mDevice);
@@ -184,6 +192,8 @@ public class AudioStreamsHelperTest {
List bisSyncState = new ArrayList<>();
bisSyncState.add(1L);
when(source.getBisSyncState()).thenReturn(bisSyncState);
+ when(source.getSourceDevice()).thenReturn(mSourceDevice);
+ when(mSourceDevice.getAddress()).thenReturn(address);
mHelper.removeSource(BROADCAST_ID_2);
@@ -217,6 +227,52 @@ public class AudioStreamsHelperTest {
assertThat(list.get(0)).isEqualTo(source);
}
+ @Test
+ public void getAllPresentSources_noSource() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+
+ List devices = new ArrayList<>();
+ devices.add(mDevice);
+
+ String address = "00:00:00:00:00:00";
+
+ when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
+ BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class);
+ when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID);
+ when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source));
+ when(source.getSourceDevice()).thenReturn(mSourceDevice);
+ when(mSourceDevice.getAddress()).thenReturn(address);
+
+ var list = mHelper.getAllPresentSources();
+ assertThat(list).isEmpty();
+ }
+
+ @Test
+ public void getAllPresentSources_returnSource() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ String address = "11:22:33:44:55:66";
+
+ List devices = new ArrayList<>();
+ devices.add(mDevice);
+
+ when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
+ BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class);
+ when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice);
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID);
+ when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source));
+ when(source.getSourceDevice()).thenReturn(mSourceDevice);
+ when(mSourceDevice.getAddress()).thenReturn(address);
+ List bisSyncState = new ArrayList<>();
+ when(source.getBisSyncState()).thenReturn(bisSyncState);
+
+ var list = mHelper.getAllPresentSources();
+ assertThat(list).isNotEmpty();
+ assertThat(list.get(0)).isEqualTo(source);
+ }
+
@Test
public void startMediaService_noDevice_doNothing() {
mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME);
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java
index 164c2f093e8..1e645282227 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java
@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams;
+import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
@@ -25,6 +27,7 @@ import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.platform.test.flag.junit.SetFlagsRule;
import org.junit.Before;
import org.junit.Rule;
@@ -41,14 +44,18 @@ import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class AudioStreamsProgressCategoryCallbackTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock private AudioStreamsProgressCategoryController mController;
@Mock private BluetoothDevice mDevice;
@Mock private BluetoothLeBroadcastReceiveState mState;
@Mock private BluetoothLeBroadcastMetadata mMetadata;
+ @Mock private BluetoothDevice mSourceDevice;
private AudioStreamsProgressCategoryCallback mCallback;
@Before
public void setUp() {
+ mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
mCallback = new AudioStreamsProgressCategoryCallback(mController);
}
@@ -62,6 +69,20 @@ public class AudioStreamsProgressCategoryCallbackTest {
verify(mController).handleSourceConnected(any());
}
+ @Test
+ public void testOnReceiveStateChanged_sourcePresent() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ String address = "11:22:33:44:55:66";
+
+ List bisSyncState = new ArrayList<>();
+ when(mState.getBisSyncState()).thenReturn(bisSyncState);
+ when(mState.getSourceDevice()).thenReturn(mSourceDevice);
+ when(mSourceDevice.getAddress()).thenReturn(address);
+ mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
+
+ verify(mController).handleSourcePresent(any());
+ }
+
@Test
public void testOnReceiveStateChanged_badCode() {
when(mState.getPaSyncState())
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java
index fd1b649fabf..227748ae232 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java
@@ -20,10 +20,12 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Aud
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SYNCED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat;
@@ -41,12 +43,14 @@ import static org.robolectric.Shadows.shadowOf;
import static java.util.Collections.emptyList;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
@@ -96,6 +100,8 @@ import java.util.List;
})
public class AudioStreamsProgressCategoryControllerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private static final String VALID_METADATA =
"BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;"
+ "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;";
@@ -115,6 +121,7 @@ public class AudioStreamsProgressCategoryControllerTest {
@Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private CachedBluetoothDevice mDevice;
@Mock private AudioStreamsProgressCategoryPreference mPreference;
+ @Mock private BluetoothDevice mSourceDevice;
private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner;
private Fragment mFragment;
@@ -125,6 +132,7 @@ public class AudioStreamsProgressCategoryControllerTest {
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList());
+ mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager);
@@ -282,6 +290,29 @@ public class AudioStreamsProgressCategoryControllerTest {
verify(mController, never()).moveToState(any(), any());
}
+ @Test
+ public void testOnStart_initHasDevice_getPresentSources() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+
+ // Setup a device
+ ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+ List connectedList = new ArrayList<>();
+ // Empty connected device list
+ when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(connectedList);
+
+ mController.onStart(mLifecycleOwner);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ verify(mAudioStreamsHelper).getAllPresentSources();
+ verify(mLeBroadcastAssistant).startSearchingForSources(any());
+
+ var dialog = ShadowAlertDialog.getLatestAlertDialog();
+ assertThat(dialog).isNull();
+
+ verify(mController, never()).moveToState(any(), any());
+ }
+
@Test
public void testOnStart_handleSourceFromQrCode() {
// Setup a device
@@ -764,6 +795,58 @@ public class AudioStreamsProgressCategoryControllerTest {
assertThat(states.get(1)).isEqualTo(ADD_SOURCE_FAILED);
}
+ @Test
+ public void testHandleSourcePresent_updateState() {
+ mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
+ String address = "11:22:33:44:55:66";
+
+ // Setup a device
+ ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+ // Setup mPreference so it's not null
+ mController.displayPreference(mScreen);
+
+ // A new source found
+ when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
+ mController.handleSourceFound(mMetadata);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // The connected source is identified as having a bad code
+ BluetoothLeBroadcastReceiveState receiveState =
+ mock(BluetoothLeBroadcastReceiveState.class);
+ when(receiveState.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
+ when(receiveState.getSourceDevice()).thenReturn(mSourceDevice);
+ when(mSourceDevice.getAddress()).thenReturn(address);
+ List bisSyncState = new ArrayList<>();
+ when(receiveState.getBisSyncState()).thenReturn(bisSyncState);
+
+ // The new found source is identified as failed to connect
+ mController.handleSourcePresent(receiveState);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ ArgumentCaptor preference =
+ ArgumentCaptor.forClass(AudioStreamPreference.class);
+ ArgumentCaptor state =
+ ArgumentCaptor.forClass(
+ AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+ verify(mController, times(2)).moveToState(preference.capture(), state.capture());
+ List preferences = preference.getAllValues();
+ assertThat(preferences.size()).isEqualTo(2);
+ List states = state.getAllValues();
+ assertThat(states.size()).isEqualTo(2);
+
+ // Verify one preference is created with SYNCED
+ assertThat(preferences.get(0).getAudioStreamBroadcastId())
+ .isEqualTo(NEWLY_FOUND_BROADCAST_ID);
+ assertThat(states.get(0)).isEqualTo(SYNCED);
+
+ // Verify the preference is updated to state ADD_SOURCE_FAILED
+ assertThat(preferences.get(1).getAudioStreamBroadcastId())
+ .isEqualTo(NEWLY_FOUND_BROADCAST_ID);
+ assertThat(states.get(1)).isEqualTo(SOURCE_PRESENT);
+ }
+
private static BluetoothLeBroadcastReceiveState createConnectedMock(int id) {
var connected = mock(BluetoothLeBroadcastReceiveState.class);
List bisSyncState = new ArrayList<>();
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentStateTest.java
new file mode 100644
index 00000000000..fd84fefb3e5
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourcePresentStateTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import static android.app.settings.SettingsEnums.AUDIO_STREAM_MAIN;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.SourcePresentState.AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.ShadowFragment;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+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 SourcePresentStateTest {
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+ private static final int BROADCAST_ID = 1;
+ private static final String BROADCAST_TITLE = "title";
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ @Mock private AudioStreamPreference mPreference;
+ @Mock private AudioStreamsProgressCategoryController mController;
+ @Mock private AudioStreamsHelper mHelper;
+ @Mock private AudioStreamsRepository mRepository;
+ @Mock private AudioStreamsDashboardFragment mFragment;
+ @Mock private FragmentActivity mActivity;
+ private FakeFeatureFactory mFeatureFactory;
+ private SourcePresentState mInstance;
+
+ @Before
+ public void setUp() {
+ when(mFragment.getActivity()).thenReturn(mActivity);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ mInstance = new SourcePresentState();
+ when(mPreference.getAudioStreamBroadcastId()).thenReturn(BROADCAST_ID);
+ when(mPreference.getTitle()).thenReturn(BROADCAST_TITLE);
+ }
+
+ @Test
+ public void testGetInstance() {
+ mInstance = SourcePresentState.getInstance();
+ assertThat(mInstance).isNotNull();
+ assertThat(mInstance).isInstanceOf(SourcePresentState.class);
+ }
+
+ @Test
+ public void testGetSummary() {
+ int summary = mInstance.getSummary();
+ assertThat(summary).isEqualTo(AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY);
+ }
+
+ @Test
+ public void testGetStateEnum() {
+ AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
+ mInstance.getStateEnum();
+ assertThat(stateEnum)
+ .isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
+ }
+
+ @Test
+ public void testGetOnClickListener_startSubSettings() {
+ when(mController.getFragment()).thenReturn(mFragment);
+ when(mFragment.getMetricsCategory()).thenReturn(AUDIO_STREAM_MAIN);
+
+ Preference.OnPreferenceClickListener listener = mInstance.getOnClickListener(mController);
+ assertThat(listener).isNotNull();
+
+ // mContext is not an Activity context, calling startActivity() from outside of an Activity
+ // context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this
+ // AndroidRuntimeException.
+ Context activityContext = mock(Context.class);
+ when(mPreference.getContext()).thenReturn(activityContext);
+
+ listener.onPreferenceClick(mPreference);
+
+ ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(activityContext).startActivity(argumentCaptor.capture());
+
+ Intent intent = argumentCaptor.getValue();
+ assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+ .isEqualTo(AudioStreamDetailsFragment.class.getName());
+ assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0))
+ .isEqualTo(R.string.audio_streams_detail_page_title);
+ assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0))
+ .isEqualTo(AUDIO_STREAM_MAIN);
+
+ Bundle bundle = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+ assertThat(bundle).isNotNull();
+ assertThat(bundle.getString(AudioStreamDetailsFragment.BROADCAST_NAME_ARG))
+ .isEqualTo(BROADCAST_TITLE);
+ assertThat(bundle.getInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG))
+ .isEqualTo(BROADCAST_ID);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java
index 051eda7c442..c7d0c60efa8 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java
@@ -59,6 +59,11 @@ public class ShadowAudioStreamsHelper {
return sMockHelper.getAllConnectedSources();
}
+ @Implementation
+ public List getAllPresentSources() {
+ return sMockHelper.getAllPresentSources();
+ }
+
/** Gets {@link CachedBluetoothDevice} in sharing or le connected */
@Implementation
public static Optional getCachedBluetoothDeviceInSharingOrLeConnected(
diff --git a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java b/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java
index 8d6d2d9bc4e..1823d6d6bed 100644
--- a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java
+++ b/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java
@@ -32,7 +32,6 @@ import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
-import android.os.UserManager;
import android.provider.Settings;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
@@ -73,8 +72,6 @@ public class MobileNetworkSummaryControllerTest {
@Mock
private PreferenceScreen mPreferenceScreen;
@Mock
- private UserManager mUserManager;
- @Mock
private MobileNetworkRepository mMobileNetworkRepository;
@Mock
private MobileNetworkRepository.MobileNetworkCallback mMobileNetworkCallback;
@@ -92,7 +89,6 @@ public class MobileNetworkSummaryControllerTest {
doReturn(mTelephonyManager).when(mContext).getSystemService(TelephonyManager.class);
doReturn(mSubscriptionManager).when(mContext).getSystemService(SubscriptionManager.class);
doReturn(mEuiccManager).when(mContext).getSystemService(EuiccManager.class);
- doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
mMobileNetworkRepository = MobileNetworkRepository.getInstance(mContext);
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
@@ -118,21 +114,6 @@ public class MobileNetworkSummaryControllerTest {
SubscriptionUtil.setAvailableSubscriptionsForTesting(null);
}
- @Test
- public void isAvailable_wifiOnlyMode_notAvailable() {
- when(mTelephonyManager.isDataCapable()).thenReturn(false);
- when(mUserManager.isAdminUser()).thenReturn(true);
-
- assertThat(mController.isAvailable()).isFalse();
- }
-
- @Test
- public void isAvailable_secondaryUser_notAvailable() {
- when(mTelephonyManager.isDataCapable()).thenReturn(true);
- when(mUserManager.isAdminUser()).thenReturn(false);
- assertThat(mController.isAvailable()).isFalse();
- }
-
@Test
public void getSummary_noSubscriptions_returnSummaryCorrectly() {
mController.displayPreference(mPreferenceScreen);
diff --git a/tests/robotests/src/com/android/settings/network/TopLevelNetworkEntryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/TopLevelNetworkEntryPreferenceControllerTest.java
deleted file mode 100644
index 8e0c8631e4f..00000000000
--- a/tests/robotests/src/com/android/settings/network/TopLevelNetworkEntryPreferenceControllerTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2018 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 static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.os.UserManager;
-import android.text.BidiFormatter;
-import android.util.FeatureFlagUtils;
-
-import com.android.settings.R;
-import com.android.settings.testutils.shadow.ShadowRestrictedLockUtilsInternal;
-import com.android.settings.testutils.shadow.ShadowUserManager;
-import com.android.settings.testutils.shadow.ShadowUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
-import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {
- ShadowRestrictedLockUtilsInternal.class,
- ShadowUtils.class,
- ShadowUserManager.class,
-})
-public class TopLevelNetworkEntryPreferenceControllerTest {
-
- @Mock
- private MobileNetworkPreferenceController mMobileNetworkPreferenceController;;
-
- private Context mContext;
- private TopLevelNetworkEntryPreferenceController mController;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mContext = RuntimeEnvironment.application;
- final ShadowUserManager um = Shadow.extract(
- RuntimeEnvironment.application.getSystemService(UserManager.class));
- um.setIsAdminUser(true);
-
- mController = new TopLevelNetworkEntryPreferenceController(mContext, "test_key");
-
- ReflectionHelpers.setField(mController, "mMobileNetworkPreferenceController",
- mMobileNetworkPreferenceController);
- }
-
- @After
- public void tearDown() {
- ShadowUtils.reset();
- }
-
- @Test
- public void getAvailabilityStatus_demoUser_nonLargeScreen_unsupported() {
- ShadowUtils.setIsDemoUser(true);
- FeatureFlagUtils.setEnabled(mContext, "settings_support_large_screen", false);
- assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
- }
-
- @Test
- public void getSummary_hasMobile_shouldReturnMobileSummary() {
- when(mMobileNetworkPreferenceController.isAvailable()).thenReturn(true);
-
- assertThat(mController.getSummary()).isEqualTo(BidiFormatter.getInstance().unicodeWrap(
- mContext.getString(R.string.network_dashboard_summary_mobile)));
- }
-
- @Test
- public void getSummary_noMobile_shouldReturnNoMobileSummary() {
- when(mMobileNetworkPreferenceController.isAvailable()).thenReturn(false);
-
- assertThat(mController.getSummary()).isEqualTo(BidiFormatter.getInstance().unicodeWrap(
- mContext.getString(R.string.network_dashboard_summary_no_mobile)));
- }
-}
diff --git a/tests/robotests/src/com/android/settings/notification/zen/ZenModeBehaviorFooterPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/zen/ZenModeBehaviorFooterPreferenceControllerTest.java
index fd795155a4f..2b0d6e7687d 100644
--- a/tests/robotests/src/com/android/settings/notification/zen/ZenModeBehaviorFooterPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/zen/ZenModeBehaviorFooterPreferenceControllerTest.java
@@ -43,7 +43,6 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.notification.zen.AbstractZenModePreferenceController.ZenModeConfigWrapper;
-import com.android.settings.notification.zen.ZenModeBehaviorFooterPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
import org.junit.Before;
@@ -206,7 +205,7 @@ public class ZenModeBehaviorFooterPreferenceControllerTest {
ZenRule injectedRule = spy(new ZenRule());
injectedRule.zenMode = ZEN_MODE_ALARMS;
injectedRule.component = mock(ComponentName.class);
- when(injectedRule.isAutomaticActive()).thenReturn(true);
+ when(injectedRule.isActive()).thenReturn(true);
when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME);
injectedAutomaticRules.put("testid", injectedRule);
@@ -226,7 +225,7 @@ public class ZenModeBehaviorFooterPreferenceControllerTest {
ZenRule injectedRule = spy(new ZenRule());
injectedRule.zenMode = ZEN_MODE_NO_INTERRUPTIONS;
injectedRule.component = mock(ComponentName.class);
- when(injectedRule.isAutomaticActive()).thenReturn(true);
+ when(injectedRule.isActive()).thenReturn(true);
when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME);
injectedAutomaticRules.put("testid", injectedRule);
diff --git a/tests/robotests/src/com/android/settings/notification/zen/ZenModeSettingsFooterPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/zen/ZenModeSettingsFooterPreferenceControllerTest.java
index efa2f558b5f..e5c2d426cae 100644
--- a/tests/robotests/src/com/android/settings/notification/zen/ZenModeSettingsFooterPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/zen/ZenModeSettingsFooterPreferenceControllerTest.java
@@ -44,7 +44,6 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.notification.zen.AbstractZenModePreferenceController.ZenModeConfigWrapper;
-import com.android.settings.notification.zen.ZenModeSettingsFooterPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
import org.junit.Before;
@@ -289,7 +288,7 @@ public class ZenModeSettingsFooterPreferenceControllerTest {
injectedRule.component = mock(ComponentName.class);
injectedRule.name = nameAndId;
injectedRule.conditionId = new Uri.Builder().authority(nameAndId).build(); // unique uri
- when(injectedRule.isAutomaticActive()).thenReturn(isActive);
+ when(injectedRule.isActive()).thenReturn(isActive);
when(mConfigWrapper.isTimeRule(injectedRule.conditionId)).thenReturn(!isApp);
if (isApp) {
when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME);
diff --git a/tests/robotests/src/com/android/settings/security/OwnerInfoPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/security/OwnerInfoPreferenceControllerTest.java
index 81f4fce1517..0db950be6de 100644
--- a/tests/robotests/src/com/android/settings/security/OwnerInfoPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/security/OwnerInfoPreferenceControllerTest.java
@@ -36,11 +36,11 @@ import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.core.ObservablePreferenceFragment;
import com.android.settings.users.OwnerInfoSettings;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.core.lifecycle.Lifecycle;
-import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment;
import org.junit.Before;
import org.junit.Test;
diff --git a/tests/spa_unit/src/com/android/settings/network/MobileNetworkListFragmentTest.kt b/tests/spa_unit/src/com/android/settings/network/MobileNetworkListFragmentTest.kt
index 3ba4bac39ce..4bb5f2f803f 100644
--- a/tests/spa_unit/src/com/android/settings/network/MobileNetworkListFragmentTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/MobileNetworkListFragmentTest.kt
@@ -17,57 +17,37 @@
package com.android.settings.network
import android.content.Context
-import android.content.res.Resources
-import android.os.UserManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settings.R
-import com.android.settingslib.spaprivileged.framework.common.userManager
+import com.android.settings.network.MobileNetworkListFragment.Companion.SearchIndexProvider
+import com.android.settings.network.telephony.SimRepository
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
-import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class)
class MobileNetworkListFragmentTest {
- private val mockUserManager = mock()
+ private val mockSimRepository = mock()
- private val mockResources = mock()
-
- private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
- on { userManager } doReturn mockUserManager
- on { resources } doReturn mockResources
- }
+ private val context: Context = ApplicationProvider.getApplicationContext()
@Test
- fun isPageSearchEnabled_adminUser_shouldReturnTrue() {
- mockUserManager.stub {
- on { isAdminUser } doReturn true
- }
- mockResources.stub {
- on { getBoolean(R.bool.config_show_sim_info) } doReturn true
- }
+ fun isPageSearchEnabled_showMobileNetworkPage_returnTrue() {
+ mockSimRepository.stub { on { showMobileNetworkPage() } doReturn true }
- val isEnabled =
- MobileNetworkListFragment.SEARCH_INDEX_DATA_PROVIDER.isPageSearchEnabled(context)
+ val isEnabled = SearchIndexProvider { mockSimRepository }.isPageSearchEnabled(context)
assertThat(isEnabled).isTrue()
}
@Test
- fun isPageSearchEnabled_nonAdminUser_shouldReturnFalse() {
- mockUserManager.stub {
- on { isAdminUser } doReturn false
- }
- mockResources.stub {
- on { getBoolean(R.bool.config_show_sim_info) } doReturn true
- }
+ fun isPageSearchEnabled_hideMobileNetworkPage_returnFalse() {
+ mockSimRepository.stub { on { showMobileNetworkPage() } doReturn false }
- val isEnabled =
- MobileNetworkListFragment.SEARCH_INDEX_DATA_PROVIDER.isPageSearchEnabled(context)
+ val isEnabled = SearchIndexProvider { mockSimRepository }.isPageSearchEnabled(context)
assertThat(isEnabled).isFalse()
}
diff --git a/tests/spa_unit/src/com/android/settings/network/TopLevelNetworkEntryPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/TopLevelNetworkEntryPreferenceControllerTest.kt
new file mode 100644
index 00000000000..27c960282dc
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/TopLevelNetworkEntryPreferenceControllerTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.text.BidiFormatter
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settings.core.BasePreferenceController
+import com.android.settings.network.telephony.SimRepository
+import com.google.common.truth.Truth.assertThat
+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 TopLevelNetworkEntryPreferenceControllerTest {
+
+ private val mockSimRepository = mock()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private var isDemoUser = false
+ private var isEmbeddingActivityEnabled = false
+
+ private var controller =
+ TopLevelNetworkEntryPreferenceController(
+ context = context,
+ preferenceKey = TEST_KEY,
+ simRepository = mockSimRepository,
+ isDemoUser = { isDemoUser },
+ isEmbeddingActivityEnabled = { isEmbeddingActivityEnabled },
+ )
+
+ @Test
+ fun getAvailabilityStatus_demoUser_largeScreen_unsupported() {
+ isDemoUser = true
+ isEmbeddingActivityEnabled = true
+
+ val availabilityStatus = controller.availabilityStatus
+
+ assertThat(availabilityStatus).isEqualTo(BasePreferenceController.AVAILABLE)
+ }
+
+ @Test
+ fun getAvailabilityStatus_demoUser_nonLargeScreen_unsupported() {
+ isDemoUser = true
+ isEmbeddingActivityEnabled = false
+
+ val availabilityStatus = controller.availabilityStatus
+
+ assertThat(availabilityStatus).isEqualTo(BasePreferenceController.UNSUPPORTED_ON_DEVICE)
+ }
+
+ @Test
+ fun getSummary_hasMobile_shouldReturnMobileSummary() {
+ mockSimRepository.stub { on { showMobileNetworkPage() } doReturn true }
+
+ val summary = controller.summary
+
+ assertThat(summary)
+ .isEqualTo(
+ BidiFormatter.getInstance()
+ .unicodeWrap(context.getString(R.string.network_dashboard_summary_mobile))
+ )
+ }
+
+ @Test
+ fun getSummary_noMobile_shouldReturnNoMobileSummary() {
+ mockSimRepository.stub { on { showMobileNetworkPage() } doReturn false }
+
+ val summary = controller.summary
+
+ assertThat(summary)
+ .isEqualTo(
+ BidiFormatter.getInstance()
+ .unicodeWrap(context.getString(R.string.network_dashboard_summary_no_mobile))
+ )
+ }
+
+ private companion object {
+ const val TEST_KEY = "test_key"
+ }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SimRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SimRepositoryTest.kt
new file mode 100644
index 00000000000..bbcac086a10
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/SimRepositoryTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.telephony
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.UserManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class SimRepositoryTest {
+
+ private val mockUserManager = mock()
+
+ private val mockPackageManager = mock()
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { userManager } doReturn mockUserManager
+ on { packageManager } doReturn mockPackageManager
+ }
+
+ private val repository = SimRepository(context)
+
+ @Test
+ fun showMobileNetworkPage_adminUserAndHasTelephony_returnTrue() {
+ mockUserManager.stub {
+ on { isAdminUser } doReturn true
+ }
+ mockPackageManager.stub {
+ on { hasSystemFeature(PackageManager.FEATURE_TELEPHONY) } doReturn true
+ }
+
+ val showMobileNetworkPage = repository.showMobileNetworkPage()
+
+ assertThat(showMobileNetworkPage).isTrue()
+ }
+
+ @Test
+ fun showMobileNetworkPage_notAdminUser_returnFalse() {
+ mockUserManager.stub {
+ on { isAdminUser } doReturn false
+ }
+ mockPackageManager.stub {
+ on { hasSystemFeature(PackageManager.FEATURE_TELEPHONY) } doReturn true
+ }
+
+ val showMobileNetworkPage = repository.showMobileNetworkPage()
+
+ assertThat(showMobileNetworkPage).isFalse()
+ }
+
+ @Test fun showMobileNetworkPage_noTelephony_returnFalse() {
+ mockUserManager.stub {
+ on { isAdminUser } doReturn true
+ }
+ mockPackageManager.stub {
+ on { hasSystemFeature(PackageManager.FEATURE_TELEPHONY) } doReturn false
+ }
+
+ val showMobileNetworkPage = repository.showMobileNetworkPage()
+
+ assertThat(showMobileNetworkPage).isFalse()
+ }
+}
diff --git a/tests/unit/src/com/android/settings/network/MobileNetworkPreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/MobileNetworkPreferenceControllerTest.java
deleted file mode 100644
index 1231c01b749..00000000000
--- a/tests/unit/src/com/android/settings/network/MobileNetworkPreferenceControllerTest.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2020 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 static androidx.lifecycle.Lifecycle.Event;
-
-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 static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.os.Looper;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.provider.Settings.Global;
-import android.telephony.PhoneStateListener;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LifecycleRegistry;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceManager;
-import androidx.preference.PreferenceScreen;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
-import com.android.settingslib.RestrictedPreference;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class MobileNetworkPreferenceControllerTest {
- private Context mContext;
- @Mock
- private TelephonyManager mTelephonyManager;
- @Mock
- private SubscriptionManager mSubscriptionManager;
-
- @Mock
- private UserManager mUserManager;
-
- private PreferenceManager mPreferenceManager;
- private PreferenceScreen mScreen;
-
- @Mock
- private LifecycleOwner mLifecycleOwner;
- private LifecycleRegistry mLifecycleRegistry;
- private MobileNetworkPreferenceController mController;
- private Preference mPreference;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mContext = spy(ApplicationProvider.getApplicationContext());
- when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
- when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager);
- when(mSubscriptionManager.createForAllUserProfiles()).thenReturn(mSubscriptionManager);
- when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
- mPreferenceManager = new PreferenceManager(mContext);
- mScreen = mPreferenceManager.createPreferenceScreen(mContext);
- mPreference = new Preference(mContext);
- mPreference.setKey(MobileNetworkPreferenceController.KEY_MOBILE_NETWORK_SETTINGS);
-
- mLifecycleRegistry = new LifecycleRegistry(mLifecycleOwner);
- when(mLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
- }
-
- @Test
- public void secondaryUser_prefIsNotAvailable() {
- when(mUserManager.isAdminUser()).thenReturn(false);
- when(mTelephonyManager.isDataCapable()).thenReturn(true);
-
- mController = new MobileNetworkPreferenceController(mContext);
- assertThat(mController.isAvailable()).isFalse();
- }
-
- @Test
- public void wifiOnly_prefIsNotAvailable() {
- when(mUserManager.isAdminUser()).thenReturn(true);
- when(mTelephonyManager.isDataCapable()).thenReturn(false);
-
- mController = new MobileNetworkPreferenceController(mContext);
- assertThat(mController.isAvailable()).isFalse();
- }
-
- @Test
- @UiThreadTest
- public void goThroughLifecycle_isAvailable_shouldListenToServiceChange() {
- mController = spy(new MobileNetworkPreferenceController(mContext));
- mLifecycleRegistry.addObserver(mController);
- doReturn(true).when(mController).isAvailable();
-
- mLifecycleRegistry.handleLifecycleEvent(Event.ON_START);
- verify(mController).onStart();
- verify(mTelephonyManager).registerTelephonyCallback(
- mContext.getMainExecutor(), mController.mTelephonyCallback);
-
- mLifecycleRegistry.handleLifecycleEvent(Event.ON_STOP);
- verify(mController).onStop();
- verify(mTelephonyManager).unregisterTelephonyCallback(mController.mTelephonyCallback);
- }
-
- @Test
- @UiThreadTest
- public void serviceStateChange_shouldUpdatePrefSummary() {
- final String testCarrierName = "test";
-
- mController = spy(new MobileNetworkPreferenceController(mContext));
- mLifecycleRegistry.addObserver(mController);
- doReturn(true).when(mController).isAvailable();
-
- mScreen.addPreference(mPreference);
-
- // Display pref and go through lifecycle to set up listener.
- mController.displayPreference(mScreen);
- mLifecycleRegistry.handleLifecycleEvent(Event.ON_START);
- verify(mController).onStart();
- verify(mTelephonyManager).registerTelephonyCallback(
- mContext.getMainExecutor(), mController.mTelephonyCallback);
-
- doReturn(testCarrierName).when(mController).getSummary();
-
- mController.mTelephonyCallback.onServiceStateChanged(null);
-
- // Carrier name should be set.
- Assert.assertEquals(mPreference.getSummary(), testCarrierName);
- }
-
- @Test
- public void airplaneModeTurnedOn_shouldDisablePreference() {
- Settings.Global.putInt(mContext.getContentResolver(),
- Global.AIRPLANE_MODE_ON, 1);
- mController = spy(new MobileNetworkPreferenceController(mContext));
- final RestrictedPreference mPreference = new RestrictedPreference(mContext);
- mController.updateState(mPreference);
- assertThat(mPreference.isEnabled()).isFalse();
- }
-
- @Test
- public void airplaneModeTurnedOffAndNoUserRestriction_shouldEnablePreference() {
- Settings.Global.putInt(mContext.getContentResolver(),
- Global.AIRPLANE_MODE_ON, 0);
- mController = spy(new MobileNetworkPreferenceController(mContext));
- final RestrictedPreference mPreference = new RestrictedPreference(mContext);
- mPreference.setDisabledByAdmin(null);
- mController.updateState(mPreference);
- assertThat(mPreference.isEnabled()).isTrue();
- }
-
- @Test
- public void airplaneModeTurnedOffAndHasUserRestriction_shouldDisablePreference() {
- Settings.Global.putInt(mContext.getContentResolver(),
- Global.AIRPLANE_MODE_ON, 0);
- mController = spy(new MobileNetworkPreferenceController(mContext));
- final RestrictedPreference mPreference = new RestrictedPreference(mContext);
- mPreference.setDisabledByAdmin(EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN);
- mController.updateState(mPreference);
- assertThat(mPreference.isEnabled()).isFalse();
- }
-}