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