Merge changes from topic "catalyst" into main

* changes:
  [Catalyst] Use hybrid mode for sound screen
  [Catalyst] Use hybrid mode for display screen
  [Catalyst] Support hybrid mode
diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java
index 4d53772..92e99cf 100644
--- a/src/com/android/settings/dashboard/DashboardFragment.java
+++ b/src/com/android/settings/dashboard/DashboardFragment.java
@@ -53,6 +53,7 @@
 import com.android.settingslib.core.lifecycle.Lifecycle;
 import com.android.settingslib.drawer.DashboardCategory;
 import com.android.settingslib.drawer.Tile;
+import com.android.settingslib.preference.PreferenceScreenCreator;
 import com.android.settingslib.search.Indexable;
 
 import java.util.ArrayList;
@@ -60,6 +61,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -98,7 +100,8 @@
         mDashboardFeatureProvider =
                 FeatureFactory.getFeatureFactory().getDashboardFeatureProvider();
 
-        if (!isCatalystEnabled()) {
+        PreferenceScreenCreator preferenceScreenCreator = getPreferenceScreenCreator();
+        if (preferenceScreenCreator == null || !preferenceScreenCreator.hasCompleteHierarchy()) {
             // Load preference controllers from code
             final List<AbstractPreferenceController> controllersFromCode =
                     createPreferenceControllers(context);
@@ -383,8 +386,12 @@
             return;
         }
         PreferenceScreen screen;
-        if (isCatalystEnabled()) {
+        PreferenceScreenCreator preferenceScreenCreator = getPreferenceScreenCreator();
+        if (preferenceScreenCreator != null) {
             screen = createPreferenceScreen();
+            if (!preferenceScreenCreator.hasCompleteHierarchy()) {
+                removeControllersForHybridMode();
+            }
             setPreferenceScreen(screen);
             requireActivity().setTitle(screen.getTitle());
         } else {
@@ -395,13 +402,42 @@
         displayResourceTilesToScreen(screen);
     }
 
+    /**
+     * Removes preference controllers that have been migrated to catalyst.
+     *
+     * In hybrid mode, preference screen is inflated from XML resource, while preference metadata
+     * in the preference hierarchy are used to update preference widget UI. To avoid conflict,
+     * remove the preference controllers.
+     */
+    private void removeControllersForHybridMode() {
+        Set<String> keys = getPreferenceKeysInHierarchy();
+        Iterator<AbstractPreferenceController> iterator = mControllers.iterator();
+        while (iterator.hasNext()) {
+            AbstractPreferenceController controller = iterator.next();
+            String key = controller.getPreferenceKey();
+            if (keys.contains(key)) {
+                Log.i(TAG, "Remove preference controller for " + key);
+                iterator.remove();
+                List<AbstractPreferenceController> controllers = mPreferenceControllers.get(
+                        controller.getClass());
+                if (controllers != null) {
+                    controllers.remove(controller);
+                }
+            }
+        }
+    }
+
     /** Returns if catalyst is enabled on current screen. */
     protected final boolean isCatalystEnabled() {
+        return getPreferenceScreenCreator() != null;
+    }
+
+    private @Nullable PreferenceScreenCreator getPreferenceScreenCreator() {
         if (!Flags.catalyst()) {
-            return false;
+            return null;
         }
         Context context = getContext();
-        return context != null ? getPreferenceScreenCreator(context) != null : false;
+        return context != null ? getPreferenceScreenCreator(context) : null;
     }
 
     /**
diff --git a/src/com/android/settings/display/DisplayScreen.kt b/src/com/android/settings/display/DisplayScreen.kt
index fceb18a..9886e4a 100644
--- a/src/com/android/settings/display/DisplayScreen.kt
+++ b/src/com/android/settings/display/DisplayScreen.kt
@@ -34,6 +34,8 @@
 
     override fun isFlagEnabled(context: Context) = Flags.catalystDisplaySettingsScreen()
 
+    override fun hasCompleteHierarchy() = false
+
     override fun fragmentClass() = DisplaySettings::class.java
 
     override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) {}
diff --git a/src/com/android/settings/notification/SoundScreen.kt b/src/com/android/settings/notification/SoundScreen.kt
index ecb0c85..f1f2749 100644
--- a/src/com/android/settings/notification/SoundScreen.kt
+++ b/src/com/android/settings/notification/SoundScreen.kt
@@ -36,12 +36,13 @@
 
     override fun isFlagEnabled(context: Context): Boolean = Flags.catalystSoundScreen()
 
+    override fun hasCompleteHierarchy() = false
+
     override fun fragmentClass(): Class<out Fragment>? = SoundSettings::class.java
 
-    override fun getPreferenceHierarchy(context: Context) =
-        preferenceHierarchy(this) {}
+    override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) {}
 
     companion object {
         const val KEY = "sound_screen"
     }
-}
\ No newline at end of file
+}
diff --git a/tests/robotests/src/com/android/settings/display/DisplayScreenTest.kt b/tests/robotests/src/com/android/settings/display/DisplayScreenTest.kt
index 50c706f..d869b84 100644
--- a/tests/robotests/src/com/android/settings/display/DisplayScreenTest.kt
+++ b/tests/robotests/src/com/android/settings/display/DisplayScreenTest.kt
@@ -17,24 +17,32 @@
 
 import android.content.ContextWrapper
 import android.content.res.Resources
-import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.internal.widget.LockPatternUtils
+import com.android.settings.flags.Flags
+import com.android.settings.testutils.FakeFeatureFactory
+import com.android.settingslib.preference.CatalystScreenTestCase
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.stub
 
 @RunWith(AndroidJUnit4::class)
-class DisplayScreenTest {
-    val preferenceScreenCreator = DisplayScreen()
+class DisplayScreenTest : CatalystScreenTestCase() {
+
+    override val preferenceScreenCreator = DisplayScreen()
+
+    override val flagName: String
+        get() = Flags.FLAG_CATALYST_DISPLAY_SETTINGS_SCREEN
 
     private val mockResources = mock<Resources>()
 
-    private val context =
-        object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
+    private val contextWrapper =
+        object : ContextWrapper(context) {
             override fun getResources(): Resources = mockResources
         }
 
@@ -47,13 +55,25 @@
     fun isAvailable_configTrue_shouldReturnTrue() {
         mockResources.stub { on { getBoolean(anyInt()) } doReturn true }
 
-        assertThat(preferenceScreenCreator.isAvailable(context)).isTrue()
+        assertThat(preferenceScreenCreator.isAvailable(contextWrapper)).isTrue()
     }
 
     @Test
     fun isAvailable_configFalse_shouldReturnFalse() {
         mockResources.stub { on { getBoolean(anyInt()) } doReturn false }
 
-        assertThat(preferenceScreenCreator.isAvailable(context)).isFalse()
+        assertThat(preferenceScreenCreator.isAvailable(contextWrapper)).isFalse()
+    }
+
+    override fun migration() {
+        // avoid UnsupportedOperationException when getDisplay from context
+        System.setProperty("robolectric.createActivityContexts", "true")
+
+        val lockPatternUtils = mock<LockPatternUtils> { on { isSecure(anyInt()) } doReturn true }
+        FakeFeatureFactory.setupForTest().securityFeatureProvider.stub {
+            on { getLockPatternUtils(any()) } doReturn lockPatternUtils
+        }
+
+        super.migration()
     }
 }
diff --git a/tests/robotests/src/com/android/settings/notification/SoundScreenTest.kt b/tests/robotests/src/com/android/settings/notification/SoundScreenTest.kt
index 83b29d2..1333ed5 100644
--- a/tests/robotests/src/com/android/settings/notification/SoundScreenTest.kt
+++ b/tests/robotests/src/com/android/settings/notification/SoundScreenTest.kt
@@ -15,39 +15,23 @@
  */
 package com.android.settings.notification
 
-import android.content.Context
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.platform.test.flag.junit.SetFlagsRule
-import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.settings.flags.Flags
+import com.android.settingslib.preference.CatalystScreenTestCase
 import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
-class SoundScreenTest {
-    @get:Rule val setFlagsRule = SetFlagsRule()
-    private val context: Context = ApplicationProvider.getApplicationContext()
-    private val soundScreen = SoundScreen()
+class SoundScreenTest : CatalystScreenTestCase() {
+
+    override val preferenceScreenCreator = SoundScreen()
+
+    override val flagName: String
+        get() = Flags.FLAG_CATALYST_SOUND_SCREEN
 
     @Test
     fun key() {
-        assertThat(soundScreen.key).isEqualTo(SoundScreen.KEY)
+        assertThat(preferenceScreenCreator.key).isEqualTo(SoundScreen.KEY)
     }
-
-    @Test
-    @EnableFlags(Flags.FLAG_CATALYST_SOUND_SCREEN)
-    fun isFlagEnabled_returnTrue() {
-        assertThat(soundScreen.isFlagEnabled(context)).isTrue()
-    }
-
-    @Test
-    @DisableFlags(Flags.FLAG_CATALYST_SOUND_SCREEN)
-    fun isFlagEnabled_returnFalse() {
-        assertThat(soundScreen.isFlagEnabled(context)).isFalse()
-    }
-
-}
\ No newline at end of file
+}