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

Commit 5813eb45 authored by Andrii Kulian's avatar Andrii Kulian
Browse files

Notify app clients about managed Bubbles removals

The user can remove a Bubble requested by an app. This adds
a callback to notify the app, so that it can clean up its internal
state and avoid requestiong actions on a Bubble that's no longer
there.

IMultitaskingControllerCallback is used by Shell to notify the
MultitaskingController in WM Core, which will then pass it to
BubbleContainerManager in the client process.

The BubbleContainerCallback is the public extensions API used to
notify the application.

MultitaskingController also automatically cleans up the bubbles if
the client process dies.

Bug: 407149510
Flag: com.android.window.flags.enable_experimental_bubbles_controller
Test: Manual, removing a Bubble created by the demo app
Change-Id: I52e2196dcdd7ee3896d5298260bb1979943aa04f
parent d440fa39
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package android.window;
import android.os.Bundle;
import android.os.IBinder;
import android.content.Intent;
import android.window.IMultitaskingControllerCallback;
import android.window.IMultitaskingDelegate;

/**
@@ -37,13 +38,14 @@ import android.window.IMultitaskingDelegate;
interface IMultitaskingController {
    /**
     * Method used by WMShell to register itself as a delegate that can respond to the app requests.
     * @return a callback used to notify the client about the changes in the managed windows.
     */
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)")
    oneway void registerMultitaskingDelegate(in IMultitaskingDelegate delegate);
    IMultitaskingControllerCallback registerMultitaskingDelegate(in IMultitaskingDelegate delegate);

    /**
     * Returns an instance of an interface for use by applications to make requests to the system.
     */
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.REQUEST_SYSTEM_MULTITASKING_CONTROLS)")
    @nullable IMultitaskingDelegate getClientInterface();
    @nullable IMultitaskingDelegate getClientInterface(in IMultitaskingControllerCallback callback);
}
+29 −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 android.window;

import android.os.IBinder;
import android.content.Intent;

/**
 * System private callback for notifying the registered clients about the updates related to
 * multitasking features they requested to control.
 * @hide
 */
interface IMultitaskingControllerCallback {
    oneway void onBubbleRemoved(in IBinder token);
}
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright 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 androidx.window.extensions.bubble;

import android.os.IBinder;

/**
 * Public interface used to notify applications about the changes to the bubble containers they
 * manage.
 */
public interface BubbleContainerCallback {
    /** Notifies about removal of a Bubble that was previously opened by the client. */
    void onBubbleRemoved(IBinder token);
}
+74 −13
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package androidx.window.extensions.bubble;

import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.RequiresPermission;
import android.app.ActivityTaskManager;
import android.content.Intent;
@@ -24,12 +25,16 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.window.IMultitaskingController;
import android.window.IMultitaskingControllerCallback;
import android.window.IMultitaskingDelegate;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * The interface that apps use to request control over Bubbles - special windowing feature that
@@ -41,13 +46,16 @@ public class BubbleContainerManager {

    private static BubbleContainerManager sInstance;

    // Interface used to communicate with the controller in the server
    private IMultitaskingDelegate mControllerInterface;

    // Callback used to update the client about changes from the controller in the server
    @NonNull
    private final IMultitaskingDelegate mClientInterface;
    private final ControllerCallback mControllerCallback = new ControllerCallback();

    private BubbleContainerManager(@NonNull IMultitaskingDelegate clientInterface) {
        Objects.requireNonNull(clientInterface);
        mClientInterface = clientInterface;
    }
    // Callback into the application code
    @NonNull
    private final List<CallbackRecord> mAppCallbacks = new ArrayList<>();

    /**
     * Obtain an instance of the class to make requests related to Bubbles system multi-tasking
@@ -66,10 +74,14 @@ public class BubbleContainerManager {
            return sInstance;
        }
        try {
            IMultitaskingController controller = ActivityTaskManager.getService()
            final IMultitaskingController controller = ActivityTaskManager.getService()
                    .getWindowOrganizerController().getMultitaskingController();
            IMultitaskingDelegate clientInterface = controller.getClientInterface();
            sInstance = new BubbleContainerManager(clientInterface);
            final BubbleContainerManager manager = new BubbleContainerManager();
            final IMultitaskingDelegate controllerInterface = controller.getClientInterface(
                    manager.mControllerCallback);
            Objects.requireNonNull(controllerInterface);
            sInstance = manager;
            sInstance.mControllerInterface = controllerInterface;
            return sInstance;
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception getting an instance of BubbleContainerManager",
@@ -89,13 +101,12 @@ public class BubbleContainerManager {
     */
    public void createBubble(IBinder token, Intent intent, boolean collapsed) {
        try {
            mClientInterface.createBubble(token, intent, collapsed);
            mControllerInterface.createBubble(token, intent, collapsed);
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception creating a Bubble", new RuntimeException(e));
        }
    }

    // TODO(b/407149510): Handle the case when the user removes the Bubble after it was created.
    /**
     * Update the state of an existing Bubble to either collapse or expand it.
     * @param token a token uniquely identifying a bubble.
@@ -103,7 +114,7 @@ public class BubbleContainerManager {
     */
    public void updateBubbleState(IBinder token, boolean collapsed) {
        try {
            mClientInterface.updateBubbleState(token, collapsed);
            mControllerInterface.updateBubbleState(token, collapsed);
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception updating Bubble state", new RuntimeException(e));
        }
@@ -116,7 +127,7 @@ public class BubbleContainerManager {
     */
    public void updateBubbleMessage(IBinder token, String message) {
        try {
            mClientInterface.updateBubbleMessage(token, message);
            mControllerInterface.updateBubbleMessage(token, message);
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception updating Bubble message", new RuntimeException(e));
        }
@@ -128,9 +139,59 @@ public class BubbleContainerManager {
     */
    public void removeBubble(IBinder token) {
        try {
            mClientInterface.removeBubble(token);
            mControllerInterface.removeBubble(token);
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception removing Bubble", new RuntimeException(e));
        }
    }

    /**
     * Registers a callback to get notified about changes to the managed bubbles.
     */
    public void registerBubbleContainerCallback(@NonNull @CallbackExecutor Executor executor,
            @NonNull BubbleContainerCallback callback) {
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);
        for (CallbackRecord record : mAppCallbacks) {
            if (record.mCallback.equals(callback)) {
                throw new IllegalArgumentException("Callback already registered");
            }
        }
        mAppCallbacks.add(new CallbackRecord(executor, callback));
    }

    /**
     * Unregisters a callback that was previously registered.
     * @see #registerBubbleContainerCallback(Executor, BubbleContainerCallback)
     */
    public void unregisterBubbleContainerCallback(@NonNull BubbleContainerCallback callback) {
        for (CallbackRecord record : mAppCallbacks) {
            if (record.mCallback.equals(callback)) {
                mAppCallbacks.remove(record);
                return;
            }
        }
        Log.e(TAG, "Didn't find a matching callback to remove");
    }

    private static class CallbackRecord {
        final Executor mExecutor;
        final BubbleContainerCallback mCallback;

        CallbackRecord(@NonNull Executor executor, @NonNull BubbleContainerCallback callback) {
            mExecutor = executor;
            mCallback = callback;
        }
    }

    private class ControllerCallback extends IMultitaskingControllerCallback.Stub {
        @Override
        public void onBubbleRemoved(IBinder token) {
            for (CallbackRecord callbackRecord : mAppCallbacks) {
                callbackRecord.mExecutor.execute(() -> {
                    callbackRecord.mCallback.onBubbleRemoved(token);
                });
            }
        }
    }
}
+7 −1
Original line number Diff line number Diff line
@@ -83,6 +83,7 @@ import android.view.ViewRootImpl;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.window.IMultitaskingController;
import android.window.IMultitaskingControllerCallback;
import android.window.ScreenCapture;
import android.window.ScreenCapture.SynchronousScreenCaptureListener;
import android.window.TransitionInfo;
@@ -556,8 +557,10 @@ public class BubbleController implements ConfigurationChangeListener,
                        this, mBubbleData, mCurrentUserId);
                final IMultitaskingController mtController = ActivityTaskManager.getService()
                        .getWindowOrganizerController().getMultitaskingController();
                final IMultitaskingControllerCallback callback =
                        mtController.registerMultitaskingDelegate(delegate);
                mBubbleMultitaskingDelegate = delegate;
                mBubbleMultitaskingDelegate.setControllerCallback(callback);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to register Bubble multitasking delegate.", e);
            }
@@ -2430,6 +2433,9 @@ public class BubbleController implements ConfigurationChangeListener,
                @Bubbles.DismissReason final int reason = removed.second;

                mBubbleViewCallback.removeBubble(bubble);
                if (bubble.getClientToken() != null && mBubbleMultitaskingDelegate != null) {
                    mBubbleMultitaskingDelegate.onBubbleRemoved(bubble.getClientToken(), reason);
                }

                // Leave the notification in place if we're dismissing due to user switching, or
                // because DND is suppressing the bubble. In both of those cases, we need to be able
Loading