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

Commit f46f8e48 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Hidden API for Chooser interactive session." into main

parents df062e97 edd5a679
Loading
Loading
Loading
Loading
+432 −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 android.service.chooser;

import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;

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

/**
 * <p>A class that represents an interactive Chooser session.</p>
 * <p>An instance of the class can be used as a value for <em>an</em> {@link Intent#ACTION_CHOOSER}
 * extra to establish a bi-directional communication channel with Chooser.
 * <p>A {@link ChooserSessionUpdateListener} callback can be used to receive updates about the
 * session and communication from Chooser.</p>
 *
 * @hide
 *
 */
public final class ChooserSession implements Parcelable {

    private static final String TAG = "ChooserSession";

    private final IChooserControllerCallback mSessionCallbackBinder;

    // mChooserSession is expected to be null only on the Chooser side
    @Nullable
    private final ChooserSessionImpl mChooserSession;

    /**
     * An alias for {@code ChooserSession(Looper.getMainLooper())}.
     */
    public ChooserSession() {
        this(new Handler(Looper.getMainLooper()));
    }

    /**
     * @param handler a thread {@link ChooserSessionUpdateListener} callbacks will be delivered on.
     */
    public ChooserSession(Handler handler) {
        this(new ChooserSessionImpl(handler));
    }

    private ChooserSession(IChooserControllerCallback sessionBinder) {
        mSessionCallbackBinder = sessionBinder;
        mChooserSession = (sessionBinder instanceof ChooserSessionImpl)
                ? (ChooserSessionImpl) sessionBinder
                : null;
    }

    /**
     * @return true if the session is active: i.e. is not being cancelled by the client
     * (see {@link #cancel()}) or closed by the Chooser.
     */
    public boolean isActive() {
        return mChooserSession != null && mChooserSession.isActive();
    }

    /**
     * Cancel the session and close the Chooser.
     */
    public void cancel() {
        if (mChooserSession != null) {
            mChooserSession.cancel();
        }
    }

    /**
     * @return underlying {@link IChooserControllerCallback} binder.
     *
     * @hide
     */
    public IChooserControllerCallback getSessionCallbackBinder() {
        return mSessionCallbackBinder;
    }

    /**
     * <p>Get the active {@link ChooserController} or {@code null} if none is available.</p>
     * A chooser controller becomes available after the Chooser has registered it and stays
     * available while the session is active and the Chooser process is alive. It is possible for a
     * session to remain active without a Chooser process. For example, this could happen when the
     * client launches another activity on top of the Chooser session and the system reclaims the
     * new backgrounded chooser process. In such example, upon navigating back to the session, a
     * restored Chooser should register a new {@link ChooserController}.
     */
    @Nullable
    public ChooserController getChooserController() {
        return mChooserSession == null ? null : mChooserSession.getChooserController();
    }

    /**
     * @param listener make sure that the callback is cleared at the end of a component's lifecycle
     * (e.g. Activity) or provide a properly maintained WeakReference wrapper to avoid memory leaks.
     */
    public void setChooserStateListener(@Nullable ChooserSessionUpdateListener listener) {
        if (mChooserSession != null) {
            mChooserSession.setChooserStateListener(
                    listener == null
                            ? null
                            : new ChooserSessionUpdateListenerWrapper(this, listener));
        }
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        if (mChooserSession != null) {
            synchronized (mChooserSession) {
                dest.writeStrongBinder(mChooserSession);
            }
        }
    }

    public static final Parcelable.Creator<ChooserSession> CREATOR = new Creator<>() {
        @Override
        public ChooserSession createFromParcel(Parcel source) {
            IChooserControllerCallback binder =
                    IChooserControllerCallback.Stub.asInterface(
                            source.readStrongBinder());
            return binder == null ? null : new ChooserSession(binder);
        }

        @Override
        public ChooserSession[] newArray(int size) {
            return new ChooserSession[size];
        }
    };

    /**
     * A callback interface for Chooser session state updates.
     */
    public interface ChooserSessionUpdateListener {

        /**
         * Gets invoked when a {@link ChooserController} becomes available.
         * @param session a reference this callback is registered to.
         * @param chooserController active chooser controller.
         */
        void onChooserConnected(ChooserSession session, ChooserController chooserController);

        /**
         * Gets invoked when a {@link ChooserController} becomes unavailable.
         */
        void onChooserDisconnected(ChooserSession session);

        /**
         * Gets invoked when the session is closed by the Chooser.
         */
        void onSessionClosed(ChooserSession session);

        /**
         * A temporary support method; expected to be replaced by some other WindowManager API.
         */
        void onDrawerVerticalOffsetChanged(ChooserSession session, int offset);
    }

    /**
     * An interface for updating the Chooser.
     */
    public interface ChooserController {

        /**
         * Update chooser intent in a Chooser session.
         */
        // TODO: list all the updatable parameters in the javadoc.
        void updateIntent(Intent intent) throws RemoteException;
    }

    // Just to hide Chooser binder object from the client.
    private static class ChooserControllerWrapper implements ChooserController {
        public final IChooserController controller;

        private ChooserControllerWrapper(IChooserController controller) {
            this.controller = controller;
        }

        @Override
        public void updateIntent(Intent intent) throws RemoteException {
            controller.updateIntent(intent);
        }
    }

    private static class ChooserSessionUpdateListenerWrapper {
        private final ChooserSession mSession;
        private final ChooserSessionUpdateListener mListener;

        ChooserSessionUpdateListenerWrapper(
                ChooserSession mSession, ChooserSessionUpdateListener mListener) {
            this.mSession = mSession;
            this.mListener = mListener;
        }

        public void onChooserConnected(ChooserController chooserController) {
            mListener.onChooserConnected(mSession, chooserController);
        }

        public void onChooserDisconnected() {
            mListener.onChooserDisconnected(mSession);
        }

        public void onSessionClosed() {
            mListener.onSessionClosed(mSession);
        }

        public void onDrawerVerticalOffsetChanged(int offset) {
            mListener.onDrawerVerticalOffsetChanged(mSession, offset);
        }
    }

    private static class ChooserSessionImpl extends IChooserControllerCallback.Stub {
        private final Handler mHandler;
        private volatile ChooserSessionUpdateListenerWrapper mListener;
        private volatile boolean mIsActive = true;
        @Nullable
        private volatile ChooserControllerWrapper mChooserController;
        @Nullable
        private IBinder.DeathRecipient mChooserControllerLinkToDeath;

        ChooserSessionImpl(Handler handler) {
            mHandler = handler;
        }

        @Override
        public void registerChooserController(
                @Nullable final IChooserController chooserController) {
            mHandler.post(() -> setChooserController(chooserController));
        }

        @Override
        public void onDrawerVerticalOffsetChanged(int offset) {
            mHandler.post(() -> notifyDrawerVerticalOffsetChanged(offset));
        }

        public boolean isActive() {
            return mIsActive;
        }

        public void cancel() {
            mIsActive = false;
            mListener = null;
            if (mHandler.getLooper().isCurrentThread()) {
                doClose();
            } else {
                mHandler.post(this::doClose);
            }
        }

        @Nullable
        public ChooserController getChooserController() {
            return mChooserController;
        }

        public void setChooserStateListener(
                @Nullable ChooserSessionUpdateListenerWrapper listener) {
            mListener = listener;
            publishState();
        }

        private void publishState() {
            if (mHandler.getLooper().isCurrentThread()) {
                if (!mIsActive) {
                    notifySessionClosed();
                } else if (mChooserController == null) {
                    notifyChooserDisconnected();
                } else {
                    notifyChooserConnected(mChooserController);
                }
            } else {
                mHandler.post(this::publishState);
            }
        }

        private void doClose() {
            ChooserControllerWrapper controllerWrapper = mChooserController;
            if (controllerWrapper != null) {
                if (mChooserControllerLinkToDeath != null) {
                    safeUnlinkToDeath(
                            controllerWrapper.controller.asBinder(), mChooserControllerLinkToDeath);
                }
                safeUpdateChooserIntent(controllerWrapper.controller, null);
            }
            mChooserController = null;
            mChooserControllerLinkToDeath = null;
        }

        private void setChooserController(IChooserController chooserController) {
            Log.d(
                    TAG,
                    "setIntentUpdater; isOpen: " + mIsActive
                            + ", chooserController: " + chooserController);
            if (!mIsActive && chooserController != null) {
                // close Chooser
                safeUpdateChooserIntent(chooserController, null);
                return;
            }
            ChooserControllerWrapper controllerWrapper = mChooserController;
            if (controllerWrapper != null
                    && areEqual(controllerWrapper.controller, chooserController)) {
                return;
            }

            disconnectCurrentIntentUpdater();

            if (chooserController != null) {
                controllerWrapper = new ChooserControllerWrapper(chooserController);
                this.mChooserController = controllerWrapper;
                mChooserControllerLinkToDeath = createDeathRecipient(chooserController);
                try {
                    chooserController.asBinder().linkToDeath(mChooserControllerLinkToDeath, 0);
                    notifyChooserConnected(controllerWrapper);
                } catch (RemoteException e) {
                    // binder has already died
                    this.mChooserController = null;
                    mChooserControllerLinkToDeath = null;
                }
            } else {
                mIsActive = false;
                notifySessionClosed();
            }
        }

        @MainThread
        private void disconnectCurrentIntentUpdater() {
            ChooserControllerWrapper controllerWrapper = mChooserController;
            if (controllerWrapper != null) {
                if (mChooserControllerLinkToDeath != null) {
                    safeUnlinkToDeath(
                            controllerWrapper.controller.asBinder(), mChooserControllerLinkToDeath);
                }
                mChooserController = null;
                mChooserControllerLinkToDeath = null;
                notifyChooserDisconnected();
            }
        }

        private IBinder.DeathRecipient createDeathRecipient(IChooserController chooserController) {
            return () -> {
                Log.d(TAG, "chooser died");
                mHandler.post(() -> {
                    ChooserControllerWrapper controllerWrapper = this.mChooserController;
                    if (areEqual(
                            controllerWrapper == null ? null : controllerWrapper.controller,
                            chooserController)) {
                        this.mChooserController = null;
                        mChooserControllerLinkToDeath = null;
                        mListener.onChooserDisconnected();
                    }
                });
            };
        }

        private void notifyDrawerVerticalOffsetChanged(int offset) {
            ChooserSessionUpdateListenerWrapper listener = mListener;
            if (listener != null) {
                listener.onDrawerVerticalOffsetChanged(offset);
            }
        }

        private void notifyChooserConnected(ChooserController chooserController) {
            ChooserSessionUpdateListenerWrapper listener = mListener;
            if (listener != null) {
                listener.onChooserConnected(chooserController);
            }
        }

        private void notifySessionClosed() {
            ChooserSessionUpdateListenerWrapper listener = mListener;
            if (listener != null) {
                listener.onSessionClosed();
            }
        }

        private void notifyChooserDisconnected() {
            ChooserSessionUpdateListenerWrapper listener = mListener;
            if (listener != null) {
                listener.onChooserDisconnected();
            }
        }

        private static void safeUpdateChooserIntent(
                IChooserController chooserController, @Nullable Intent chooserIntent) {
            try {
                chooserController.updateIntent(chooserIntent);
            } catch (RemoteException ignored) {
            }
        }

        private static void safeUnlinkToDeath(IBinder binder, IBinder.DeathRecipient linkToDeath) {
            try {
                binder.unlinkToDeath(linkToDeath, 0);
            } catch (Exception ignored) {
            }
        }

        private static boolean areEqual(
                @Nullable IChooserController left, @Nullable IChooserController right) {
            if (left == null && right == null) {
                return true;
            }
            if (left == null || right == null) {
                return false;
            }
            return left.asBinder().equals(right.asBinder());
        }
    }
}
+24 −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 android.service.chooser;

import android.content.Intent;

/** {@hide} */
interface IChooserController {
    oneway void updateIntent(in Intent intent);
}
+25 −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 android.service.chooser;

import android.service.chooser.IChooserController;

/** {@hide} */
interface IChooserControllerCallback {
    oneway void registerChooserController(in IChooserController updater);
    oneway void onDrawerVerticalOffsetChanged(in int offset);
}