diff --git a/aconfig/catalyst/sound_screen.aconfig b/aconfig/catalyst/sound_screen.aconfig index d5caddfa8b0..4ba339625c5 100644 --- a/aconfig/catalyst/sound_screen.aconfig +++ b/aconfig/catalyst/sound_screen.aconfig @@ -8,6 +8,13 @@ flag { bug: "323791114" } +flag { + name: "catalyst_media_controls" + namespace: "android_settings" + description: "Flag for media page" + bug: "337243570" +} + flag { name: "catalyst_vibration_intensity_screen" namespace: "android_settings" diff --git a/res/xml/media_controls_settings.xml b/res/xml/media_controls_settings.xml index ad5ceb24cd9..897eaa3a08b 100644 --- a/res/xml/media_controls_settings.xml +++ b/res/xml/media_controls_settings.xml @@ -21,14 +21,14 @@ android:title="@string/media_controls_title"> diff --git a/res/xml/sound_settings.xml b/res/xml/sound_settings.xml index eed44067619..cab3b2029a9 100644 --- a/res/xml/sound_settings.xml +++ b/res/xml/sound_settings.xml @@ -126,7 +126,7 @@ settings:controller="com.android.settings.notification.SpatialAudioParentPreferenceController"/> (settingsStore), KeyValueStore { + override fun contains(key: String) = settingsStore.contains(key) + + override fun getDefaultValue(key: String, valueType: Class) = true as T + + override fun getValue(key: String, valueType: Class) = + settingsStore.getValue(key, valueType) ?: getDefaultValue(key, valueType) + + override fun setValue(key: String, valueType: Class, value: T?) = + settingsStore.setValue(key, valueType, value) + } + + companion object { + const val KEY = MEDIA_CONTROLS_LOCK_SCREEN + } +} +// LINT.ThenChange(MediaControlsLockScreenPreferenceController.java) \ No newline at end of file diff --git a/src/com/android/settings/sound/MediaControlsPreferenceController.java b/src/com/android/settings/sound/MediaControlsPreferenceController.java index e180b34fe34..51a06ae3beb 100644 --- a/src/com/android/settings/sound/MediaControlsPreferenceController.java +++ b/src/com/android/settings/sound/MediaControlsPreferenceController.java @@ -29,6 +29,7 @@ import com.android.settings.core.TogglePreferenceController; /** * Toggle for media controls resumption setting */ +// LINT.IfChange public class MediaControlsPreferenceController extends TogglePreferenceController { public MediaControlsPreferenceController(Context context, String key) { @@ -56,4 +57,6 @@ public class MediaControlsPreferenceController extends TogglePreferenceControlle public int getSliceHighlightMenuRes() { return R.string.menu_key_sound; } + } +// LINT.ThenChange(MediaControlsSwitchPreference.kt) \ No newline at end of file diff --git a/src/com/android/settings/sound/MediaControlsScreen.kt b/src/com/android/settings/sound/MediaControlsScreen.kt new file mode 100644 index 00000000000..d63259c641c --- /dev/null +++ b/src/com/android/settings/sound/MediaControlsScreen.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 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.content.Context + +import com.android.settings.R +import com.android.settings.flags.Flags +import com.android.settingslib.datastore.AbstractKeyedDataObservable +import com.android.settingslib.datastore.HandlerExecutor +import com.android.settingslib.datastore.KeyedObserver +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.KeyedObservableDelegate +import com.android.settingslib.datastore.SettingsSecureStore +import com.android.settingslib.datastore.SettingsStore +import com.android.settingslib.metadata.PreferenceChangeReason +import com.android.settingslib.metadata.ProvidePreferenceScreen +import com.android.settingslib.metadata.PreferenceSummaryProvider +import com.android.settingslib.metadata.preferenceHierarchy + +import com.android.settingslib.preference.PreferenceScreenCreator + +// LINT.IfChange +@ProvidePreferenceScreen(MediaControlsScreen.KEY) +class MediaControlsScreen(context: Context) : + AbstractKeyedDataObservable(), PreferenceScreenCreator, PreferenceSummaryProvider { + + private val observer = + KeyedObserver { _, _ -> notifyChange(KEY, PreferenceChangeReason.STATE) } + + private val mediaControlsStore = MediaControlsStore(SettingsSecureStore.get(context)) + + override val key: String + get() = KEY + + override val title: Int + get() = R.string.media_controls_title + + override val keywords: Int + get() = R.string.keywords_media_controls + + override fun onFirstObserverAdded() { + mediaControlsStore.addObserver( + MediaControlsSwitchPreference.KEY, + observer, + HandlerExecutor.main, + ) + } + + override fun onLastObserverRemoved() { + mediaControlsStore.removeObserver(MediaControlsSwitchPreference.KEY, observer) + } + + override fun isFlagEnabled(context: Context) = Flags.catalystMediaControls() + + override fun fragmentClass() = MediaControlsSettings::class.java + + override fun getPreferenceHierarchy(context: Context) = + preferenceHierarchy(context, this) { + +MediaControlsSwitchPreference(mediaControlsStore) + +MediaControlsLockscreenSwitchPreference() + } + + override fun getSummary(context: Context): CharSequence? = + if (mediaControlsStore.getBoolean(MediaControlsSwitchPreference.KEY) == false) { + context.getString(R.string.media_controls_hide_player) + } else { + context.getString(R.string.media_controls_show_player) + } + + @Suppress("UNCHECKED_CAST") + class MediaControlsStore(private val settingsStore: SettingsStore) : + KeyedObservableDelegate(settingsStore), KeyValueStore { + override fun contains(key: String) = settingsStore.contains(key) + + override fun getDefaultValue(key: String, valueType: Class) = true as T + + override fun getValue(key: String, valueType: Class) = + settingsStore.getValue(key, valueType) ?: getDefaultValue(key, valueType) + + override fun setValue(key: String, valueType: Class, value: T?) = + settingsStore.setValue(key, valueType, value) + } + + companion object { + const val KEY = "media_controls" + } +} +// LINT.ThenChange(MediaControlsSettings.java) \ No newline at end of file diff --git a/src/com/android/settings/sound/MediaControlsSettings.java b/src/com/android/settings/sound/MediaControlsSettings.java index f914b5d6241..27fb8a64666 100644 --- a/src/com/android/settings/sound/MediaControlsSettings.java +++ b/src/com/android/settings/sound/MediaControlsSettings.java @@ -17,6 +17,10 @@ package com.android.settings.sound; import android.app.settings.SettingsEnums; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; @@ -26,6 +30,7 @@ import com.android.settingslib.search.SearchIndexable; /** * Media control settings located in the sound menu */ +// LINT.IfChange @SearchIndexable public class MediaControlsSettings extends DashboardFragment { @@ -48,4 +53,10 @@ public class MediaControlsSettings extends DashboardFragment { public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider(R.xml.media_controls_settings); + + @Override + public @Nullable String getPreferenceScreenBindingKey(@NonNull Context context) { + return MediaControlsScreen.KEY; + } } +// LINT.ThenChange(MediaControlsScreen.kt) diff --git a/src/com/android/settings/sound/MediaControlsSwitchPreference.kt b/src/com/android/settings/sound/MediaControlsSwitchPreference.kt new file mode 100644 index 00000000000..60df2658b7f --- /dev/null +++ b/src/com/android/settings/sound/MediaControlsSwitchPreference.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2025 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.content.Context +import android.provider.Settings.Secure.MEDIA_CONTROLS_RESUME + +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.SettingsSecureStore +import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel +import com.android.settingslib.metadata.SwitchPreference +import com.android.settings.R + +// LINT.IfChange +class MediaControlsSwitchPreference( + private val mediaControlsStore: MediaControlsScreen.MediaControlsStore, +) : SwitchPreference( + KEY, + R.string.media_controls_resume_title, + R.string.media_controls_resume_description, +) { + override val sensitivityLevel + get() = SensitivityLevel.NO_SENSITIVITY + + override val keywords: Int + get() = R.string.keywords_media_controls + + override fun getReadPermissions(context: Context) = SettingsSecureStore.getReadPermissions() + + override fun getWritePermissions(context: Context) = SettingsSecureStore.getWritePermissions() + + override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = + ReadWritePermit.ALLOW + + override fun getWritePermit(context: Context, callingPid: Int, callingUid: Int) = + ReadWritePermit.ALLOW + + override fun storage(context: Context): KeyValueStore = mediaControlsStore + + companion object { + const val KEY = MEDIA_CONTROLS_RESUME + } +} +// LINT.ThenChange(MediaControlsPreferenceController.java) \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/sound/MediaControlsParentPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/sound/MediaControlsParentPreferenceControllerTest.java index d9edee20255..e01123604ec 100644 --- a/tests/robotests/src/com/android/settings/sound/MediaControlsParentPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/sound/MediaControlsParentPreferenceControllerTest.java @@ -38,7 +38,7 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class MediaControlsParentPreferenceControllerTest { - private static final String KEY = "media_controls_summary"; + private static final String KEY = "media_controls"; private Context mContext; private int mOriginalQs; diff --git a/tests/unit/src/com/android/settings/sound/MediaControlsLockScreenPreferenceControllerTest.java b/tests/unit/src/com/android/settings/sound/MediaControlsLockScreenPreferenceControllerTest.java index 6c1aede4b59..75980fc179e 100644 --- a/tests/unit/src/com/android/settings/sound/MediaControlsLockScreenPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/sound/MediaControlsLockScreenPreferenceControllerTest.java @@ -32,6 +32,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +// LINT.IfChange @RunWith(AndroidJUnit4.class) public class MediaControlsLockScreenPreferenceControllerTest { @@ -87,3 +88,4 @@ public class MediaControlsLockScreenPreferenceControllerTest { Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, -1)).isEqualTo(1); } } +// LINT.ThenChange(MediaControlsLockScreenSwitchPreferenceTest.kt) \ No newline at end of file diff --git a/tests/unit/src/com/android/settings/sound/MediaControlsLockScreenSwitchPreferenceTest.kt b/tests/unit/src/com/android/settings/sound/MediaControlsLockScreenSwitchPreferenceTest.kt new file mode 100644 index 00000000000..57e0ba4e064 --- /dev/null +++ b/tests/unit/src/com/android/settings/sound/MediaControlsLockScreenSwitchPreferenceTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2025 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.content.Context + +import androidx.preference.SwitchPreferenceCompat +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import com.android.settingslib.preference.createAndBindWidget +import com.google.common.truth.Truth.assertThat + +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +// LINT.IfChange +@RunWith(AndroidJUnit4::class) +class MediaControlsLockScreenSwitchPreferenceTest { + private val appContext: Context = ApplicationProvider.getApplicationContext() + private var originalValue: Boolean? = null + private val key = MediaControlsLockscreenSwitchPreference.KEY + + private val preference = MediaControlsLockscreenSwitchPreference() + + @Before + fun setUp() { + originalValue = preference.storage(appContext).getBoolean(key) + } + + @After + fun tearDown() { + preference.storage(appContext).setBoolean(key, originalValue) + } + + @Test + fun mediaControlsLockScreenDefaultValue_isChecked() { + preference.storage(appContext).setBoolean(key, null) + + val switchPreference = getSwitchPreferenceCompat() + + assertThat(switchPreference.isChecked).isTrue() + } + + @Test + fun mediaControlsLockScreenEnabled_switchPreferenceIsChecked() { + setMediaControlsLockScreenEnabled(true) + + val switchPreference = getSwitchPreferenceCompat() + + assertThat(switchPreference.isChecked).isTrue() + } + + @Test + fun mediaControlsLockScreenDisabled_switchPreferenceIsNotChecked() { + setMediaControlsLockScreenEnabled(false) + + val switchPreference = getSwitchPreferenceCompat() + + assertThat(switchPreference.isChecked).isFalse() + } + + @Test + fun click_defaultMediaControlsLockScreenEnabled_turnOff() { + setMediaControlsLockScreenEnabled(true) + + val switchPreference = getSwitchPreferenceCompat().apply { performClick() } + + assertThat(switchPreference.isChecked).isFalse() + } + + @Test + fun click_defaultMediaControlsLockScreenDisabled_turnOn() { + setMediaControlsLockScreenEnabled(false) + + val switchPreference = getSwitchPreferenceCompat().apply { performClick() } + + assertThat(switchPreference.isChecked).isTrue() + } + + private fun getSwitchPreferenceCompat(): SwitchPreferenceCompat = + preference.createAndBindWidget(appContext) + + + private fun setMediaControlsLockScreenEnabled(value: Boolean) = + preference.storage(appContext).setBoolean(key, value) +} +// LINT.ThenChange(MediaControlsLockScreenPreferenceControllerTest.java) \ No newline at end of file diff --git a/tests/unit/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java b/tests/unit/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java index 57307320260..fba6350852e 100644 --- a/tests/unit/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/sound/MediaControlsPreferenceControllerTest.java @@ -34,6 +34,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +// LINT.IfChange @RunWith(AndroidJUnit4.class) public class MediaControlsPreferenceControllerTest { @@ -88,3 +89,4 @@ public class MediaControlsPreferenceControllerTest { Settings.Secure.MEDIA_CONTROLS_RESUME, -1)).isEqualTo(1); } } +// LINT.ThenChange(MediaControlsSwitchPreferenceTest.kt) diff --git a/tests/unit/src/com/android/settings/sound/MediaControlsSwitchPreferenceTest.kt b/tests/unit/src/com/android/settings/sound/MediaControlsSwitchPreferenceTest.kt new file mode 100644 index 00000000000..0a5c5f8713c --- /dev/null +++ b/tests/unit/src/com/android/settings/sound/MediaControlsSwitchPreferenceTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2025 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.content.Context + +import androidx.preference.SwitchPreferenceCompat +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import com.android.settingslib.datastore.SettingsSecureStore +import com.android.settingslib.preference.createAndBindWidget +import com.google.common.truth.Truth.assertThat + +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +// LINT.IfChange +@RunWith(AndroidJUnit4::class) +class MediaControlsSwitchPreferenceTest { + private val appContext: Context = ApplicationProvider.getApplicationContext() + private var originalValue: Boolean? = null + private val store = MediaControlsScreen.MediaControlsStore(SettingsSecureStore.get(appContext)) + private val key = MediaControlsSwitchPreference.KEY + + private val preference = MediaControlsSwitchPreference(store) + + @Before + fun setUp() { + originalValue = preference.storage(appContext).getBoolean(key) + } + + @After + fun tearDown() { + preference.storage(appContext).setBoolean(key, originalValue) + } + + @Test + fun mediaControlsLockScreenDefaultValue_isChecked() { + preference.storage(appContext).setBoolean(key, null) + + val switchPreference = getSwitchPreferenceCompat() + + assertThat(switchPreference.isChecked).isTrue() + } + + @Test + fun mediaControlsLockScreenEnabled_switchPreferenceIsChecked() { + setMediaControlsResumeEnabled(true) + + val switchPreference = getSwitchPreferenceCompat() + + assertThat(switchPreference.isChecked).isTrue() + } + + @Test + fun mediaControlsLockScreenDisabled_switchPreferenceIsNotChecked() { + setMediaControlsResumeEnabled(false) + + val switchPreference = getSwitchPreferenceCompat() + + assertThat(switchPreference.isChecked).isFalse() + } + + @Test + fun click_defaultMediaControlsLockScreenEnabled_turnOff() { + setMediaControlsResumeEnabled(true) + + val switchPreference = getSwitchPreferenceCompat().apply { performClick() } + + assertThat(switchPreference.isChecked).isFalse() + } + + @Test + fun click_defaultMediaControlsLockScreenDisabled_turnOn() { + setMediaControlsResumeEnabled(false) + + val switchPreference = getSwitchPreferenceCompat().apply { performClick() } + + assertThat(switchPreference.isChecked).isTrue() + } + + private fun getSwitchPreferenceCompat(): SwitchPreferenceCompat = + preference.createAndBindWidget(appContext) + + + private fun setMediaControlsResumeEnabled(value: Boolean) = + preference.storage(appContext).setBoolean(key, value) +} +// LINT.ThenChange(MediaControlsPreferenceControllerTest.java) \ No newline at end of file