1 /*
2  * Copyright 2018 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.media;
18 
19 import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS;
20 import static android.media.MediaConstants.KEY_CONNECTION_HINTS;
21 import static android.media.MediaConstants.KEY_PACKAGE_NAME;
22 import static android.media.MediaConstants.KEY_PID;
23 import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
24 import static android.media.MediaConstants.KEY_SESSION2LINK;
25 import static android.media.MediaConstants.KEY_TOKEN_EXTRAS;
26 import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR;
27 import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED;
28 import static android.media.Session2Token.TYPE_SESSION;
29 
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.content.ComponentName;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.ServiceConnection;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.os.IBinder;
39 import android.os.Process;
40 import android.os.RemoteException;
41 import android.os.ResultReceiver;
42 import android.util.ArrayMap;
43 import android.util.ArraySet;
44 import android.util.Log;
45 
46 import java.util.concurrent.Executor;
47 
48 /**
49  * This API is not generally intended for third party application developers.
50  * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
51  * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
52  * Library</a> for consistent behavior across all devices.
53  *
54  * Allows an app to interact with an active {@link MediaSession2} or a
55  * {@link MediaSession2Service} which would provide {@link MediaSession2}. Media buttons and other
56  * commands can be sent to the session.
57  */
58 public class MediaController2 implements AutoCloseable {
59     static final String TAG = "MediaController2";
60     static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
61 
62     @SuppressWarnings("WeakerAccess") /* synthetic access */
63     final ControllerCallback mCallback;
64 
65     private final IBinder.DeathRecipient mDeathRecipient = () -> close();
66     private final Context mContext;
67     private final Session2Token mSessionToken;
68     private final Executor mCallbackExecutor;
69     private final Controller2Link mControllerStub;
70     private final Handler mResultHandler;
71     private final SessionServiceConnection mServiceConnection;
72 
73     private final Object mLock = new Object();
74     //@GuardedBy("mLock")
75     private boolean mClosed;
76     //@GuardedBy("mLock")
77     private int mNextSeqNumber;
78     //@GuardedBy("mLock")
79     private Session2Link mSessionBinder;
80     //@GuardedBy("mLock")
81     private Session2CommandGroup mAllowedCommands;
82     //@GuardedBy("mLock")
83     private Session2Token mConnectedToken;
84     //@GuardedBy("mLock")
85     private ArrayMap<ResultReceiver, Integer> mPendingCommands;
86     //@GuardedBy("mLock")
87     private ArraySet<Integer> mRequestedCommandSeqNumbers;
88     //@GuardedBy("mLock")
89     private boolean mPlaybackActive;
90 
91     /**
92      * Create a {@link MediaController2} from the {@link Session2Token}.
93      * This connects to the session and may wake up the service if it's not available.
94      *
95      * @param context context
96      * @param token token to connect to
97      * @param connectionHints a session-specific argument to send to the session when connecting.
98      *                        The contents of this bundle may affect the connection result.
99      * @param executor executor to run callbacks on.
100      * @param callback controller callback to receive changes in.
101      */
MediaController2(@onNull Context context, @NonNull Session2Token token, @NonNull Bundle connectionHints, @NonNull Executor executor, @NonNull ControllerCallback callback)102     MediaController2(@NonNull Context context, @NonNull Session2Token token,
103             @NonNull Bundle connectionHints, @NonNull Executor executor,
104             @NonNull ControllerCallback callback) {
105         if (context == null) {
106             throw new IllegalArgumentException("context shouldn't be null");
107         }
108         if (token == null) {
109             throw new IllegalArgumentException("token shouldn't be null");
110         }
111         mContext = context;
112         mSessionToken = token;
113         mCallbackExecutor = (executor == null) ? context.getMainExecutor() : executor;
114         mCallback = (callback == null) ? new ControllerCallback() {} : callback;
115         mControllerStub = new Controller2Link(this);
116         // NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
117         mResultHandler = new Handler(context.getMainLooper());
118 
119         mNextSeqNumber = 0;
120         mPendingCommands = new ArrayMap<>();
121         mRequestedCommandSeqNumbers = new ArraySet<>();
122 
123         boolean connectRequested;
124         if (token.getType() == TYPE_SESSION) {
125             mServiceConnection = null;
126             connectRequested = requestConnectToSession(connectionHints);
127         } else {
128             mServiceConnection = new SessionServiceConnection(connectionHints);
129             connectRequested = requestConnectToService();
130         }
131         if (!connectRequested) {
132             close();
133         }
134     }
135 
136     @Override
close()137     public void close() {
138         synchronized (mLock) {
139             if (mClosed) {
140                 // Already closed. Ignore rest of clean up code.
141                 // Note: unbindService() throws IllegalArgumentException when it's called twice.
142                 return;
143             }
144             if (DEBUG) {
145                 Log.d(TAG, "closing " + this);
146             }
147             mClosed = true;
148             if (mServiceConnection != null) {
149                 // Note: This should be called even when the bindService() has returned false.
150                 mContext.unbindService(mServiceConnection);
151             }
152             if (mSessionBinder != null) {
153                 try {
154                     mSessionBinder.disconnect(mControllerStub, getNextSeqNumber());
155                     mSessionBinder.unlinkToDeath(mDeathRecipient, 0);
156                 } catch (RuntimeException e) {
157                     // No-op
158                 }
159             }
160             mConnectedToken = null;
161             mPendingCommands.clear();
162             mRequestedCommandSeqNumbers.clear();
163             mCallbackExecutor.execute(() -> {
164                 mCallback.onDisconnected(MediaController2.this);
165             });
166             mSessionBinder = null;
167         }
168     }
169 
170     /**
171      * Returns {@link Session2Token} of the connected session.
172      * If it is not connected yet, it returns {@code null}.
173      * <p>
174      * This may differ with the {@link Session2Token} from the constructor. For example, if the
175      * controller is created with the token for {@link MediaSession2Service}, this would return
176      * token for the {@link MediaSession2} in the service.
177      *
178      * @return Session2Token of the connected session, or {@code null} if not connected
179      */
180     @Nullable
getConnectedToken()181     public Session2Token getConnectedToken() {
182         synchronized (mLock) {
183             return mConnectedToken;
184         }
185     }
186 
187     /**
188      * Returns whether the session's playback is active.
189      *
190      * @return {@code true} if playback active. {@code false} otherwise.
191      * @see ControllerCallback#onPlaybackActiveChanged(MediaController2, boolean)
192      */
isPlaybackActive()193     public boolean isPlaybackActive() {
194         synchronized (mLock) {
195             return mPlaybackActive;
196         }
197     }
198 
199     /**
200      * Sends a session command to the session
201      * <p>
202      * @param command the session command
203      * @param args optional arguments
204      * @return a token which will be sent together in {@link ControllerCallback#onCommandResult}
205      *        when its result is received.
206      */
207     @NonNull
sendSessionCommand(@onNull Session2Command command, @Nullable Bundle args)208     public Object sendSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) {
209         if (command == null) {
210             throw new IllegalArgumentException("command shouldn't be null");
211         }
212 
213         ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) {
214             protected void onReceiveResult(int resultCode, Bundle resultData) {
215                 synchronized (mLock) {
216                     mPendingCommands.remove(this);
217                 }
218                 mCallbackExecutor.execute(() -> {
219                     mCallback.onCommandResult(MediaController2.this, this,
220                             command, new Session2Command.Result(resultCode, resultData));
221                 });
222             }
223         };
224 
225         synchronized (mLock) {
226             if (mSessionBinder != null) {
227                 int seq = getNextSeqNumber();
228                 mPendingCommands.put(resultReceiver, seq);
229                 try {
230                     mSessionBinder.sendSessionCommand(mControllerStub, seq, command, args,
231                             resultReceiver);
232                 } catch (RuntimeException e)  {
233                     mPendingCommands.remove(resultReceiver);
234                     resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
235                 }
236             }
237         }
238         return resultReceiver;
239     }
240 
241     /**
242      * Cancels the session command previously sent.
243      *
244      * @param token the token which is returned from {@link #sendSessionCommand}.
245      */
cancelSessionCommand(@onNull Object token)246     public void cancelSessionCommand(@NonNull Object token) {
247         if (token == null) {
248             throw new IllegalArgumentException("token shouldn't be null");
249         }
250         synchronized (mLock) {
251             if (mSessionBinder == null) return;
252             Integer seq = mPendingCommands.remove(token);
253             if (seq != null) {
254                 mSessionBinder.cancelSessionCommand(mControllerStub, seq);
255             }
256         }
257     }
258 
259     // Called by Controller2Link.onConnected
onConnected(int seq, Bundle connectionResult)260     void onConnected(int seq, Bundle connectionResult) {
261         Session2Link sessionBinder = connectionResult.getParcelable(KEY_SESSION2LINK);
262         Session2CommandGroup allowedCommands =
263                 connectionResult.getParcelable(KEY_ALLOWED_COMMANDS);
264         boolean playbackActive = connectionResult.getBoolean(KEY_PLAYBACK_ACTIVE);
265 
266         Bundle tokenExtras = connectionResult.getBundle(KEY_TOKEN_EXTRAS);
267         if (tokenExtras == null) {
268             Log.w(TAG, "extras shouldn't be null.");
269             tokenExtras = Bundle.EMPTY;
270         } else if (MediaSession2.hasCustomParcelable(tokenExtras)) {
271             Log.w(TAG, "extras contain custom parcelable. Ignoring.");
272             tokenExtras = Bundle.EMPTY;
273         }
274 
275         if (DEBUG) {
276             Log.d(TAG, "notifyConnected sessionBinder=" + sessionBinder
277                     + ", allowedCommands=" + allowedCommands);
278         }
279         if (sessionBinder == null || allowedCommands == null) {
280             // Connection rejected.
281             close();
282             return;
283         }
284         synchronized (mLock) {
285             mSessionBinder = sessionBinder;
286             mAllowedCommands = allowedCommands;
287             mPlaybackActive = playbackActive;
288 
289             // Implementation for the local binder is no-op,
290             // so can be used without worrying about deadlock.
291             sessionBinder.linkToDeath(mDeathRecipient, 0);
292             mConnectedToken = new Session2Token(mSessionToken.getUid(), TYPE_SESSION,
293                     mSessionToken.getPackageName(), sessionBinder, tokenExtras);
294         }
295         mCallbackExecutor.execute(() -> {
296             mCallback.onConnected(MediaController2.this, allowedCommands);
297         });
298     }
299 
300     // Called by Controller2Link.onDisconnected
onDisconnected(int seq)301     void onDisconnected(int seq) {
302         // close() will call mCallback.onDisconnected
303         close();
304     }
305 
306     // Called by Controller2Link.onPlaybackActiveChanged
onPlaybackActiveChanged(int seq, boolean playbackActive)307     void onPlaybackActiveChanged(int seq, boolean playbackActive) {
308         synchronized (mLock) {
309             mPlaybackActive = playbackActive;
310         }
311         mCallbackExecutor.execute(() -> {
312             mCallback.onPlaybackActiveChanged(MediaController2.this, playbackActive);
313         });
314     }
315 
316     // Called by Controller2Link.onSessionCommand
onSessionCommand(int seq, Session2Command command, Bundle args, @Nullable ResultReceiver resultReceiver)317     void onSessionCommand(int seq, Session2Command command, Bundle args,
318             @Nullable ResultReceiver resultReceiver) {
319         synchronized (mLock) {
320             mRequestedCommandSeqNumbers.add(seq);
321         }
322         mCallbackExecutor.execute(() -> {
323             boolean isCanceled;
324             synchronized (mLock) {
325                 isCanceled = !mRequestedCommandSeqNumbers.remove(seq);
326             }
327             if (isCanceled) {
328                 if (resultReceiver != null) {
329                     resultReceiver.send(RESULT_INFO_SKIPPED, null);
330                 }
331                 return;
332             }
333             Session2Command.Result result = mCallback.onSessionCommand(
334                     MediaController2.this, command, args);
335             if (resultReceiver != null) {
336                 if (result == null) {
337                     resultReceiver.send(RESULT_INFO_SKIPPED, null);
338                 } else {
339                     resultReceiver.send(result.getResultCode(), result.getResultData());
340                 }
341             }
342         });
343     }
344 
345     // Called by Controller2Link.onSessionCommand
onCancelCommand(int seq)346     void onCancelCommand(int seq) {
347         synchronized (mLock) {
348             mRequestedCommandSeqNumbers.remove(seq);
349         }
350     }
351 
getNextSeqNumber()352     private int getNextSeqNumber() {
353         synchronized (mLock) {
354             return mNextSeqNumber++;
355         }
356     }
357 
createConnectionRequest(@onNull Bundle connectionHints)358     private Bundle createConnectionRequest(@NonNull Bundle connectionHints) {
359         Bundle connectionRequest = new Bundle();
360         connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
361         connectionRequest.putInt(KEY_PID, Process.myPid());
362         connectionRequest.putBundle(KEY_CONNECTION_HINTS, connectionHints);
363         return connectionRequest;
364     }
365 
requestConnectToSession(@onNull Bundle connectionHints)366     private boolean requestConnectToSession(@NonNull Bundle connectionHints) {
367         Session2Link sessionBinder = mSessionToken.getSessionLink();
368         Bundle connectionRequest = createConnectionRequest(connectionHints);
369         try {
370             sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
371         } catch (RuntimeException e) {
372             Log.w(TAG, "Failed to call connection request", e);
373             return false;
374         }
375         return true;
376     }
377 
requestConnectToService()378     private boolean requestConnectToService() {
379         // Service. Needs to get fresh binder whenever connection is needed.
380         final Intent intent = new Intent(MediaSession2Service.SERVICE_INTERFACE);
381         intent.setClassName(mSessionToken.getPackageName(), mSessionToken.getServiceName());
382 
383         // Use bindService() instead of startForegroundService() to start session service for three
384         // reasons.
385         // 1. Prevent session service owner's stopSelf() from destroying service.
386         //    With the startForegroundService(), service's call of stopSelf() will trigger immediate
387         //    onDestroy() calls on the main thread even when onConnect() is running in another
388         //    thread.
389         // 2. Minimize APIs for developers to take care about.
390         //    With bindService(), developers only need to take care about Service.onBind()
391         //    but Service.onStartCommand() should be also taken care about with the
392         //    startForegroundService().
393         // 3. Future support for UI-less playback
394         //    If a service wants to keep running, it should be either foreground service or
395         //    bound service. But there had been request for the feature for system apps
396         //    and using bindService() will be better fit with it.
397         synchronized (mLock) {
398             boolean result = mContext.bindService(
399                     intent, mServiceConnection, Context.BIND_AUTO_CREATE);
400             if (!result) {
401                 Log.w(TAG, "bind to " + mSessionToken + " failed");
402                 return false;
403             } else if (DEBUG) {
404                 Log.d(TAG, "bind to " + mSessionToken + " succeeded");
405             }
406         }
407         return true;
408     }
409 
410     /**
411      * This API is not generally intended for third party application developers.
412      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
413      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
414      * Library</a> for consistent behavior across all devices.
415      * <p>
416      * Builder for {@link MediaController2}.
417      * <p>
418      * Any incoming event from the {@link MediaSession2} will be handled on the callback
419      * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default.
420      */
421     public static final class Builder {
422         private Context mContext;
423         private Session2Token mToken;
424         private Bundle mConnectionHints;
425         private Executor mCallbackExecutor;
426         private ControllerCallback mCallback;
427 
428         /**
429          * Creates a builder for {@link MediaController2}.
430          *
431          * @param context context
432          * @param token token of the session to connect to
433          */
Builder(@onNull Context context, @NonNull Session2Token token)434         public Builder(@NonNull Context context, @NonNull Session2Token token) {
435             if (context == null) {
436                 throw new IllegalArgumentException("context shouldn't be null");
437             }
438             if (token == null) {
439                 throw new IllegalArgumentException("token shouldn't be null");
440             }
441             mContext = context;
442             mToken = token;
443         }
444 
445         /**
446          * Set the connection hints for the controller.
447          * <p>
448          * {@code connectionHints} is a session-specific argument to send to the session when
449          * connecting. The contents of this bundle may affect the connection result.
450          * <p>
451          * An {@link IllegalArgumentException} will be thrown if the bundle contains any
452          * non-framework Parcelable objects.
453          *
454          * @param connectionHints a bundle which contains the connection hints
455          * @return The Builder to allow chaining
456          */
457         @NonNull
setConnectionHints(@onNull Bundle connectionHints)458         public Builder setConnectionHints(@NonNull Bundle connectionHints) {
459             if (connectionHints == null) {
460                 throw new IllegalArgumentException("connectionHints shouldn't be null");
461             }
462             if (MediaSession2.hasCustomParcelable(connectionHints)) {
463                 throw new IllegalArgumentException("connectionHints shouldn't contain any custom "
464                         + "parcelables");
465             }
466             mConnectionHints = new Bundle(connectionHints);
467             return this;
468         }
469 
470         /**
471          * Set callback for the controller and its executor.
472          *
473          * @param executor callback executor
474          * @param callback session callback.
475          * @return The Builder to allow chaining
476          */
477         @NonNull
setControllerCallback(@onNull Executor executor, @NonNull ControllerCallback callback)478         public Builder setControllerCallback(@NonNull Executor executor,
479                 @NonNull ControllerCallback callback) {
480             if (executor == null) {
481                 throw new IllegalArgumentException("executor shouldn't be null");
482             }
483             if (callback == null) {
484                 throw new IllegalArgumentException("callback shouldn't be null");
485             }
486             mCallbackExecutor = executor;
487             mCallback = callback;
488             return this;
489         }
490 
491         /**
492          * Build {@link MediaController2}.
493          *
494          * @return a new controller
495          */
496         @NonNull
build()497         public MediaController2 build() {
498             if (mCallbackExecutor == null) {
499                 mCallbackExecutor = mContext.getMainExecutor();
500             }
501             if (mCallback == null) {
502                 mCallback = new ControllerCallback() {};
503             }
504             if (mConnectionHints == null) {
505                 mConnectionHints = Bundle.EMPTY;
506             }
507             return new MediaController2(
508                     mContext, mToken, mConnectionHints, mCallbackExecutor, mCallback);
509         }
510     }
511 
512     /**
513      * This API is not generally intended for third party application developers.
514      * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
515      * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
516      * Library</a> for consistent behavior across all devices.
517      * <p>
518      * Interface for listening to change in activeness of the {@link MediaSession2}.
519      */
520     public abstract static class ControllerCallback {
521         /**
522          * Called when the controller is successfully connected to the session. The controller
523          * becomes available afterwards.
524          *
525          * @param controller the controller for this event
526          * @param allowedCommands commands that's allowed by the session.
527          */
onConnected(@onNull MediaController2 controller, @NonNull Session2CommandGroup allowedCommands)528         public void onConnected(@NonNull MediaController2 controller,
529                 @NonNull Session2CommandGroup allowedCommands) {}
530 
531         /**
532          * Called when the session refuses the controller or the controller is disconnected from
533          * the session. The controller becomes unavailable afterwards and the callback wouldn't
534          * be called.
535          * <p>
536          * It will be also called after the {@link #close()}, so you can put clean up code here.
537          * You don't need to call {@link #close()} after this.
538          *
539          * @param controller the controller for this event
540          */
onDisconnected(@onNull MediaController2 controller)541         public void onDisconnected(@NonNull MediaController2 controller) {}
542 
543         /**
544          * Called when the session's playback activeness is changed.
545          *
546          * @param controller the controller for this event
547          * @param playbackActive {@code true} if the session's playback is active.
548          *                       {@code false} otherwise.
549          * @see MediaController2#isPlaybackActive()
550          */
onPlaybackActiveChanged(@onNull MediaController2 controller, boolean playbackActive)551         public void onPlaybackActiveChanged(@NonNull MediaController2 controller,
552                 boolean playbackActive) {}
553 
554         /**
555          * Called when the connected session sent a session command.
556          *
557          * @param controller the controller for this event
558          * @param command the session command
559          * @param args optional arguments
560          * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
561          *         will be sent to the session.
562          */
563         @Nullable
onSessionCommand(@onNull MediaController2 controller, @NonNull Session2Command command, @Nullable Bundle args)564         public Session2Command.Result onSessionCommand(@NonNull MediaController2 controller,
565                 @NonNull Session2Command command, @Nullable Bundle args) {
566             return null;
567         }
568 
569         /**
570          * Called when the command sent to the connected session is finished.
571          *
572          * @param controller the controller for this event
573          * @param token the token got from {@link MediaController2#sendSessionCommand}
574          * @param command the session command
575          * @param result the result of the session command
576          */
onCommandResult(@onNull MediaController2 controller, @NonNull Object token, @NonNull Session2Command command, @NonNull Session2Command.Result result)577         public void onCommandResult(@NonNull MediaController2 controller, @NonNull Object token,
578                 @NonNull Session2Command command, @NonNull Session2Command.Result result) {}
579     }
580 
581     // This will be called on the main thread.
582     private class SessionServiceConnection implements ServiceConnection {
583         private final Bundle mConnectionHints;
584 
SessionServiceConnection(@ullable Bundle connectionHints)585         SessionServiceConnection(@Nullable Bundle connectionHints) {
586             mConnectionHints = connectionHints;
587         }
588 
589         @Override
onServiceConnected(ComponentName name, IBinder service)590         public void onServiceConnected(ComponentName name, IBinder service) {
591             // Note that it's always main-thread.
592             boolean connectRequested = false;
593             try {
594                 if (DEBUG) {
595                     Log.d(TAG, "onServiceConnected " + name + " " + this);
596                 }
597                 // Sanity check
598                 if (!mSessionToken.getPackageName().equals(name.getPackageName())) {
599                     Log.wtf(TAG, "Expected connection to " + mSessionToken.getPackageName()
600                             + " but is connected to " + name);
601                     return;
602                 }
603                 IMediaSession2Service iService = IMediaSession2Service.Stub.asInterface(service);
604                 if (iService == null) {
605                     Log.wtf(TAG, "Service interface is missing.");
606                     return;
607                 }
608                 Bundle connectionRequest = createConnectionRequest(mConnectionHints);
609                 iService.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
610                 connectRequested = true;
611             } catch (RemoteException e) {
612                 Log.w(TAG, "Service " + name + " has died prematurely", e);
613             } finally {
614                 if (!connectRequested) {
615                     close();
616                 }
617             }
618         }
619 
620         @Override
onServiceDisconnected(ComponentName name)621         public void onServiceDisconnected(ComponentName name) {
622             // Temporal lose of the binding because of the service crash. System will automatically
623             // rebind, so just no-op.
624             if (DEBUG) {
625                 Log.w(TAG, "Session service " + name + " is disconnected.");
626             }
627             close();
628         }
629 
630         @Override
onBindingDied(ComponentName name)631         public void onBindingDied(ComponentName name) {
632             // Permanent lose of the binding because of the service package update or removed.
633             // This SessionServiceRecord will be removed accordingly, but forget session binder here
634             // for sure.
635             close();
636         }
637     }
638 }
639