/* * 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 android.service.chooser; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.IBinder.DeathRecipient; 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; import com.android.intentresolver.IChooserController; import com.android.intentresolver.IChooserInteractiveSessionCallback; /** *

An interactive Chooser session API candidate.

*

A class that represents an interactive Chooser session.

*

An instance of the class should be put in an argument to a chooser intent. *

A {@link ChooserSessionUpdateListener} callback can be used to receive updates about the * session and communication from Chooser.

*/ public final class ChooserSession implements Parcelable { private static final String TAG = "ChooserSession"; private final IChooserInteractiveSessionCallback 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(IChooserInteractiveSessionCallback 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(); } } /** * Should be a @Hidden API for Chooser to get access to the binder. */ IChooserInteractiveSessionCallback getSessionCallbackBinder() { return mSessionCallbackBinder; } /** *

Get the active {@link ChooserController} or {@code null} if none is available.

* 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 CREATOR = new Creator<>() { @Override public ChooserSession createFromParcel(Parcel source) { IChooserInteractiveSessionCallback binder = IChooserInteractiveSessionCallback.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 IChooserInteractiveSessionCallback.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 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()); } } }