• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.service.chooser;
18 
19 import android.content.Intent;
20 import android.os.Handler;
21 import android.os.IBinder;
22 import android.os.IBinder.DeathRecipient;
23 import android.os.Looper;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.os.RemoteException;
27 import android.util.Log;
28 
29 import androidx.annotation.MainThread;
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 
33 import com.android.intentresolver.IChooserController;
34 import com.android.intentresolver.IChooserInteractiveSessionCallback;
35 
36 /**
37  * <p>An interactive Chooser session API candidate.</p>
38  * <p>A class that represents an interactive Chooser session.</p>
39  * <p>An instance of the class should be put in an argument to a chooser intent.
40  * <p>A {@link ChooserSessionUpdateListener} callback can be used to receive updates about the
41  * session and communication from Chooser.</p>
42  */
43 public final class ChooserSession implements Parcelable {
44 
45     private static final String TAG = "ChooserSession";
46 
47     private final IChooserInteractiveSessionCallback mSessionCallbackBinder;
48 
49     // mChooserSession is expected to be null only on the Chooser side
50     @Nullable
51     private final ChooserSessionImpl mChooserSession;
52 
53     /**
54      * An alias for {@code ChooserSession(Looper.getMainLooper())}.
55      */
ChooserSession()56     public ChooserSession() {
57         this(new Handler(Looper.getMainLooper()));
58     }
59 
60     /**
61      * @param handler a thread {@link ChooserSessionUpdateListener} callbacks will be delivered on.
62      */
ChooserSession(Handler handler)63     public ChooserSession(Handler handler) {
64         this(new ChooserSessionImpl(handler));
65     }
66 
ChooserSession(IChooserInteractiveSessionCallback sessionBinder)67     private ChooserSession(IChooserInteractiveSessionCallback sessionBinder) {
68         mSessionCallbackBinder = sessionBinder;
69         mChooserSession = (sessionBinder instanceof ChooserSessionImpl)
70                 ? (ChooserSessionImpl) sessionBinder
71                 : null;
72     }
73 
74     /**
75      * @return true if the session is active: i.e. is not being cancelled by the client
76      * (see {@link #cancel()}) or closed by the Chooser.
77      */
isActive()78     public boolean isActive() {
79         return mChooserSession != null && mChooserSession.isActive();
80     }
81 
82     /**
83      * Cancel the session and close the Chooser.
84      */
cancel()85     public void cancel() {
86         if (mChooserSession != null) {
87             mChooserSession.cancel();
88         }
89     }
90 
91     /**
92      * Should be a @Hidden API for Chooser to get access to the binder.
93      */
getSessionCallbackBinder()94     IChooserInteractiveSessionCallback getSessionCallbackBinder() {
95         return mSessionCallbackBinder;
96     }
97 
98     /**
99      * <p>Get the active {@link ChooserController} or {@code null} if none is available.</p>
100      * A chooser controller becomes available after the Chooser has registered it and stays
101      * available while the session is active and the Chooser process is alive. It is possible for a
102      * session to remain active without a Chooser process. For example, this could happen when the
103      * client launches another activity on top of the Chooser session and the system reclaims the
104      * new backgrounded chooser process. In such example, upon navigating back to the session, a
105      * restored Chooser should register a new {@link ChooserController}.
106      */
107     @Nullable
getChooserController()108     public ChooserController getChooserController() {
109         return mChooserSession == null ? null : mChooserSession.getChooserController();
110     }
111 
112     /**
113      * @param listener make sure that the callback is cleared at the end of a component's lifecycle
114      * (e.g. Activity) or provide a properly maintained WeakReference wrapper to avoid memory leaks.
115      */
setChooserStateListener(@ullable ChooserSessionUpdateListener listener)116     public void setChooserStateListener(@Nullable ChooserSessionUpdateListener listener) {
117         if (mChooserSession != null) {
118             mChooserSession.setChooserStateListener(
119                     listener == null
120                             ? null
121                             : new ChooserSessionUpdateListenerWrapper(this, listener));
122         }
123     }
124 
125     @Override
describeContents()126     public int describeContents() {
127         return 0;
128     }
129 
130     @Override
writeToParcel(@onNull Parcel dest, int flags)131     public void writeToParcel(@NonNull Parcel dest, int flags) {
132         if (mChooserSession != null) {
133             synchronized (mChooserSession) {
134                 dest.writeStrongBinder(mChooserSession);
135             }
136         }
137     }
138 
139     public static final Parcelable.Creator<ChooserSession> CREATOR = new Creator<>() {
140                 @Override
141                 public ChooserSession createFromParcel(Parcel source) {
142                     IChooserInteractiveSessionCallback binder =
143                             IChooserInteractiveSessionCallback.Stub.asInterface(
144                                     source.readStrongBinder());
145                     return binder == null ? null : new ChooserSession(binder);
146                 }
147 
148                 @Override
149                 public ChooserSession[] newArray(int size) {
150                     return new ChooserSession[size];
151                 }
152             };
153 
154     /**
155      * A callback interface for Chooser session state updates.
156      */
157     public interface ChooserSessionUpdateListener {
158 
159         /**
160          * Gets invoked when a {@link ChooserController} becomes available.
161          * @param session a reference this callback is registered to.
162          * @param chooserController active chooser controller.
163          */
onChooserConnected(ChooserSession session, ChooserController chooserController)164         void onChooserConnected(ChooserSession session, ChooserController chooserController);
165 
166         /**
167          * Gets invoked when a {@link ChooserController} becomes unavailable.
168          */
onChooserDisconnected(ChooserSession session)169         void onChooserDisconnected(ChooserSession session);
170 
171         /**
172          * Gets invoked when the session is closed by the Chooser.
173          */
onSessionClosed(ChooserSession session)174         void onSessionClosed(ChooserSession session);
175 
176         /**
177          * A temporary support method; expected to be replaced by some other WindowManager API.
178          */
onDrawerVerticalOffsetChanged(ChooserSession session, int offset)179         void onDrawerVerticalOffsetChanged(ChooserSession session, int offset);
180     }
181 
182     /**
183      * An interface for updating the Chooser.
184      */
185     public interface ChooserController {
186 
187         /**
188          * Update chooser intent in a Chooser session.
189          */
190         // TODO: list all the updatable parameters in the javadoc.
updateIntent(Intent intent)191         void updateIntent(Intent intent) throws RemoteException;
192     }
193 
194     // Just to hide Chooser binder object from the client.
195     private static class ChooserControllerWrapper implements ChooserController {
196         public final IChooserController controller;
197 
ChooserControllerWrapper(IChooserController controller)198         private ChooserControllerWrapper(IChooserController controller) {
199             this.controller = controller;
200         }
201 
202         @Override
updateIntent(Intent intent)203         public void updateIntent(Intent intent) throws RemoteException {
204             controller.updateIntent(intent);
205         }
206     }
207 
208     private static class ChooserSessionUpdateListenerWrapper {
209         private final ChooserSession mSession;
210         private final ChooserSessionUpdateListener mListener;
211 
ChooserSessionUpdateListenerWrapper( ChooserSession mSession, ChooserSessionUpdateListener mListener)212         ChooserSessionUpdateListenerWrapper(
213                 ChooserSession mSession, ChooserSessionUpdateListener mListener) {
214             this.mSession = mSession;
215             this.mListener = mListener;
216         }
217 
onChooserConnected(ChooserController chooserController)218         public void onChooserConnected(ChooserController chooserController) {
219             mListener.onChooserConnected(mSession, chooserController);
220         }
221 
onChooserDisconnected()222         public void onChooserDisconnected() {
223             mListener.onChooserDisconnected(mSession);
224         }
225 
onSessionClosed()226         public void onSessionClosed() {
227             mListener.onSessionClosed(mSession);
228         }
229 
onDrawerVerticalOffsetChanged(int offset)230         public void onDrawerVerticalOffsetChanged(int offset) {
231             mListener.onDrawerVerticalOffsetChanged(mSession, offset);
232         }
233     }
234 
235     private static class ChooserSessionImpl extends IChooserInteractiveSessionCallback.Stub {
236         private final Handler mHandler;
237         private volatile ChooserSessionUpdateListenerWrapper mListener;
238         private volatile boolean mIsActive = true;
239         @Nullable
240         private volatile ChooserControllerWrapper mChooserController;
241         @Nullable
242         private IBinder.DeathRecipient mChooserControllerLinkToDeath;
243 
ChooserSessionImpl(Handler handler)244         ChooserSessionImpl(Handler handler) {
245             mHandler = handler;
246         }
247 
248         @Override
registerChooserController( @ullable final IChooserController chooserController)249         public void registerChooserController(
250                 @Nullable final IChooserController chooserController) {
251             mHandler.post(() -> setChooserController(chooserController));
252         }
253 
254         @Override
onDrawerVerticalOffsetChanged(int offset)255         public void onDrawerVerticalOffsetChanged(int offset) {
256             mHandler.post(() -> notifyDrawerVerticalOffsetChanged(offset));
257         }
258 
isActive()259         public boolean isActive() {
260             return mIsActive;
261         }
262 
cancel()263         public void cancel() {
264             mIsActive = false;
265             mListener = null;
266             if (mHandler.getLooper().isCurrentThread()) {
267                 doClose();
268             } else {
269                 mHandler.post(this::doClose);
270             }
271         }
272 
273         @Nullable
getChooserController()274         public ChooserController getChooserController() {
275             return mChooserController;
276         }
277 
setChooserStateListener( @ullable ChooserSessionUpdateListenerWrapper listener)278         public void setChooserStateListener(
279                 @Nullable ChooserSessionUpdateListenerWrapper listener) {
280             mListener = listener;
281             publishState();
282         }
283 
publishState()284         private void publishState() {
285             if (mHandler.getLooper().isCurrentThread()) {
286                 if (!mIsActive) {
287                     notifySessionClosed();
288                 } else if (mChooserController == null) {
289                     notifyChooserDisconnected();
290                 } else {
291                     notifyChooserConnected(mChooserController);
292                 }
293             } else {
294                 mHandler.post(this::publishState);
295             }
296         }
297 
doClose()298         private void doClose() {
299             ChooserControllerWrapper controllerWrapper = mChooserController;
300             if (controllerWrapper != null) {
301                 if (mChooserControllerLinkToDeath != null) {
302                     safeUnlinkToDeath(
303                             controllerWrapper.controller.asBinder(), mChooserControllerLinkToDeath);
304                 }
305                 safeUpdateChooserIntent(controllerWrapper.controller, null);
306             }
307             mChooserController = null;
308             mChooserControllerLinkToDeath = null;
309         }
310 
setChooserController(IChooserController chooserController)311         private void setChooserController(IChooserController chooserController) {
312             Log.d(
313                     TAG,
314                     "setIntentUpdater; isOpen: " + mIsActive
315                             + ", chooserController: " + chooserController);
316             if (!mIsActive && chooserController != null) {
317                 // close Chooser
318                 safeUpdateChooserIntent(chooserController, null);
319                 return;
320             }
321             ChooserControllerWrapper controllerWrapper = mChooserController;
322             if (controllerWrapper != null
323                     && areEqual(controllerWrapper.controller, chooserController)) {
324                 return;
325             }
326 
327             disconnectCurrentIntentUpdater();
328 
329             if (chooserController != null) {
330                 controllerWrapper = new ChooserControllerWrapper(chooserController);
331                 this.mChooserController = controllerWrapper;
332                 mChooserControllerLinkToDeath = createDeathRecipient(chooserController);
333                 try {
334                     chooserController.asBinder().linkToDeath(mChooserControllerLinkToDeath, 0);
335                     notifyChooserConnected(controllerWrapper);
336                 } catch (RemoteException e) {
337                     // binder has already died
338                     this.mChooserController = null;
339                     mChooserControllerLinkToDeath = null;
340                 }
341             } else {
342                 mIsActive = false;
343                 notifySessionClosed();
344             }
345         }
346 
347         @MainThread
disconnectCurrentIntentUpdater()348         private void disconnectCurrentIntentUpdater() {
349             ChooserControllerWrapper controllerWrapper = mChooserController;
350             if (controllerWrapper != null) {
351                 if (mChooserControllerLinkToDeath != null) {
352                     safeUnlinkToDeath(
353                             controllerWrapper.controller.asBinder(), mChooserControllerLinkToDeath);
354                 }
355                 mChooserController = null;
356                 mChooserControllerLinkToDeath = null;
357                 notifyChooserDisconnected();
358             }
359         }
360 
createDeathRecipient(IChooserController chooserController)361         private DeathRecipient createDeathRecipient(IChooserController chooserController) {
362             return () -> {
363                 Log.d(TAG, "chooser died");
364                 mHandler.post(() -> {
365                     ChooserControllerWrapper controllerWrapper = this.mChooserController;
366                     if (areEqual(
367                             controllerWrapper == null ? null : controllerWrapper.controller,
368                             chooserController)) {
369                         this.mChooserController = null;
370                         mChooserControllerLinkToDeath = null;
371                         mListener.onChooserDisconnected();
372                     }
373                 });
374             };
375         }
376 
notifyDrawerVerticalOffsetChanged(int offset)377         private void notifyDrawerVerticalOffsetChanged(int offset) {
378             ChooserSessionUpdateListenerWrapper listener = mListener;
379             if (listener != null) {
380                 listener.onDrawerVerticalOffsetChanged(offset);
381             }
382         }
383 
notifyChooserConnected(ChooserController chooserController)384         private void notifyChooserConnected(ChooserController chooserController) {
385             ChooserSessionUpdateListenerWrapper listener = mListener;
386             if (listener != null) {
387                 listener.onChooserConnected(chooserController);
388             }
389         }
390 
notifySessionClosed()391         private void notifySessionClosed() {
392             ChooserSessionUpdateListenerWrapper listener = mListener;
393             if (listener != null) {
394                 listener.onSessionClosed();
395             }
396         }
397 
notifyChooserDisconnected()398         private void notifyChooserDisconnected() {
399             ChooserSessionUpdateListenerWrapper listener = mListener;
400             if (listener != null) {
401                 listener.onChooserDisconnected();
402             }
403         }
404 
safeUpdateChooserIntent( IChooserController chooserController, @Nullable Intent chooserIntent)405         private static void safeUpdateChooserIntent(
406                 IChooserController chooserController, @Nullable Intent chooserIntent) {
407             try {
408                 chooserController.updateIntent(chooserIntent);
409             } catch (RemoteException ignored) {
410             }
411         }
412 
safeUnlinkToDeath(IBinder binder, IBinder.DeathRecipient linkToDeath)413         private static void safeUnlinkToDeath(IBinder binder, IBinder.DeathRecipient linkToDeath) {
414             try {
415                 binder.unlinkToDeath(linkToDeath, 0);
416             } catch (Exception ignored) {
417             }
418         }
419 
areEqual( @ullable IChooserController left, @Nullable IChooserController right)420         private static boolean areEqual(
421                 @Nullable IChooserController left, @Nullable IChooserController right) {
422             if (left == null && right == null) {
423                 return true;
424             }
425             if (left == null || right == null) {
426                 return false;
427             }
428             return left.asBinder().equals(right.asBinder());
429         }
430     }
431 }
432