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