• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019 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 com.android.server.media;
18 
19 import static android.media.MediaRoute2ProviderService.REQUEST_ID_NONE;
20 
21 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
22 
23 import android.annotation.NonNull;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.ServiceConnection;
28 import android.media.IMediaRoute2ProviderService;
29 import android.media.IMediaRoute2ProviderServiceCallback;
30 import android.media.MediaRoute2ProviderInfo;
31 import android.media.MediaRoute2ProviderService;
32 import android.media.RouteDiscoveryPreference;
33 import android.media.RoutingSessionInfo;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.IBinder;
37 import android.os.IBinder.DeathRecipient;
38 import android.os.Looper;
39 import android.os.RemoteException;
40 import android.os.UserHandle;
41 import android.text.TextUtils;
42 import android.util.Log;
43 import android.util.Slog;
44 
45 import com.android.internal.annotations.GuardedBy;
46 
47 import java.io.PrintWriter;
48 import java.lang.ref.WeakReference;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Objects;
53 
54 /**
55  * Maintains a connection to a particular {@link MediaRoute2ProviderService}.
56  */
57 final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider
58         implements ServiceConnection {
59     private static final String TAG = "MR2ProviderSvcProxy";
60     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
61 
62     private final Context mContext;
63     private final int mUserId;
64     private final Handler mHandler;
65 
66     // Connection state
67     private boolean mRunning;
68     private boolean mBound;
69     private Connection mActiveConnection;
70     private boolean mConnectionReady;
71 
72     private boolean mIsManagerScanning;
73     private RouteDiscoveryPreference mLastDiscoveryPreference = null;
74 
75     @GuardedBy("mLock")
76     final List<RoutingSessionInfo> mReleasingSessions = new ArrayList<>();
77 
MediaRoute2ProviderServiceProxy(@onNull Context context, @NonNull ComponentName componentName, int userId)78     MediaRoute2ProviderServiceProxy(@NonNull Context context, @NonNull ComponentName componentName,
79             int userId) {
80         super(componentName);
81         mContext = Objects.requireNonNull(context, "Context must not be null.");
82         mUserId = userId;
83         mHandler = new Handler(Looper.myLooper());
84     }
85 
dump(PrintWriter pw, String prefix)86     public void dump(PrintWriter pw, String prefix) {
87         pw.println(prefix + "Proxy");
88         pw.println(prefix + "  mUserId=" + mUserId);
89         pw.println(prefix + "  mRunning=" + mRunning);
90         pw.println(prefix + "  mBound=" + mBound);
91         pw.println(prefix + "  mActiveConnection=" + mActiveConnection);
92         pw.println(prefix + "  mConnectionReady=" + mConnectionReady);
93     }
94 
setManagerScanning(boolean managerScanning)95     public void setManagerScanning(boolean managerScanning) {
96         if (mIsManagerScanning != managerScanning) {
97             mIsManagerScanning = managerScanning;
98             updateBinding();
99         }
100     }
101 
102     @Override
requestCreateSession(long requestId, String packageName, String routeId, Bundle sessionHints)103     public void requestCreateSession(long requestId, String packageName, String routeId,
104             Bundle sessionHints) {
105         if (mConnectionReady) {
106             mActiveConnection.requestCreateSession(requestId, packageName, routeId, sessionHints);
107             updateBinding();
108         }
109     }
110 
111     @Override
releaseSession(long requestId, String sessionId)112     public void releaseSession(long requestId, String sessionId) {
113         if (mConnectionReady) {
114             mActiveConnection.releaseSession(requestId, sessionId);
115             updateBinding();
116         }
117     }
118 
119     @Override
updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference)120     public void updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference) {
121         mLastDiscoveryPreference = discoveryPreference;
122         if (mConnectionReady) {
123             mActiveConnection.updateDiscoveryPreference(discoveryPreference);
124         }
125         updateBinding();
126     }
127 
128     @Override
selectRoute(long requestId, String sessionId, String routeId)129     public void selectRoute(long requestId, String sessionId, String routeId) {
130         if (mConnectionReady) {
131             mActiveConnection.selectRoute(requestId, sessionId, routeId);
132         }
133     }
134 
135     @Override
deselectRoute(long requestId, String sessionId, String routeId)136     public void deselectRoute(long requestId, String sessionId, String routeId) {
137         if (mConnectionReady) {
138             mActiveConnection.deselectRoute(requestId, sessionId, routeId);
139         }
140     }
141 
142     @Override
transferToRoute(long requestId, String sessionId, String routeId)143     public void transferToRoute(long requestId, String sessionId, String routeId) {
144         if (mConnectionReady) {
145             mActiveConnection.transferToRoute(requestId, sessionId, routeId);
146         }
147     }
148 
149     @Override
setRouteVolume(long requestId, String routeId, int volume)150     public void setRouteVolume(long requestId, String routeId, int volume) {
151         if (mConnectionReady) {
152             mActiveConnection.setRouteVolume(requestId, routeId, volume);
153             updateBinding();
154         }
155     }
156 
157     @Override
setSessionVolume(long requestId, String sessionId, int volume)158     public void setSessionVolume(long requestId, String sessionId, int volume) {
159         if (mConnectionReady) {
160             mActiveConnection.setSessionVolume(requestId, sessionId, volume);
161             updateBinding();
162         }
163     }
164 
165     @Override
prepareReleaseSession(@onNull String sessionId)166     public void prepareReleaseSession(@NonNull String sessionId) {
167         synchronized (mLock) {
168             for (RoutingSessionInfo session : mSessionInfos) {
169                 if (TextUtils.equals(session.getId(), sessionId)) {
170                     mSessionInfos.remove(session);
171                     mReleasingSessions.add(session);
172                     break;
173                 }
174             }
175         }
176     }
177 
hasComponentName(String packageName, String className)178     public boolean hasComponentName(String packageName, String className) {
179         return mComponentName.getPackageName().equals(packageName)
180                 && mComponentName.getClassName().equals(className);
181     }
182 
start()183     public void start() {
184         if (!mRunning) {
185             if (DEBUG) {
186                 Slog.d(TAG, this + ": Starting");
187             }
188             mRunning = true;
189             updateBinding();
190         }
191     }
192 
stop()193     public void stop() {
194         if (mRunning) {
195             if (DEBUG) {
196                 Slog.d(TAG, this + ": Stopping");
197             }
198             mRunning = false;
199             updateBinding();
200         }
201     }
202 
rebindIfDisconnected()203     public void rebindIfDisconnected() {
204         //TODO: When we are connecting to the service, calling this will unbind and bind again.
205         // We'd better not unbind if we are connecting.
206         if (mActiveConnection == null && shouldBind()) {
207             unbind();
208             bind();
209         }
210     }
211 
updateBinding()212     private void updateBinding() {
213         if (shouldBind()) {
214             bind();
215         } else {
216             unbind();
217         }
218     }
219 
shouldBind()220     private boolean shouldBind() {
221         if (mRunning) {
222             // Bind when there is a discovery preference or an active route session.
223             return (mLastDiscoveryPreference != null
224                     && !mLastDiscoveryPreference.getPreferredFeatures().isEmpty())
225                     || !getSessionInfos().isEmpty()
226                     || mIsManagerScanning;
227         }
228         return false;
229     }
230 
bind()231     private void bind() {
232         if (!mBound) {
233             if (DEBUG) {
234                 Slog.d(TAG, this + ": Binding");
235             }
236 
237             Intent service = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
238             service.setComponent(mComponentName);
239             try {
240                 mBound = mContext.bindServiceAsUser(service, this,
241                         Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
242                         new UserHandle(mUserId));
243                 if (!mBound && DEBUG) {
244                     Slog.d(TAG, this + ": Bind failed");
245                 }
246             } catch (SecurityException ex) {
247                 if (DEBUG) {
248                     Slog.d(TAG, this + ": Bind failed", ex);
249                 }
250             }
251         }
252     }
253 
unbind()254     private void unbind() {
255         if (mBound) {
256             if (DEBUG) {
257                 Slog.d(TAG, this + ": Unbinding");
258             }
259 
260             mBound = false;
261             disconnect();
262             mContext.unbindService(this);
263         }
264     }
265 
266     @Override
onServiceConnected(ComponentName name, IBinder service)267     public void onServiceConnected(ComponentName name, IBinder service) {
268         if (DEBUG) {
269             Slog.d(TAG, this + ": Connected");
270         }
271 
272         if (mBound) {
273             disconnect();
274             IMediaRoute2ProviderService serviceBinder =
275                     IMediaRoute2ProviderService.Stub.asInterface(service);
276             if (serviceBinder != null) {
277                 Connection connection = new Connection(serviceBinder);
278                 if (connection.register()) {
279                     mActiveConnection = connection;
280                 } else {
281                     if (DEBUG) {
282                         Slog.d(TAG, this + ": Registration failed");
283                     }
284                 }
285             } else {
286                 Slog.e(TAG, this + ": Service returned invalid binder");
287             }
288         }
289     }
290 
291     @Override
onServiceDisconnected(ComponentName name)292     public void onServiceDisconnected(ComponentName name) {
293         if (DEBUG) {
294             Slog.d(TAG, this + ": Service disconnected");
295         }
296         disconnect();
297     }
298 
299     @Override
onBindingDied(ComponentName name)300     public void onBindingDied(ComponentName name) {
301         if (DEBUG) {
302             Slog.d(TAG, this + ": Service binding died");
303         }
304         unbind();
305         if (shouldBind()) {
306             bind();
307         }
308     }
309 
onConnectionReady(Connection connection)310     private void onConnectionReady(Connection connection) {
311         if (mActiveConnection == connection) {
312             mConnectionReady = true;
313             if (mLastDiscoveryPreference != null) {
314                 updateDiscoveryPreference(mLastDiscoveryPreference);
315             }
316         }
317     }
318 
onConnectionDied(Connection connection)319     private void onConnectionDied(Connection connection) {
320         if (mActiveConnection == connection) {
321             if (DEBUG) {
322                 Slog.d(TAG, this + ": Service connection died");
323             }
324             disconnect();
325         }
326     }
327 
onProviderUpdated(Connection connection, MediaRoute2ProviderInfo providerInfo)328     private void onProviderUpdated(Connection connection, MediaRoute2ProviderInfo providerInfo) {
329         if (mActiveConnection != connection) {
330             return;
331         }
332         if (DEBUG) {
333             Slog.d(TAG, this + ": updated");
334         }
335         setAndNotifyProviderState(providerInfo);
336     }
337 
onSessionCreated(Connection connection, long requestId, RoutingSessionInfo newSession)338     private void onSessionCreated(Connection connection, long requestId,
339             RoutingSessionInfo newSession) {
340         if (mActiveConnection != connection) {
341             return;
342         }
343 
344         if (newSession == null) {
345             Slog.w(TAG, "onSessionCreated: Ignoring null session sent from " + mComponentName);
346             return;
347         }
348 
349         newSession = assignProviderIdForSession(newSession);
350         String newSessionId = newSession.getId();
351 
352         synchronized (mLock) {
353             if (mSessionInfos.stream()
354                     .anyMatch(session -> TextUtils.equals(session.getId(), newSessionId))
355                     || mReleasingSessions.stream()
356                     .anyMatch(session -> TextUtils.equals(session.getId(), newSessionId))) {
357                 Slog.w(TAG, "onSessionCreated: Duplicate session already exists. Ignoring.");
358                 return;
359             }
360             mSessionInfos.add(newSession);
361         }
362 
363         mCallback.onSessionCreated(this, requestId, newSession);
364     }
365 
findSessionByIdLocked(RoutingSessionInfo session)366     private int findSessionByIdLocked(RoutingSessionInfo session) {
367         for (int i = 0; i < mSessionInfos.size(); i++) {
368             if (TextUtils.equals(mSessionInfos.get(i).getId(), session.getId())) {
369                 return i;
370             }
371         }
372         return -1;
373     }
374 
375 
onSessionsUpdated(Connection connection, List<RoutingSessionInfo> sessions)376     private void onSessionsUpdated(Connection connection, List<RoutingSessionInfo> sessions) {
377         if (mActiveConnection != connection) {
378             return;
379         }
380 
381         int targetIndex = 0;
382         synchronized (mLock) {
383             for (RoutingSessionInfo session : sessions) {
384                 if (session == null) continue;
385                 session = assignProviderIdForSession(session);
386 
387                 int sourceIndex = findSessionByIdLocked(session);
388                 if (sourceIndex < 0) {
389                     mSessionInfos.add(targetIndex++, session);
390                     dispatchSessionCreated(REQUEST_ID_NONE, session);
391                 } else if (sourceIndex < targetIndex) {
392                     Slog.w(TAG, "Ignoring duplicate session ID: " + session.getId());
393                 } else {
394                     mSessionInfos.set(sourceIndex, session);
395                     Collections.swap(mSessionInfos, sourceIndex, targetIndex++);
396                     dispatchSessionUpdated(session);
397                 }
398             }
399             for (int i = mSessionInfos.size() - 1; i >= targetIndex; i--) {
400                 RoutingSessionInfo releasedSession = mSessionInfos.remove(i);
401                 dispatchSessionReleased(releasedSession);
402             }
403         }
404     }
405 
onSessionReleased(Connection connection, RoutingSessionInfo releaedSession)406     private void onSessionReleased(Connection connection, RoutingSessionInfo releaedSession) {
407         if (mActiveConnection != connection) {
408             return;
409         }
410         if (releaedSession == null) {
411             Slog.w(TAG, "onSessionReleased: Ignoring null session sent from " + mComponentName);
412             return;
413         }
414 
415         releaedSession = assignProviderIdForSession(releaedSession);
416 
417         boolean found = false;
418         synchronized (mLock) {
419             for (RoutingSessionInfo session : mSessionInfos) {
420                 if (TextUtils.equals(session.getId(), releaedSession.getId())) {
421                     mSessionInfos.remove(session);
422                     found = true;
423                     break;
424                 }
425             }
426             if (!found) {
427                 for (RoutingSessionInfo session : mReleasingSessions) {
428                     if (TextUtils.equals(session.getId(), releaedSession.getId())) {
429                         mReleasingSessions.remove(session);
430                         return;
431                     }
432                 }
433             }
434         }
435 
436         if (!found) {
437             Slog.w(TAG, "onSessionReleased: Matching session info not found");
438             return;
439         }
440 
441         mCallback.onSessionReleased(this, releaedSession);
442     }
443 
dispatchSessionCreated(long requestId, RoutingSessionInfo session)444     private void dispatchSessionCreated(long requestId, RoutingSessionInfo session) {
445         mHandler.sendMessage(
446                 obtainMessage(mCallback::onSessionCreated, this, requestId, session));
447     }
448 
dispatchSessionUpdated(RoutingSessionInfo session)449     private void dispatchSessionUpdated(RoutingSessionInfo session) {
450         mHandler.sendMessage(
451                 obtainMessage(mCallback::onSessionUpdated, this, session));
452     }
453 
dispatchSessionReleased(RoutingSessionInfo session)454     private void dispatchSessionReleased(RoutingSessionInfo session) {
455         mHandler.sendMessage(
456                 obtainMessage(mCallback::onSessionReleased, this, session));
457     }
458 
assignProviderIdForSession(RoutingSessionInfo sessionInfo)459     private RoutingSessionInfo assignProviderIdForSession(RoutingSessionInfo sessionInfo) {
460         return new RoutingSessionInfo.Builder(sessionInfo)
461                 .setOwnerPackageName(mComponentName.getPackageName())
462                 .setProviderId(getUniqueId())
463                 .build();
464     }
465 
onRequestFailed(Connection connection, long requestId, int reason)466     private void onRequestFailed(Connection connection, long requestId, int reason) {
467         if (mActiveConnection != connection) {
468             return;
469         }
470 
471         if (requestId == REQUEST_ID_NONE) {
472             Slog.w(TAG, "onRequestFailed: Ignoring requestId REQUEST_ID_NONE");
473             return;
474         }
475 
476         mCallback.onRequestFailed(this, requestId, reason);
477     }
478 
disconnect()479     private void disconnect() {
480         if (mActiveConnection != null) {
481             mConnectionReady = false;
482             mActiveConnection.dispose();
483             mActiveConnection = null;
484             setAndNotifyProviderState(null);
485             synchronized (mLock) {
486                 for (RoutingSessionInfo sessionInfo : mSessionInfos) {
487                     mCallback.onSessionReleased(this, sessionInfo);
488                 }
489                 mSessionInfos.clear();
490                 mReleasingSessions.clear();
491             }
492         }
493     }
494 
495     @Override
toString()496     public String toString() {
497         return "Service connection " + mComponentName.flattenToShortString();
498     }
499 
500     private final class Connection implements DeathRecipient {
501         private final IMediaRoute2ProviderService mService;
502         private final ServiceCallbackStub mCallbackStub;
503 
Connection(IMediaRoute2ProviderService serviceBinder)504         Connection(IMediaRoute2ProviderService serviceBinder) {
505             mService = serviceBinder;
506             mCallbackStub = new ServiceCallbackStub(this);
507         }
508 
register()509         public boolean register() {
510             try {
511                 mService.asBinder().linkToDeath(this, 0);
512                 mService.setCallback(mCallbackStub);
513                 mHandler.post(() -> onConnectionReady(Connection.this));
514                 return true;
515             } catch (RemoteException ex) {
516                 binderDied();
517             }
518             return false;
519         }
520 
dispose()521         public void dispose() {
522             mService.asBinder().unlinkToDeath(this, 0);
523             mCallbackStub.dispose();
524         }
525 
requestCreateSession(long requestId, String packageName, String routeId, Bundle sessionHints)526         public void requestCreateSession(long requestId, String packageName, String routeId,
527                 Bundle sessionHints) {
528             try {
529                 mService.requestCreateSession(requestId, packageName, routeId, sessionHints);
530             } catch (RemoteException ex) {
531                 Slog.e(TAG, "requestCreateSession: Failed to deliver request.");
532             }
533         }
534 
releaseSession(long requestId, String sessionId)535         public void releaseSession(long requestId, String sessionId) {
536             try {
537                 mService.releaseSession(requestId, sessionId);
538             } catch (RemoteException ex) {
539                 Slog.e(TAG, "releaseSession: Failed to deliver request.");
540             }
541         }
542 
updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference)543         public void updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference) {
544             try {
545                 mService.updateDiscoveryPreference(discoveryPreference);
546             } catch (RemoteException ex) {
547                 Slog.e(TAG, "updateDiscoveryPreference: Failed to deliver request.");
548             }
549         }
550 
selectRoute(long requestId, String sessionId, String routeId)551         public void selectRoute(long requestId, String sessionId, String routeId) {
552             try {
553                 mService.selectRoute(requestId, sessionId, routeId);
554             } catch (RemoteException ex) {
555                 Slog.e(TAG, "selectRoute: Failed to deliver request.", ex);
556             }
557         }
558 
deselectRoute(long requestId, String sessionId, String routeId)559         public void deselectRoute(long requestId, String sessionId, String routeId) {
560             try {
561                 mService.deselectRoute(requestId, sessionId, routeId);
562             } catch (RemoteException ex) {
563                 Slog.e(TAG, "deselectRoute: Failed to deliver request.", ex);
564             }
565         }
566 
transferToRoute(long requestId, String sessionId, String routeId)567         public void transferToRoute(long requestId, String sessionId, String routeId) {
568             try {
569                 mService.transferToRoute(requestId, sessionId, routeId);
570             } catch (RemoteException ex) {
571                 Slog.e(TAG, "transferToRoute: Failed to deliver request.", ex);
572             }
573         }
574 
setRouteVolume(long requestId, String routeId, int volume)575         public void setRouteVolume(long requestId, String routeId, int volume) {
576             try {
577                 mService.setRouteVolume(requestId, routeId, volume);
578             } catch (RemoteException ex) {
579                 Slog.e(TAG, "setRouteVolume: Failed to deliver request.", ex);
580             }
581         }
582 
setSessionVolume(long requestId, String sessionId, int volume)583         public void setSessionVolume(long requestId, String sessionId, int volume) {
584             try {
585                 mService.setSessionVolume(requestId, sessionId, volume);
586             } catch (RemoteException ex) {
587                 Slog.e(TAG, "setSessionVolume: Failed to deliver request.", ex);
588             }
589         }
590 
591         @Override
binderDied()592         public void binderDied() {
593             mHandler.post(() -> onConnectionDied(Connection.this));
594         }
595 
postProviderUpdated(MediaRoute2ProviderInfo providerInfo)596         void postProviderUpdated(MediaRoute2ProviderInfo providerInfo) {
597             mHandler.post(() -> onProviderUpdated(Connection.this, providerInfo));
598         }
599 
postSessionCreated(long requestId, RoutingSessionInfo sessionInfo)600         void postSessionCreated(long requestId, RoutingSessionInfo sessionInfo) {
601             mHandler.post(() -> onSessionCreated(Connection.this, requestId, sessionInfo));
602         }
603 
postSessionsUpdated(List<RoutingSessionInfo> sessionInfo)604         void postSessionsUpdated(List<RoutingSessionInfo> sessionInfo) {
605             mHandler.post(() -> onSessionsUpdated(Connection.this, sessionInfo));
606         }
607 
postSessionReleased(RoutingSessionInfo sessionInfo)608         void postSessionReleased(RoutingSessionInfo sessionInfo) {
609             mHandler.post(() -> onSessionReleased(Connection.this, sessionInfo));
610         }
611 
postRequestFailed(long requestId, int reason)612         void postRequestFailed(long requestId, int reason) {
613             mHandler.post(() -> onRequestFailed(Connection.this, requestId, reason));
614         }
615     }
616 
617     private static final class ServiceCallbackStub extends
618             IMediaRoute2ProviderServiceCallback.Stub {
619         private final WeakReference<Connection> mConnectionRef;
620 
ServiceCallbackStub(Connection connection)621         ServiceCallbackStub(Connection connection) {
622             mConnectionRef = new WeakReference<>(connection);
623         }
624 
dispose()625         public void dispose() {
626             mConnectionRef.clear();
627         }
628 
629         @Override
notifyProviderUpdated(MediaRoute2ProviderInfo providerInfo)630         public void notifyProviderUpdated(MediaRoute2ProviderInfo providerInfo) {
631             Connection connection = mConnectionRef.get();
632             if (connection != null) {
633                 connection.postProviderUpdated(providerInfo);
634             }
635         }
636 
637         @Override
notifySessionCreated(long requestId, RoutingSessionInfo sessionInfo)638         public void notifySessionCreated(long requestId, RoutingSessionInfo sessionInfo) {
639             Connection connection = mConnectionRef.get();
640             if (connection != null) {
641                 connection.postSessionCreated(requestId, sessionInfo);
642             }
643         }
644 
645         @Override
notifySessionsUpdated(List<RoutingSessionInfo> sessionInfo)646         public void notifySessionsUpdated(List<RoutingSessionInfo> sessionInfo) {
647             Connection connection = mConnectionRef.get();
648             if (connection != null) {
649                 connection.postSessionsUpdated(sessionInfo);
650             }
651         }
652 
653         @Override
notifySessionReleased(RoutingSessionInfo sessionInfo)654         public void notifySessionReleased(RoutingSessionInfo sessionInfo) {
655             Connection connection = mConnectionRef.get();
656             if (connection != null) {
657                 connection.postSessionReleased(sessionInfo);
658             }
659         }
660 
661         @Override
notifyRequestFailed(long requestId, int reason)662         public void notifyRequestFailed(long requestId, int reason) {
663             Connection connection = mConnectionRef.get();
664             if (connection != null) {
665                 connection.postRequestFailed(requestId, reason);
666             }
667         }
668     }
669 }
670