1 /*
2  * Copyright (C) 2016 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.telephony;
18 
19 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.SdkConstant;
24 import android.annotation.SystemApi;
25 import android.annotation.TestApi;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.ServiceConnection;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.telephony.mbms.InternalStreamingServiceCallback;
32 import android.telephony.mbms.InternalStreamingSessionCallback;
33 import android.telephony.mbms.MbmsErrors;
34 import android.telephony.mbms.MbmsStreamingSessionCallback;
35 import android.telephony.mbms.MbmsUtils;
36 import android.telephony.mbms.StreamingService;
37 import android.telephony.mbms.StreamingServiceCallback;
38 import android.telephony.mbms.StreamingServiceInfo;
39 import android.telephony.mbms.vendor.IMbmsStreamingService;
40 import android.util.ArraySet;
41 import android.util.Log;
42 
43 import java.util.List;
44 import java.util.Set;
45 import java.util.concurrent.Executor;
46 import java.util.concurrent.atomic.AtomicBoolean;
47 import java.util.concurrent.atomic.AtomicReference;
48 
49 /**
50  * This class provides functionality for streaming media over MBMS.
51  */
52 public class MbmsStreamingSession implements AutoCloseable {
53     private static final String LOG_TAG = "MbmsStreamingSession";
54 
55     /**
56      * Service action which must be handled by the middleware implementing the MBMS streaming
57      * interface.
58      * @hide
59      */
60     @SystemApi
61     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
62     public static final String MBMS_STREAMING_SERVICE_ACTION =
63             "android.telephony.action.EmbmsStreaming";
64 
65     /**
66      * Metadata key that specifies the component name of the service to bind to for file-download.
67      * @hide
68      */
69     @TestApi
70     public static final String MBMS_STREAMING_SERVICE_OVERRIDE_METADATA =
71             "mbms-streaming-service-override";
72 
73     private static AtomicBoolean sIsInitialized = new AtomicBoolean(false);
74 
75     private AtomicReference<IMbmsStreamingService> mService = new AtomicReference<>(null);
76     private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
77         @Override
78         public void binderDied() {
79             sIsInitialized.set(false);
80             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification");
81         }
82     };
83 
84     private InternalStreamingSessionCallback mInternalCallback;
85     private ServiceConnection mServiceConnection;
86     private Set<StreamingService> mKnownActiveStreamingServices = new ArraySet<>();
87 
88     private final Context mContext;
89     private int mSubscriptionId = INVALID_SUBSCRIPTION_ID;
90 
91     /** @hide */
MbmsStreamingSession(Context context, Executor executor, int subscriptionId, MbmsStreamingSessionCallback callback)92     private MbmsStreamingSession(Context context, Executor executor, int subscriptionId,
93             MbmsStreamingSessionCallback callback) {
94         mContext = context;
95         mSubscriptionId = subscriptionId;
96         mInternalCallback = new InternalStreamingSessionCallback(callback, executor);
97     }
98 
99     /**
100      * Create a new {@link MbmsStreamingSession} using the given subscription ID.
101      *
102      * Note that this call will bind a remote service. You may not call this method on your app's
103      * main thread.
104      *
105      * You may only have one instance of {@link MbmsStreamingSession} per UID. If you call this
106      * method while there is an active instance of {@link MbmsStreamingSession} in your process
107      * (in other words, one that has not had {@link #close()} called on it), this method will
108      * throw an {@link IllegalStateException}. If you call this method in a different process
109      * running under the same UID, an error will be indicated via
110      * {@link MbmsStreamingSessionCallback#onError(int, String)}.
111      *
112      * Note that initialization may fail asynchronously. If you wish to try again after you
113      * receive such an asynchronous error, you must call {@link #close()} on the instance of
114      * {@link MbmsStreamingSession} that you received before calling this method again.
115      *
116      * @param context The {@link Context} to use.
117      * @param executor The executor on which you wish to execute callbacks.
118      * @param subscriptionId The subscription ID to use.
119      * @param callback A callback object on which you wish to receive results of asynchronous
120      *                 operations.
121      * @return An instance of {@link MbmsStreamingSession}, or null if an error occurred.
122      */
create(@onNull Context context, @NonNull Executor executor, int subscriptionId, final @NonNull MbmsStreamingSessionCallback callback)123     public static @Nullable MbmsStreamingSession create(@NonNull Context context,
124             @NonNull Executor executor, int subscriptionId,
125             final @NonNull MbmsStreamingSessionCallback callback) {
126         if (!sIsInitialized.compareAndSet(false, true)) {
127             throw new IllegalStateException("Cannot create two instances of MbmsStreamingSession");
128         }
129         MbmsStreamingSession session = new MbmsStreamingSession(context, executor,
130                 subscriptionId, callback);
131 
132         final int result = session.bindAndInitialize();
133         if (result != MbmsErrors.SUCCESS) {
134             sIsInitialized.set(false);
135             executor.execute(new Runnable() {
136                 @Override
137                 public void run() {
138                     callback.onError(result, null);
139                 }
140             });
141             return null;
142         }
143         return session;
144     }
145 
146     /**
147      * Create a new {@link MbmsStreamingSession} using the system default data subscription ID.
148      * See {@link #create(Context, Executor, int, MbmsStreamingSessionCallback)}.
149      */
create(@onNull Context context, @NonNull Executor executor, @NonNull MbmsStreamingSessionCallback callback)150     public static MbmsStreamingSession create(@NonNull Context context,
151             @NonNull Executor executor, @NonNull MbmsStreamingSessionCallback callback) {
152         return create(context, executor, SubscriptionManager.getDefaultSubscriptionId(), callback);
153     }
154 
155     /**
156      * Terminates this instance. Also terminates
157      * any streaming services spawned from this instance as if
158      * {@link StreamingService#close()} had been called on them. After this method returns,
159      * no further callbacks originating from the middleware will be enqueued on the provided
160      * instance of {@link MbmsStreamingSessionCallback}, but callbacks that have already been
161      * enqueued will still be delivered.
162      *
163      * It is safe to call {@link #create(Context, Executor, int, MbmsStreamingSessionCallback)} to
164      * obtain another instance of {@link MbmsStreamingSession} immediately after this method
165      * returns.
166      *
167      * May throw an {@link IllegalStateException}
168      */
close()169     public void close() {
170         try {
171             IMbmsStreamingService streamingService = mService.get();
172             if (streamingService == null || mServiceConnection == null) {
173                 // Ignore and return, assume already disposed.
174                 return;
175             }
176             streamingService.dispose(mSubscriptionId);
177             for (StreamingService s : mKnownActiveStreamingServices) {
178                 s.getCallback().stop();
179             }
180             mKnownActiveStreamingServices.clear();
181             mContext.unbindService(mServiceConnection);
182         } catch (RemoteException e) {
183             // Ignore for now
184         } finally {
185             mService.set(null);
186             sIsInitialized.set(false);
187             mServiceConnection = null;
188             mInternalCallback.stop();
189         }
190     }
191 
192     /**
193      * An inspection API to retrieve the list of streaming media currently be advertised.
194      * The results are returned asynchronously via
195      * {@link MbmsStreamingSessionCallback#onStreamingServicesUpdated(List)} on the callback
196      * provided upon creation.
197      *
198      * Multiple calls replace the list of service classes of interest.
199      *
200      * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}.
201      *
202      * @param serviceClassList A list of streaming service classes that the app would like updates
203      *                         on. The exact names of these classes should be negotiated with the
204      *                         wireless carrier separately.
205      */
requestUpdateStreamingServices(List<String> serviceClassList)206     public void requestUpdateStreamingServices(List<String> serviceClassList) {
207         IMbmsStreamingService streamingService = mService.get();
208         if (streamingService == null) {
209             throw new IllegalStateException("Middleware not yet bound");
210         }
211         try {
212             int returnCode = streamingService.requestUpdateStreamingServices(
213                     mSubscriptionId, serviceClassList);
214             if (returnCode == MbmsErrors.UNKNOWN) {
215                 // Unbind and throw an obvious error
216                 close();
217                 throw new IllegalStateException("Middleware must not return an unknown error code");
218             }
219             if (returnCode != MbmsErrors.SUCCESS) {
220                 sendErrorToApp(returnCode, null);
221             }
222         } catch (RemoteException e) {
223             Log.w(LOG_TAG, "Remote process died");
224             mService.set(null);
225             sIsInitialized.set(false);
226             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
227         }
228     }
229 
230     /**
231      * Starts streaming a requested service, reporting status to the indicated callback.
232      * Returns an object used to control that stream. The stream may not be ready for consumption
233      * immediately upon return from this method -- wait until the streaming state has been
234      * reported via
235      * {@link android.telephony.mbms.StreamingServiceCallback#onStreamStateUpdated(int, int)}
236      *
237      * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}
238      *
239      * Asynchronous errors through the callback include any of the errors in
240      * {@link MbmsErrors.GeneralErrors} or
241      * {@link MbmsErrors.StreamingErrors}.
242      *
243      * @param serviceInfo The information about the service to stream.
244      * @param executor The executor on which you wish to execute callbacks for this stream.
245      * @param callback A callback that'll be called when something about the stream changes.
246      * @return An instance of {@link StreamingService} through which the stream can be controlled.
247      *         May be {@code null} if an error occurred.
248      */
startStreaming(StreamingServiceInfo serviceInfo, @NonNull Executor executor, StreamingServiceCallback callback)249     public @Nullable StreamingService startStreaming(StreamingServiceInfo serviceInfo,
250             @NonNull Executor executor, StreamingServiceCallback callback) {
251         IMbmsStreamingService streamingService = mService.get();
252         if (streamingService == null) {
253             throw new IllegalStateException("Middleware not yet bound");
254         }
255 
256         InternalStreamingServiceCallback serviceCallback = new InternalStreamingServiceCallback(
257                 callback, executor);
258 
259         StreamingService serviceForApp = new StreamingService(
260                 mSubscriptionId, streamingService, this, serviceInfo, serviceCallback);
261         mKnownActiveStreamingServices.add(serviceForApp);
262 
263         try {
264             int returnCode = streamingService.startStreaming(
265                     mSubscriptionId, serviceInfo.getServiceId(), serviceCallback);
266             if (returnCode == MbmsErrors.UNKNOWN) {
267                 // Unbind and throw an obvious error
268                 close();
269                 throw new IllegalStateException("Middleware must not return an unknown error code");
270             }
271             if (returnCode != MbmsErrors.SUCCESS) {
272                 sendErrorToApp(returnCode, null);
273                 return null;
274             }
275         } catch (RemoteException e) {
276             Log.w(LOG_TAG, "Remote process died");
277             mService.set(null);
278             sIsInitialized.set(false);
279             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
280             return null;
281         }
282 
283         return serviceForApp;
284     }
285 
286     /** @hide */
onStreamingServiceStopped(StreamingService service)287     public void onStreamingServiceStopped(StreamingService service) {
288         mKnownActiveStreamingServices.remove(service);
289     }
290 
bindAndInitialize()291     private int bindAndInitialize() {
292         mServiceConnection = new ServiceConnection() {
293             @Override
294             public void onServiceConnected(ComponentName name, IBinder service) {
295                 IMbmsStreamingService streamingService =
296                         IMbmsStreamingService.Stub.asInterface(service);
297                 int result;
298                 try {
299                     result = streamingService.initialize(mInternalCallback,
300                             mSubscriptionId);
301                 } catch (RemoteException e) {
302                     Log.e(LOG_TAG, "Service died before initialization");
303                     sendErrorToApp(
304                             MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE,
305                             e.toString());
306                     sIsInitialized.set(false);
307                     return;
308                 } catch (RuntimeException e) {
309                     Log.e(LOG_TAG, "Runtime exception during initialization");
310                     sendErrorToApp(
311                             MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE,
312                             e.toString());
313                     sIsInitialized.set(false);
314                     return;
315                 }
316                 if (result == MbmsErrors.UNKNOWN) {
317                     // Unbind and throw an obvious error
318                     close();
319                     throw new IllegalStateException("Middleware must not return"
320                             + " an unknown error code");
321                 }
322                 if (result != MbmsErrors.SUCCESS) {
323                     sendErrorToApp(result, "Error returned during initialization");
324                     sIsInitialized.set(false);
325                     return;
326                 }
327                 try {
328                     streamingService.asBinder().linkToDeath(mDeathRecipient, 0);
329                 } catch (RemoteException e) {
330                     sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST,
331                             "Middleware lost during initialization");
332                     sIsInitialized.set(false);
333                     return;
334                 }
335                 mService.set(streamingService);
336             }
337 
338             @Override
339             public void onServiceDisconnected(ComponentName name) {
340                 sIsInitialized.set(false);
341                 mService.set(null);
342             }
343 
344             @Override
345             public void onNullBinding(ComponentName name) {
346                 Log.w(LOG_TAG, "bindAndInitialize: Remote service returned null");
347                 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST,
348                         "Middleware service binding returned null");
349                 sIsInitialized.set(false);
350                 mService.set(null);
351                 mContext.unbindService(this);
352             }
353         };
354         return MbmsUtils.startBinding(mContext, MBMS_STREAMING_SERVICE_ACTION, mServiceConnection);
355     }
356 
sendErrorToApp(int errorCode, String message)357     private void sendErrorToApp(int errorCode, String message) {
358         try {
359             mInternalCallback.onError(errorCode, message);
360         } catch (RemoteException e) {
361             // Ignore, should not happen locally.
362         }
363     }
364 }
365