/* * 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.display import android.content.Context import android.graphics.RectF import android.hardware.display.DisplayManager import android.hardware.display.DisplayTopology import android.hardware.display.DisplayTopology.POSITION_BOTTOM import android.hardware.display.DisplayTopology.POSITION_LEFT import android.hardware.display.DisplayTopology.POSITION_RIGHT import android.hardware.display.DisplayTopology.POSITION_TOP import android.provider.Settings import android.util.DisplayMetrics import android.util.Size import android.view.Display.DEFAULT_DISPLAY import android.view.Display.Mode import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewManager import android.widget.FrameLayout import androidx.preference.PreferenceViewHolder import androidx.test.core.app.ApplicationProvider import androidx.test.core.view.MotionEventBuilder import com.android.settings.R import com.android.settings.flags.FakeFeatureFlagsImpl import com.android.settings.flags.Flags.FLAG_SHOW_STACKED_MIRRORING_DISPLAY_CONNECTED_DISPLAY_SETTING import com.android.settings.flags.Flags.FLAG_SHOW_TABBED_CONNECTED_DISPLAY_SETTING import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import kotlin.math.abs import kotlin.math.min import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class DisplayTopologyPreferenceTest { val context = ApplicationProvider.getApplicationContext() val featureFlags = FakeFeatureFlagsImpl() val injector = TestInjector(context, featureFlags) val preference = DisplayTopologyPreference(injector) val rootView = View.inflate(context, preference.layoutResource, /* root= */ null) val holder = PreferenceViewHolder.createInstanceForTests(rootView) init { preference.onBindViewHolder(holder) featureFlags.setFlag(FLAG_SHOW_STACKED_MIRRORING_DISPLAY_CONNECTED_DISPLAY_SETTING, true) featureFlags.setFlag(FLAG_SHOW_TABBED_CONNECTED_DISPLAY_SETTING, false) } class TestInjector(context: Context, featureFlags: FakeFeatureFlagsImpl) : ConnectedDisplayInjector(context) { var displaysSize = mutableMapOf() var topology: DisplayTopology? = null var topologyListener: Consumer? = null var displayListener: DisplayManager.DisplayListener? = null override var displayTopology: DisplayTopology? get() = topology set(value) { topology = value } override val flags = DesktopExperienceFlags(featureFlags) /** A log of events related to wallpaper revealing. */ val revealLog = mutableListOf() override val densityDpi = DisplayMetrics.DENSITY_DEFAULT override fun getLogicalSize(displayId: Int): Size? { return displaysSize[displayId] } override fun getDisplays(): List { return displaysSize .map { it -> val mode = Mode(it.value.width, it.value.height, 60f) DisplayDevice( it.key, "test:123", "HDMI", mode, listOf(mode), /* isEnabled= */ DisplayIsEnabled.YES, /* isConnectedDisplay= */ true, ) } .toList() } override fun registerTopologyListener(listener: Consumer) { if (topologyListener != null) { throw IllegalStateException("already have a listener registered: $topologyListener") } topologyListener = listener } override fun unregisterTopologyListener(listener: Consumer) { if (topologyListener != listener) { throw IllegalStateException("no such listener registered: $listener") } topologyListener = null } override fun registerDisplayListener(listener: DisplayManager.DisplayListener) { if (displayListener != null) { throw IllegalStateException("already have a listener registered: $displayListener") } displayListener = listener } override fun unregisterDisplayListener(listener: DisplayManager.DisplayListener) { if (displayListener != listener) { throw IllegalStateException("no such listener registered: $listener") } displayListener = null } override fun revealWallpaper(displayId: Int): RevealedWallpaper { val childView = View(context) val viewManager = object : ViewManager { override fun addView(view: View, params: ViewGroup.LayoutParams) { revealLog.add("unexpected invocation of addView") } override fun updateViewLayout(view: View, params: ViewGroup.LayoutParams) { revealLog.add("unexpected invocation of updateViewLayout") } override fun removeView(view: View) { if (childView === view) { revealLog.add("removed wallpaper revealer for display $displayId") } else { revealLog.add("invalid invocation of removeView") } } } revealLog.add("revealWallpaper invoked for display $displayId") return RevealedWallpaper(displayId, childView, viewManager) } } /** Returns the bounds of the non-highlighting part of the block relative to the parent. */ private fun virtualBounds(block: DisplayBlock): RectF { val d = block.highlightPx.toFloat() val x = block.x + d val y = block.y + d // Using layoutParams as a proxy for the actual width and height appears to be standard // practice in Robolectric tests, as they do not actually process layout requests. val w = block.layoutParams.width - 2 * d val h = block.layoutParams.height - 2 * d return RectF(x, y, x + w, y + h) } private fun getPaneChildren(): List = (0.. { setupTwoDisplays(childPosition, childOffset) preparePane() val paneChildren = getPaneChildren() assertThat(paneChildren).hasSize(2) // Block of child display is on the left. return if (paneChildren[0].x < paneChildren[1].x) paneChildren else paneChildren.reversed() } private fun assertSelected(block: DisplayBlock, expected: Boolean) { val vis = if (expected) View.VISIBLE else View.INVISIBLE assertThat(block.selectionMarkerView.visibility).isEqualTo(vis) } private fun setMirroringMode(enable: Boolean) { Settings.Secure.putInt( context.contentResolver, Settings.Secure.MIRROR_BUILT_IN_DISPLAY, if (enable) 1 else 0, ) } private fun dragBlockWithOneMoveEvent( block: DisplayBlock, startTimeMs: Long, endTimeMs: Long, xDiff: Float, yDiff: Float, ) { block.dispatchTouchEvent( MotionEventBuilder.newBuilder() .setAction(MotionEvent.ACTION_DOWN) .setPointer(0f, 0f) .setEventTime(startTimeMs) .build() ) block.dispatchTouchEvent( MotionEventBuilder.newBuilder() .setAction(MotionEvent.ACTION_MOVE) .setPointer(xDiff, yDiff) .setEventTime((startTimeMs + endTimeMs) / 2) .build() ) block.dispatchTouchEvent( MotionEventBuilder.newBuilder() .setAction(MotionEvent.ACTION_UP) .setPointer(xDiff, yDiff) .setEventTime(endTimeMs) .build() ) } @Test fun disabledTopology() { preference.onAttached() preference.controller.refreshPane() assertThat(preference.controller.paneContent.childCount).isEqualTo(0) // TODO(b/352648432): update test when we show the main display even when // a topology is not active. assertThat(preference.controller.topologyHint.text).isEqualTo("") } @Test fun twoDisplaysGenerateBlocks() { val (childBlock, rootBlock) = setupPaneWithTwoDisplays() val childBounds = virtualBounds(childBlock) val rootBounds = virtualBounds(rootBlock) // After accounting for padding, child should be half the length of root in each dimension. assertThat(childBounds.width()).isEqualTo(rootBounds.width() / 2) assertThat(childBounds.height()).isEqualTo(rootBounds.height() / 2) assertThat(childBounds.top).isGreaterThan(rootBounds.top) assertSelected(childBlock, false) assertSelected(rootBlock, false) assertThat(rootBounds.left).isEqualTo(childBounds.right) assertThat(preference.controller.topologyHint.text) .isEqualTo(context.getString(R.string.external_display_topology_hint)) } @Test fun threeDisplaysMirroringGenerateBlocks() { setMirroringMode(true) setupPaneWithTwoDisplays() val newDisplaySize = Size(500, 500) injector.topology!!.addDisplay( DISPLAY_ID_3, newDisplaySize.width, newDisplaySize.height, DISPLAY_DENSITY, ) injector.displaysSize[DISPLAY_ID_3] = newDisplaySize preference.controller.refreshPane() val paneChildren = getPaneChildren() assertThat(paneChildren).hasSize(3) for (i in 1..