Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 374bde7e authored by Toshiki Kikuchi's avatar Toshiki Kikuchi Committed by Android (Google) Code Review
Browse files

Merge "Switch display windowing mode when display connected" into main

parents 23809061 3f3fb030
Loading
Loading
Loading
Loading
+27 −1
Original line number Diff line number Diff line
@@ -67,6 +67,7 @@ import com.android.wm.shell.dagger.pip.PipModule;
import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler;
import com.android.wm.shell.desktopmode.DesktopImmersiveController;
import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
@@ -1014,6 +1015,30 @@ public abstract class WMShellModule {
        return new DesktopModeEventLogger();
    }

    @WMSingleton
    @Provides
    static Optional<DesktopDisplayEventHandler> provideDesktopDisplayEventHandler(
            Context context,
            ShellInit shellInit,
            Transitions transitions,
            DisplayController displayController,
            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
            IWindowManager windowManager
    ) {
        if (!DesktopModeStatus.canEnterDesktopMode(context)
                || !Flags.enableDisplayWindowingModeSwitching()) {
            return Optional.empty();
        }
        return Optional.of(
                new DesktopDisplayEventHandler(
                        context,
                        shellInit,
                        transitions,
                        displayController,
                        rootTaskDisplayAreaOrganizer,
                        windowManager));
    }

    @WMSingleton
    @Provides
    static AppHandleEducationDatastoreRepository provideAppHandleEducationDatastoreRepository(
@@ -1180,7 +1205,8 @@ public abstract class WMShellModule {
    @Provides
    static Object provideIndependentShellComponentsToCreate(
            DragAndDropController dragAndDropController,
            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) {
            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional,
            Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler) {
        return new Object();
    }
}
+95 −0
Original line number Diff line number Diff line
/*
 * 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.wm.shell.desktopmode

import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.Context
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.WindowContainerTransaction
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions

/** Handles display events in desktop mode */
class DesktopDisplayEventHandler(
    private val context: Context,
    shellInit: ShellInit,
    private val transitions: Transitions,
    private val displayController: DisplayController,
    private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
    private val windowManager: IWindowManager,
) : OnDisplaysChangedListener {

    init {
        shellInit.addInitCallback({ onInit() }, this)
    }

    private fun onInit() {
        displayController.addDisplayWindowListener(this)
    }

    override fun onDisplayAdded(displayId: Int) {
        if (displayId == DEFAULT_DISPLAY) {
            return
        }
        refreshDisplayWindowingMode()
    }

    override fun onDisplayRemoved(displayId: Int) {
        if (displayId == DEFAULT_DISPLAY) {
            return
        }
        refreshDisplayWindowingMode()
    }

    private fun refreshDisplayWindowingMode() {
        // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
        val isExtendedDisplayEnabled = 0 != Settings.Global.getInt(
            context.contentResolver, DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0
        )
        if (!isExtendedDisplayEnabled) {
            // No action needed in mirror or projected mode.
            return
        }

        val hasNonDefaultDisplay = rootTaskDisplayAreaOrganizer.getDisplayIds()
            .any { displayId -> displayId != DEFAULT_DISPLAY }
        val targetDisplayWindowingMode =
            if (hasNonDefaultDisplay) {
                WINDOWING_MODE_FREEFORM
            } else {
                // Use the default display windowing mode when no non-default display.
                windowManager.getWindowingMode(DEFAULT_DISPLAY)
            }
        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
        requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
        if (tdaInfo.configuration.windowConfiguration.windowingMode == targetDisplayWindowingMode) {
            // Already in the target mode.
            return
        }

        val wct = WindowContainerTransaction()
        wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode)
        transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
    }
}
+171 −0
Original line number Diff line number Diff line
/*
 * 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.wm.shell.desktopmode

import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.ContentResolver
import android.os.Binder
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.DisplayAreaInfo
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
import com.android.wm.shell.MockToken
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever

/**
 * Test class for [DesktopDisplayEventHandler]
 *
 * Usage: atest WMShellUnitTests:DesktopDisplayEventHandlerTest
 */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopDisplayEventHandlerTest : ShellTestCase() {

  @Mock lateinit var testExecutor: ShellExecutor
  @Mock lateinit var transitions: Transitions
  @Mock lateinit var displayController: DisplayController
  @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
  @Mock private lateinit var mockWindowManager: IWindowManager

  private lateinit var shellInit: ShellInit
  private lateinit var handler: DesktopDisplayEventHandler

  @Before
  fun setUp() {
    shellInit = spy(ShellInit(testExecutor))
    whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
    val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
    whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
    handler =
        DesktopDisplayEventHandler(
            context,
            shellInit,
            transitions,
            displayController,
            rootTaskDisplayAreaOrganizer,
            mockWindowManager,
        )
    shellInit.init()
  }

  private fun testDisplayWindowingModeSwitch(
    defaultWindowingMode: Int,
    extendedDisplayEnabled: Boolean,
    expectTransition: Boolean
  ) {
    val externalDisplayId = 100
    val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java)
    verify(displayController).addDisplayWindowListener(captor.capture())
    val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
    tda.configuration.windowConfiguration.windowingMode = defaultWindowingMode
    whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode }
    val settingsSession = ExtendedDisplaySettingsSession(
      context.contentResolver, if (extendedDisplayEnabled) 1 else 0)

    settingsSession.use {
      // The external display connected.
      whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
        .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId))
      captor.value.onDisplayAdded(externalDisplayId)
      tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
      // The external display disconnected.
      whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
        .thenReturn(intArrayOf(DEFAULT_DISPLAY))
      captor.value.onDisplayRemoved(externalDisplayId)

      if (expectTransition) {
        val arg = argumentCaptor<WindowContainerTransaction>()
        verify(transitions, times(2)).startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
        assertThat(arg.firstValue.changes[tda.token.asBinder()]?.windowingMode)
          .isEqualTo(WINDOWING_MODE_FREEFORM)
        assertThat(arg.secondValue.changes[tda.token.asBinder()]?.windowingMode)
          .isEqualTo(defaultWindowingMode)
      } else {
        verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull())
      }
    }
  }

  @Test
  fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() {
    testDisplayWindowingModeSwitch(
      defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
      extendedDisplayEnabled = false,
      expectTransition = false
    )
  }

  @Test
  fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() {
    testDisplayWindowingModeSwitch(
      defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
      extendedDisplayEnabled = true,
      expectTransition = true
    )
  }

  @Test
  fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() {
    testDisplayWindowingModeSwitch(
      defaultWindowingMode = WINDOWING_MODE_FREEFORM,
      extendedDisplayEnabled = true,
      expectTransition = false
    )
  }

  private class ExtendedDisplaySettingsSession(
    private val contentResolver: ContentResolver,
      private val overrideValue: Int
  ) : AutoCloseable {
    private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
    private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0)

    init { Settings.Global.putInt(contentResolver, settingName, overrideValue) }

    override fun close() {
      Settings.Global.putInt(contentResolver, settingName, initialValue)
    }
  }
}
 No newline at end of file