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