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

Commit 8ab0aa31 authored by Toshiki Kikuchi's avatar Toshiki Kikuchi
Browse files

Add DesktopFirstListenerManager

This CL adds DesktopFirstListener to let anyone in SysUI and Shell to
observe the state of desktop-first mode.

Flag: com.android.window.flags.enable_desktop_first_listener
Bug: 415120289
Test: DesktopFirstListenerManagerTest
Change-Id: Iddaa56d023db197487ced4c016a2f9be2e6e8ae6
parent 107c6bb3
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
/*
 * 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.wm.shell.shared.desktopmode

/**
 * A listener that will receive callbacks about desktop-first state.
 */
fun interface DesktopFirstListener {
    /**
     * Called when the desktop-first state changes.
     */
    fun onStateChanged(displayId: Int, isDesktopFirstEnabled: Boolean)
}
+22 −2
Original line number Diff line number Diff line
@@ -126,6 +126,7 @@ import com.android.wm.shell.desktopmode.VisualIndicatorUpdateScheduler;
import com.android.wm.shell.desktopmode.WindowDecorCaptionRepository;
import com.android.wm.shell.desktopmode.compatui.SystemModalsTransitionHandler;
import com.android.wm.shell.desktopmode.desktopfirst.DesktopDisplayModeController;
import com.android.wm.shell.desktopmode.desktopfirst.DesktopFirstListenerManager;
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter;
@@ -882,7 +883,8 @@ public abstract class WMShellModule {
            HomeIntentProvider homeIntentProvider,
            DesktopState desktopState,
            DesktopConfig desktopConfig,
            VisualIndicatorUpdateScheduler visualIndicatorUpdateScheduler) {
            VisualIndicatorUpdateScheduler visualIndicatorUpdateScheduler,
            Optional<DesktopFirstListenerManager> desktopFirstListenerManager) {
        return new DesktopTasksController(
                context,
                shellInit,
@@ -929,7 +931,8 @@ public abstract class WMShellModule {
                homeIntentProvider,
                desktopState,
                desktopConfig,
                visualIndicatorUpdateScheduler);
                visualIndicatorUpdateScheduler,
                desktopFirstListenerManager);
    }

    @WMSingleton
@@ -1526,6 +1529,23 @@ public abstract class WMShellModule {
                        desktopState));
    }

    @WMSingleton
    @Provides
    static Optional<DesktopFirstListenerManager> provideDesktopFirstListenerManager(
            @NonNull DesktopState desktopState,
            @NonNull ShellInit shellInit,
            @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
            @NonNull DisplayController displayController
    ) {
        if (desktopState.canEnterDesktopMode()
                && DesktopExperienceFlags.ENABLE_DESKTOP_FIRST_LISTENER.isTrue()) {
            return Optional.of(
                    new DesktopFirstListenerManager(shellInit, rootTaskDisplayAreaOrganizer,
                            displayController));
        }
        return Optional.empty();
    }

    @WMSingleton
    @Provides
    static AppHandleNotifier provideAppHandleNotifier(
+13 −0
Original line number Diff line number Diff line
@@ -16,9 +16,11 @@

package com.android.wm.shell.desktopmode;

import android.annotation.NonNull;
import android.graphics.Region;

import com.android.wm.shell.shared.annotations.ExternalThread;
import com.android.wm.shell.shared.desktopmode.DesktopFirstListener;
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;

import java.util.concurrent.Executor;
@@ -57,4 +59,15 @@ public interface DesktopMode {

    /** Called when requested to go to split screen from the current focused desktop app. */
    void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop);

    /**
     * Register a listener that will receive callbacks about desktop-first state. Once it's
     * registered, the listener immediately receives the current state.
     */
    void registerDesktopFirstListener(@NonNull DesktopFirstListener listener);

    /**
     * Unregister a registered desktop-first listener
     */
    void unregisterDesktopFirstListener(@NonNull DesktopFirstListener listener);
}
+23 −0
Original line number Diff line number Diff line
@@ -120,6 +120,7 @@ import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
import com.android.wm.shell.desktopmode.desktopfirst.DesktopFirstListenerManager
import com.android.wm.shell.desktopmode.desktopfirst.isDisplayDesktopFirst
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler
@@ -143,6 +144,7 @@ import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellDesktopThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.desktopmode.DesktopConfig
import com.android.wm.shell.shared.desktopmode.DesktopFirstListener
import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.shared.desktopmode.DesktopState
@@ -237,6 +239,7 @@ class DesktopTasksController(
    private val desktopState: DesktopState,
    private val desktopConfig: DesktopConfig,
    private val visualIndicatorUpdateScheduler: VisualIndicatorUpdateScheduler,
    private val desktopFirstListenerManager: Optional<DesktopFirstListenerManager>,
) :
    RemoteCallable<DesktopTasksController>,
    Transitions.TransitionHandler,
@@ -4766,6 +4769,26 @@ class DesktopTasksController(
            logV("moveFocusedTaskToStageSplit")
            mainExecutor.execute { this@DesktopTasksController.enterSplit(displayId, leftOrTop) }
        }

        override fun registerDesktopFirstListener(listener: DesktopFirstListener) {
            logV("registerDesktopFirstListener")
            if (desktopFirstListenerManager.isEmpty) {
                throw UnsupportedOperationException(
                    "DesktopFirstListenerManager is not available on this device"
                )
            }
            mainExecutor.execute { desktopFirstListenerManager.get().registerListener(listener) }
        }

        override fun unregisterDesktopFirstListener(listener: DesktopFirstListener) {
            logV("unregisterDesktopFirstListener")
            if (desktopFirstListenerManager.isEmpty) {
                throw UnsupportedOperationException(
                    "DesktopFirstListenerManager is not available on this device"
                )
            }
            mainExecutor.execute { desktopFirstListenerManager.get().unregisterListener(listener) }
        }
    }

    /** The interface for calls from outside the host process. */
+106 −0
Original line number Diff line number Diff line
/*
 * 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.wm.shell.desktopmode.desktopfirst

import android.util.ArrayMap
import android.util.ArraySet
import android.window.DisplayAreaInfo
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.shared.desktopmode.DesktopFirstListener
import com.android.wm.shell.sysui.ShellInit

/** Manages the desktop-first listeners */
class DesktopFirstListenerManager(
    shellInit: ShellInit,
    private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
    private val displayController: DisplayController,
) : DisplayController.OnDisplaysChangedListener {

    private val notifiedIsDesktopFirstByDisplayId = ArrayMap<Int, Boolean>()
    private val listeners = ArraySet<DesktopFirstListener>()

    private val rootTaskDisplayAreaListener =
        object : RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener {
            override fun onDisplayAreaAppeared(displayAreaInfo: DisplayAreaInfo) {
                val displayId = displayAreaInfo.displayId
                val isDesktopFirst = rootTaskDisplayAreaOrganizer.isDisplayDesktopFirst(displayId)
                notifiedIsDesktopFirstByDisplayId[displayId] = isDesktopFirst
                for (listener in listeners) {
                    listener.onStateChanged(displayId, isDesktopFirst)
                }
            }

            override fun onDisplayAreaVanished(displayAreaInfo: DisplayAreaInfo) {
                val displayId = displayAreaInfo.displayId
                notifiedIsDesktopFirstByDisplayId.remove(displayId)
            }

            override fun onDisplayAreaInfoChanged(displayAreaInfo: DisplayAreaInfo) {
                val displayId = displayAreaInfo.displayId
                val isDesktopFirst = rootTaskDisplayAreaOrganizer.isDisplayDesktopFirst(displayId)

                if (notifiedIsDesktopFirstByDisplayId[displayId] == isDesktopFirst) {
                    // No change.
                    return
                }

                for (listener in listeners) {
                    listener.onStateChanged(displayId, isDesktopFirst)
                }
                notifiedIsDesktopFirstByDisplayId[displayId] = isDesktopFirst
            }
        }

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

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

    /**
     * Register a listener that will receive callbacks about desktop-first state. Once it's
     * registered, the listener immediately receives the current state.
     */
    fun registerListener(listener: DesktopFirstListener) {
        if (!listeners.add(listener)) {
            // The listener is already registered.
            return
        }

        // Notifies the current state on registered.
        for (displayId in rootTaskDisplayAreaOrganizer.displayIds) {
            val isDesktopFirst = rootTaskDisplayAreaOrganizer.isDisplayDesktopFirst(displayId)
            listener.onStateChanged(displayId, isDesktopFirst)
        }
    }

    /** Unregister a registered desktop-first listener */
    fun unregisterListener(listener: DesktopFirstListener) {
        listeners.remove(listener)
    }

    override fun onDisplayAdded(displayId: Int) {
        rootTaskDisplayAreaOrganizer.registerListener(displayId, rootTaskDisplayAreaListener)
    }

    override fun onDisplayRemoved(displayId: Int) {
        rootTaskDisplayAreaOrganizer.unregisterListener(displayId, rootTaskDisplayAreaListener)
    }
}
Loading