[CameraPipe] Sync fix for correct display selection based on state

DisplayInfoManager.getMaxDisplay incorrectly choses the display when multiple displays are present. It can end up choosing a display in off state in current implementation.

To fix it, logic from aosp/1974040 and aosp/2267063 are sync'ed.
The method will return the max size display that is not in off state. It will return max size off state display as a fallback if all displays are in off state.

Bug: b/255754025
Test: ./gradlew bOS
Change-Id: I13013637def795d7f5c5356b5b4a8ea8ba0b871b
Signed-off-by: Tahsin Masrur <tahsinm@google.com>
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
new file mode 100644
index 0000000..7bbc024
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2022 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 androidx.camera.camera2.pipe.integration.impl
+
+import android.content.Context
+import android.graphics.Point
+import android.hardware.display.DisplayManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assume
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@Suppress("DEPRECATION") // getRealSize
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class DisplayInfoManagerTest {
+    private val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+
+    @Test
+    fun defaultDisplayIsDeviceDisplay_whenOneDisplay() {
+        // Arrange
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+
+        Assume.assumeTrue(displayManager.displays.size == 1)
+
+        val currentDisplaySize = Point()
+        displayManager.displays[0].getRealSize(currentDisplaySize)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(currentDisplaySize, size)
+    }
+
+    @Test
+    fun previewSizeAreaIsWithinMaxPreviewArea() {
+        // Act & Assert
+        val previewSize = displayInfoManager.previewSize
+        assertTrue("$previewSize has larger area than 1920 * 1080",
+            previewSize.width * previewSize.height <= 1920 * 1080)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
index 1babffd..c7745f6 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
@@ -19,6 +19,9 @@
 import android.content.Context
 import android.graphics.Point
 import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.DisplayListener
+import android.os.Handler
+import android.os.Looper
 import android.util.Size
 import android.view.Display
 import androidx.annotation.RequiresApi
@@ -31,45 +34,86 @@
 class DisplayInfoManager @Inject constructor(context: Context) {
     private val MAX_PREVIEW_SIZE = Size(1920, 1080)
 
+    companion object {
+        private var lazyMaxDisplay: Display? = null
+        private var lazyPreviewSize: Size? = null
+
+        internal fun invalidateLazyFields() {
+            lazyMaxDisplay = null
+            lazyPreviewSize = null
+        }
+
+        internal val displayListener by lazy {
+            object : DisplayListener {
+                override fun onDisplayAdded(displayId: Int) {
+                    invalidateLazyFields()
+                }
+
+                override fun onDisplayRemoved(displayId: Int) {
+                    invalidateLazyFields()
+                }
+
+                override fun onDisplayChanged(displayId: Int) {
+                    invalidateLazyFields()
+                }
+            }
+        }
+    }
+
     private val displayManager: DisplayManager by lazy {
-        context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+        (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager).also {
+            it.registerDisplayListener(displayListener, Handler(Looper.getMainLooper()))
+        }
     }
 
-    // TODO(b/198257203): Fetch latest display information for devices where display size is not
-    //  guaranteed to be fixed. (e.g. foldable devices or devices with multiple displays)
+    val defaultDisplay: Display
+        get() = getMaxSizeDisplay()
 
-    val defaultDisplay: Display by lazy {
-        getMaxSizeDisplay()
-    }
-
-    val previewSize: Size by lazy {
-        calculatePreviewSize()
-    }
+    val previewSize: Size
+        get() = calculatePreviewSize()
 
     private fun getMaxSizeDisplay(): Display {
+        lazyMaxDisplay?.let { return it }
+
         val displays = displayManager.displays
+
+        var maxDisplayWhenStateNotOff: Display? = null
+        var maxDisplaySizeWhenStateNotOff = -1
+
         var maxDisplay: Display? = null
         var maxDisplaySize = -1
 
-        // TODO(b/211945950, b/255170076): Handle STATE_OFF displays.
-
         for (display: Display in displays) {
             val displaySize = Point()
             // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
             //  is deprecated since API level 31.
             display.getRealSize(displaySize)
+
             if (displaySize.x * displaySize.y > maxDisplaySize) {
                 maxDisplaySize = displaySize.x * displaySize.y
                 maxDisplay = display
             }
+            if (display.state != Display.STATE_OFF) {
+                if (displaySize.x * displaySize.y > maxDisplaySizeWhenStateNotOff) {
+                    maxDisplaySizeWhenStateNotOff = displaySize.x * displaySize.y
+                    maxDisplayWhenStateNotOff = display
+                }
+            }
         }
-        return checkNotNull(maxDisplay) { "No displays found from ${displayManager.displays}!" }
+
+        lazyMaxDisplay = maxDisplayWhenStateNotOff ?: maxDisplay
+
+        return checkNotNull(lazyMaxDisplay) {
+            "No displays found from ${displayManager.displays}!"
+        }
     }
 
     /**
      * Calculates the device's screen resolution, or MAX_PREVIEW_SIZE, whichever is smaller.
      */
     private fun calculatePreviewSize(): Size {
+        lazyPreviewSize?.let { return it }
+
         val displaySize = Point()
         val display: Display = defaultDisplay
         // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
@@ -87,6 +131,7 @@
             displayViewSize = MAX_PREVIEW_SIZE
         }
         // TODO(b/230402463): Migrate extra cropping quirk from CameraX.
-        return displayViewSize
+
+        return displayViewSize.also { lazyPreviewSize = displayViewSize }
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
index f550d39..1d12d9b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -20,22 +20,57 @@
 import android.graphics.Point
 import android.hardware.display.DisplayManager
 import android.util.Size
+import android.view.Display
+import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.test.core.app.ApplicationProvider
+import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowDisplay
 import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
 
 @Suppress("DEPRECATION") // getRealSize
-@RunWith(RobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
 class DisplayInfoManagerTest {
     private val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
 
-    private fun addDisplay(width: Int, height: Int) {
-        ShadowDisplayManager.addDisplay(String.format("w%ddp-h%ddp", width, height))
+    private fun addDisplay(width: Int, height: Int, state: Int = Display.STATE_ON): Int {
+        val displayStr = String.format("w%ddp-h%ddp", width, height)
+        val displayId = ShadowDisplayManager.addDisplay(displayStr)
+
+        if (state != Display.STATE_ON) {
+            val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+                .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+            (Shadow.extract(displayManager.getDisplay(displayId)) as ShadowDisplay).setState(state)
+        }
+
+        return displayId
+    }
+
+    companion object {
+        @JvmStatic
+        @BeforeClass
+        fun classSetUp() {
+            DisplayInfoManager.invalidateLazyFields()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager?
+
+        displayManager?.let {
+            for (display in it.displays) {
+                removeDisplay(display.displayId)
+            }
+        }
     }
 
     @Test
@@ -68,10 +103,89 @@
         assertEquals(Point(2000, 3000), size)
     }
 
+    @Test
+    fun defaultDisplayIsMaxSizeDisplay_whenPreviousMaxDisplayRemoved() {
+        // Arrange
+        val id = addDisplay(2000, 3000)
+        addDisplay(480, 640)
+        removeDisplay(id)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeDisplay_whenNewMaxDisplayAddedAfterGettingPrevious() {
+        // Arrange
+        addDisplay(480, 640)
+
+        // Act
+        displayInfoManager.defaultDisplay
+        addDisplay(2000, 3000)
+
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(2000, 3000), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInNotOffState_whenMultipleDisplayWithSomeOffState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640)
+        addDisplay(240, 320)
+        addDisplay(200, 300, Display.STATE_OFF)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInNotOffState_whenMultipleDisplayWithNoOnState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640, Display.STATE_UNKNOWN)
+        addDisplay(240, 320, Display.STATE_UNKNOWN)
+        addDisplay(200, 300, Display.STATE_OFF)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInOffState_whenMultipleDisplayWithAllOffState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640, Display.STATE_OFF)
+        addDisplay(200, 300, Display.STATE_OFF)
+        removeDisplay(0)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(2000, 3000), size)
+    }
+
     @Test(expected = IllegalStateException::class)
     fun throwsCorrectExceptionForDefaultDisplay_whenNoDisplay() {
         // Arrange
-        ShadowDisplayManager.removeDisplay(0)
+        removeDisplay(0)
 
         // Act
         val size = Point()
@@ -95,4 +209,17 @@
         // Act & Assert
         assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
     }
+
+    @Test
+    fun previewSizeIsUpdated_whenNewDisplayAddedAfterPreviousUse() {
+        // Arrange
+        addDisplay(480, 640)
+
+        // Act
+        displayInfoManager.previewSize
+        addDisplay(2000, 3000)
+
+        // Assert
+        assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
+    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
index f40a4cf..cc035a9 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
@@ -16,19 +16,24 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
+import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.display.DisplayManager
 import android.os.Build
 import android.util.Size
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.test.core.app.ApplicationProvider
+import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
 import org.robolectric.shadows.StreamConfigurationMapBuilder
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
@@ -80,6 +85,12 @@
                 )
             )
         }
+
+        @JvmStatic
+        @BeforeClass
+        fun classSetUp() {
+            DisplayInfoManager.invalidateLazyFields()
+        }
     }
 
     private lateinit var meteringRepeating: MeteringRepeating
@@ -103,6 +114,18 @@
         ).build()
     }
 
+    @After
+    fun tearDown() {
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager?
+
+        displayManager?.let {
+            for (display in it.displays) {
+                removeDisplay(display.displayId)
+            }
+        }
+    }
+
     @Test
     fun attachedSurfaceResolutionIsLargestLessThan640x480_when640x480NotPresentInOutputSizes() {
         meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithout640x480)