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(); - } -}