1 /*
2  * Copyright (C) 2022 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.google.android.car.kitchensink.media;
18 
19 import android.app.Service;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.ServiceConnection;
24 import android.os.Binder;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.HandlerThread;
28 import android.os.IBinder;
29 import android.os.RemoteException;
30 import android.os.ResultReceiver;
31 import android.os.UserHandle;
32 import android.service.media.IMediaBrowserService;
33 import android.service.media.IMediaBrowserServiceCallbacks;
34 import android.service.media.MediaBrowserService;
35 import android.util.Log;
36 
37 import java.io.FileDescriptor;
38 import java.io.PrintWriter;
39 
40 /**
41  * A proxy to allow a connection from {@link android.media.browse.MediaBrowser} to {@link
42  * android.service.media.MediaBrowserService} across user boundaries.
43  *
44  * <p>The {@code MediaBrowser} client connects to this service as if it is connecting to a
45  * {@code MediaBrowserService}. It needs to pass the actual {@code MediaBrowserService}'s component
46  * name as part the {@code rootHints} bundle, so that this proxy can know what the actual
47  * {@code MediaBrowserService} is to connect to.
48  */
49 public class MediaBrowserProxyService extends Service {
50 
51     private static final String TAG = MediaBrowserProxyService.class.getSimpleName();
52 
53     // The key to specify the actual media browser service in the rootHints bundle.
54     public static final String MEDIA_BROWSER_SERVICE_COMPONENT_KEY =
55             "media_browser_service_component_key";
56 
57     private HandlerThread mNativeHandlerThread;
58     // Handler associated with the native worker thread.
59     private Handler mNativeHandler;
60     private Binder mBinder;
61 
62     private ComponentName mMediaBrowserServiceComponent;
63     private IMediaBrowserService mServiceBinder;
64     private MediaServiceConnection mServiceConnection;
65 
66     private final class MediaBrowserProxyServiceImpl extends IMediaBrowserService.Stub {
67 
68         private final Context mContext;
69 
MediaBrowserProxyServiceImpl(Context context)70         MediaBrowserProxyServiceImpl(Context context) {
71             mContext = context;
72         }
73 
74         @Override
connect(String pkg, Bundle rootHints, IMediaBrowserServiceCallbacks callbacks)75         public void connect(String pkg, Bundle rootHints, IMediaBrowserServiceCallbacks callbacks) {
76             Log.d(TAG, "connect() with pkg=" + pkg + ", rootHints=" + rootHints
77                     + ", callbacks=" + callbacks
78                     + ", callingUserId=" + Binder.getCallingUserHandle().getIdentifier()
79                     + ", myUserId=" + UserHandle.myUserId());
80 
81             // The actual MediaBrowserService component is specified in the bundle.
82             // Retrieve the info here and remove it from the bundle, and then pass the original
83             // rootHints to the actual MediaBrowserService.
84             String mediaBrowserServiceComponent =
85                     rootHints.getString(MEDIA_BROWSER_SERVICE_COMPONENT_KEY);
86             mMediaBrowserServiceComponent =
87                     ComponentName.unflattenFromString(mediaBrowserServiceComponent);
88             rootHints.remove(MEDIA_BROWSER_SERVICE_COMPONENT_KEY);
89             Bundle origRootHints = rootHints.size() == 0 ? null : rootHints;
90 
91             mNativeHandler.post(() -> {
92                 Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
93                 intent.setComponent(mMediaBrowserServiceComponent);
94 
95                 mServiceConnection = new MediaServiceConnection(pkg, origRootHints, callbacks);
96 
97                 boolean bound = false;
98                 try {
99                     bound = mContext.bindService(intent, mServiceConnection,
100                             Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES);
101                 } catch (Exception ex) {
102                     Log.e(TAG, "Failed binding to service " + mMediaBrowserServiceComponent);
103                 }
104 
105                 if (!bound) {
106                     Log.d(TAG, "Cannot bind to MediaBrowserService: "
107                             + mMediaBrowserServiceComponent);
108                     mServiceConnection = null;
109                     try {
110                         callbacks.onConnectFailed();
111                     } catch (RemoteException e) {
112                         Log.e(TAG, "Failed call onConnectionFailed");
113                     }
114                 }
115             });
116         }
117 
118         @Override
disconnect(IMediaBrowserServiceCallbacks callbacks)119         public void disconnect(IMediaBrowserServiceCallbacks callbacks) {
120             Log.d(TAG, "disconnect() for user " + UserHandle.myUserId());
121             mNativeHandler.post(() -> {
122                 try {
123                     mServiceBinder.disconnect(callbacks);
124                 } catch (Exception e) {
125                     Log.e(TAG, "Failed to disconnect MediaBrowserService", e);
126                 }
127             });
128         }
129 
130         @Override
addSubscriptionDeprecated(String uri, IMediaBrowserServiceCallbacks callbacks)131         public void addSubscriptionDeprecated(String uri, IMediaBrowserServiceCallbacks callbacks) {
132             Log.d(TAG, "addSubscriptionDeprecated() for user " + UserHandle.myUserId());
133             mNativeHandler.post(() -> {
134                 try {
135                     mServiceBinder.addSubscriptionDeprecated(uri, callbacks);
136                 } catch (Exception e) {
137                     Log.e(TAG, "Failed to call MediaBrowserService#addSubscriptionDeprecated",
138                             e);
139                 }
140             });
141         }
142 
143         @Override
removeSubscriptionDeprecated(String uri, IMediaBrowserServiceCallbacks callbacks)144         public void removeSubscriptionDeprecated(String uri,
145                 IMediaBrowserServiceCallbacks callbacks) {
146             Log.d(TAG, "removeSubscriptionDeprecated() for user " + UserHandle.myUserId());
147             mNativeHandler.post(() -> {
148                 try {
149                     mServiceBinder.removeSubscriptionDeprecated(uri, callbacks);
150                 } catch (Exception e) {
151                     Log.e(TAG, "Failed to call "
152                             + " MediaBrowserService#removeSubscriptionDeprecated", e);
153                 }
154             });
155         }
156 
157         @Override
getMediaItem(String uri, ResultReceiver cb, IMediaBrowserServiceCallbacks callbacks)158         public void getMediaItem(String uri, ResultReceiver cb,
159                 IMediaBrowserServiceCallbacks callbacks) {
160             Log.d(TAG, "getMediaItem() for user " + UserHandle.myUserId());
161             mNativeHandler.post(() -> {
162                 try {
163                     mServiceBinder.getMediaItem(uri, cb, callbacks);
164                 } catch (Exception e) {
165                     Log.e(TAG, "Failed to call MediaBrowserService#getMediaitem", e);
166                 }
167             });
168         }
169 
170         @Override
addSubscription(String uri, IBinder token, Bundle options, IMediaBrowserServiceCallbacks callbacks)171         public void addSubscription(String uri, IBinder token, Bundle options,
172                 IMediaBrowserServiceCallbacks callbacks) {
173             Log.d(TAG, "addSubscription() for user " + UserHandle.myUserId());
174             mNativeHandler.post(() -> {
175                 try {
176                     mServiceBinder.addSubscription(uri, token, options, callbacks);
177                 } catch (Exception e) {
178                     Log.e(TAG, "Failed to call MediaBrowserService#addSubscription", e);
179                 }
180             });
181         }
182 
183         @Override
removeSubscription(String uri, IBinder token, IMediaBrowserServiceCallbacks callbacks)184         public void removeSubscription(String uri, IBinder token,
185                 IMediaBrowserServiceCallbacks callbacks) {
186             Log.d(TAG, "removeSubscription() for user " + UserHandle.myUserId());
187             mNativeHandler.post(() -> {
188                 try {
189                     mServiceBinder.removeSubscription(uri, token, callbacks);
190                 } catch (Exception e) {
191                     Log.e(TAG, "Failed to call MediaBrowserService#removeSubscription", e);
192                 }
193             });
194         }
195     }
196 
197     @Override
onCreate()198     public void onCreate() {
199         super.onCreate();
200 
201         mNativeHandlerThread = new HandlerThread(MediaBrowserProxyService.class.getSimpleName());
202         mNativeHandlerThread.start();
203         mNativeHandler = new Handler(mNativeHandlerThread.getLooper());
204         Log.d(TAG, "Started a native handler: " + mNativeHandler + " on thread; "
205                 + mNativeHandlerThread);
206 
207         mBinder = new MediaBrowserProxyServiceImpl(this);
208     }
209 
210     @Override
onDestroy()211     public void onDestroy() {
212         mNativeHandlerThread.quit();
213         super.onDestroy();
214     }
215 
216     @Override
onBind(Intent intent)217     public IBinder onBind(Intent intent) {
218         Log.d(TAG, "onBind() intent: " + intent);
219         return mBinder;
220     }
221 
222     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)223     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
224         writer.printf("mNativeHandlerThread: %s\n", mNativeHandlerThread);
225         writer.printf("mNativeHandler: %s\n", mNativeHandler);
226         writer.printf("mBinder: %s\n", mBinder);
227         writer.printf("mMediaBrowserServiceComponent: %s\n", mMediaBrowserServiceComponent);
228         writer.printf("mServiceBinder: %s\n", mServiceBinder);
229         writer.printf("mServiceConnection: %s\n", mServiceConnection);
230     }
231 
232     private final class MediaServiceConnection implements ServiceConnection {
233 
234         private final String mCallerPackage;
235         private final Bundle mRootHints;
236         private final IMediaBrowserServiceCallbacks mCallbacks;
237 
MediaServiceConnection(String pkg, Bundle rootHints, IMediaBrowserServiceCallbacks callbacks)238         MediaServiceConnection(String pkg, Bundle rootHints,
239                 IMediaBrowserServiceCallbacks callbacks) {
240             mCallerPackage = pkg;
241             mRootHints = rootHints;
242             mCallbacks = callbacks;
243         }
244 
245         @Override
onServiceConnected(ComponentName componentName, IBinder iBinder)246         public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
247             Log.d(TAG, "onServiceConnected(ComponentName: " + componentName.toShortString()
248                     + ", Binder: " + iBinder + ")");
249 
250             mServiceBinder = IMediaBrowserService.Stub.asInterface(iBinder);
251 
252             // Connection from Proxy to MediaBrowserService has been established now.
253             // Now calling MediaBrowserService#connect().
254             mNativeHandler.post(() -> {
255                 Log.d(TAG, "Connecting MediaBrowserService " + mServiceBinder);
256                 try {
257                     mServiceBinder.connect(mCallerPackage, mRootHints, mCallbacks);
258                 } catch (Exception e) {
259                     Log.e(TAG, "Failed to connect MediaBrowserBrowserService", e);
260                 }
261             });
262         }
263 
264         @Override
onServiceDisconnected(ComponentName componentName)265         public void onServiceDisconnected(ComponentName componentName) {
266             Log.d(TAG, "onServiceDisconnected(ComponentName: "
267                     + componentName.toShortString() + ")");
268             // No need to call mCallbacks.onConnectFailed() here, because we passed the callbacks
269             // to the actual MediaBrowserService, and the call back will be invoked by it.
270             mServiceBinder = null;
271         }
272     }
273 }
274