/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.telephony; import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; import android.telephony.mbms.InternalStreamingServiceCallback; import android.telephony.mbms.InternalStreamingSessionCallback; import android.telephony.mbms.MbmsErrors; import android.telephony.mbms.MbmsStreamingSessionCallback; import android.telephony.mbms.MbmsUtils; import android.telephony.mbms.StreamingService; import android.telephony.mbms.StreamingServiceCallback; import android.telephony.mbms.StreamingServiceInfo; import android.telephony.mbms.vendor.IMbmsStreamingService; import android.util.ArraySet; import android.util.Log; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * This class provides functionality for streaming media over MBMS. */ public class MbmsStreamingSession implements AutoCloseable { private static final String LOG_TAG = "MbmsStreamingSession"; /** * Service action which must be handled by the middleware implementing the MBMS streaming * interface. * @hide */ @SystemApi @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String MBMS_STREAMING_SERVICE_ACTION = "android.telephony.action.EmbmsStreaming"; /** * Metadata key that specifies the component name of the service to bind to for file-download. * @hide */ @TestApi public static final String MBMS_STREAMING_SERVICE_OVERRIDE_METADATA = "mbms-streaming-service-override"; private static AtomicBoolean sIsInitialized = new AtomicBoolean(false); private AtomicReference mService = new AtomicReference<>(null); private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification"); } }; private InternalStreamingSessionCallback mInternalCallback; private Set mKnownActiveStreamingServices = new ArraySet<>(); private final Context mContext; private int mSubscriptionId = INVALID_SUBSCRIPTION_ID; /** @hide */ private MbmsStreamingSession(Context context, Executor executor, int subscriptionId, MbmsStreamingSessionCallback callback) { mContext = context; mSubscriptionId = subscriptionId; mInternalCallback = new InternalStreamingSessionCallback(callback, executor); } /** * Create a new {@link MbmsStreamingSession} using the given subscription ID. * * Note that this call will bind a remote service. You may not call this method on your app's * main thread. * * You may only have one instance of {@link MbmsStreamingSession} per UID. If you call this * method while there is an active instance of {@link MbmsStreamingSession} in your process * (in other words, one that has not had {@link #close()} called on it), this method will * throw an {@link IllegalStateException}. If you call this method in a different process * running under the same UID, an error will be indicated via * {@link MbmsStreamingSessionCallback#onError(int, String)}. * * Note that initialization may fail asynchronously. If you wish to try again after you * receive such an asynchronous error, you must call {@link #close()} on the instance of * {@link MbmsStreamingSession} that you received before calling this method again. * * @param context The {@link Context} to use. * @param executor The executor on which you wish to execute callbacks. * @param subscriptionId The subscription ID to use. * @param callback A callback object on which you wish to receive results of asynchronous * operations. * @return An instance of {@link MbmsStreamingSession}, or null if an error occurred. */ public static @Nullable MbmsStreamingSession create(@NonNull Context context, @NonNull Executor executor, int subscriptionId, final @NonNull MbmsStreamingSessionCallback callback) { if (!sIsInitialized.compareAndSet(false, true)) { throw new IllegalStateException("Cannot create two instances of MbmsStreamingSession"); } MbmsStreamingSession session = new MbmsStreamingSession(context, executor, subscriptionId, callback); final int result = session.bindAndInitialize(); if (result != MbmsErrors.SUCCESS) { sIsInitialized.set(false); executor.execute(new Runnable() { @Override public void run() { callback.onError(result, null); } }); return null; } return session; } /** * Create a new {@link MbmsStreamingSession} using the system default data subscription ID. * See {@link #create(Context, Executor, int, MbmsStreamingSessionCallback)}. */ public static MbmsStreamingSession create(@NonNull Context context, @NonNull Executor executor, @NonNull MbmsStreamingSessionCallback callback) { return create(context, executor, SubscriptionManager.getDefaultSubscriptionId(), callback); } /** * Terminates this instance. Also terminates * any streaming services spawned from this instance as if * {@link StreamingService#close()} had been called on them. After this method returns, * no further callbacks originating from the middleware will be enqueued on the provided * instance of {@link MbmsStreamingSessionCallback}, but callbacks that have already been * enqueued will still be delivered. * * It is safe to call {@link #create(Context, Executor, int, MbmsStreamingSessionCallback)} to * obtain another instance of {@link MbmsStreamingSession} immediately after this method * returns. * * May throw an {@link IllegalStateException} */ public void close() { try { IMbmsStreamingService streamingService = mService.get(); if (streamingService == null) { // Ignore and return, assume already disposed. return; } streamingService.dispose(mSubscriptionId); for (StreamingService s : mKnownActiveStreamingServices) { s.getCallback().stop(); } mKnownActiveStreamingServices.clear(); } catch (RemoteException e) { // Ignore for now } finally { mService.set(null); sIsInitialized.set(false); mInternalCallback.stop(); } } /** * An inspection API to retrieve the list of streaming media currently be advertised. * The results are returned asynchronously via * {@link MbmsStreamingSessionCallback#onStreamingServicesUpdated(List)} on the callback * provided upon creation. * * Multiple calls replace the list of service classes of interest. * * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}. * * @param serviceClassList A list of streaming service classes that the app would like updates * on. The exact names of these classes should be negotiated with the * wireless carrier separately. */ public void requestUpdateStreamingServices(List serviceClassList) { IMbmsStreamingService streamingService = mService.get(); if (streamingService == null) { throw new IllegalStateException("Middleware not yet bound"); } try { int returnCode = streamingService.requestUpdateStreamingServices( mSubscriptionId, serviceClassList); if (returnCode == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (returnCode != MbmsErrors.SUCCESS) { sendErrorToApp(returnCode, null); } } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } } /** * Starts streaming a requested service, reporting status to the indicated callback. * Returns an object used to control that stream. The stream may not be ready for consumption * immediately upon return from this method -- wait until the streaming state has been * reported via * {@link android.telephony.mbms.StreamingServiceCallback#onStreamStateUpdated(int, int)} * * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException} * * Asynchronous errors through the callback include any of the errors in * {@link MbmsErrors.GeneralErrors} or * {@link MbmsErrors.StreamingErrors}. * * @param serviceInfo The information about the service to stream. * @param executor The executor on which you wish to execute callbacks for this stream. * @param callback A callback that'll be called when something about the stream changes. * @return An instance of {@link StreamingService} through which the stream can be controlled. * May be {@code null} if an error occurred. */ public @Nullable StreamingService startStreaming(StreamingServiceInfo serviceInfo, @NonNull Executor executor, StreamingServiceCallback callback) { IMbmsStreamingService streamingService = mService.get(); if (streamingService == null) { throw new IllegalStateException("Middleware not yet bound"); } InternalStreamingServiceCallback serviceCallback = new InternalStreamingServiceCallback( callback, executor); StreamingService serviceForApp = new StreamingService( mSubscriptionId, streamingService, this, serviceInfo, serviceCallback); mKnownActiveStreamingServices.add(serviceForApp); try { int returnCode = streamingService.startStreaming( mSubscriptionId, serviceInfo.getServiceId(), serviceCallback); if (returnCode == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (returnCode != MbmsErrors.SUCCESS) { sendErrorToApp(returnCode, null); return null; } } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return null; } return serviceForApp; } /** @hide */ public void onStreamingServiceStopped(StreamingService service) { mKnownActiveStreamingServices.remove(service); } private int bindAndInitialize() { return MbmsUtils.startBinding(mContext, MBMS_STREAMING_SERVICE_ACTION, new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IMbmsStreamingService streamingService = IMbmsStreamingService.Stub.asInterface(service); int result; try { result = streamingService.initialize(mInternalCallback, mSubscriptionId); } catch (RemoteException e) { Log.e(LOG_TAG, "Service died before initialization"); sendErrorToApp( MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, e.toString()); sIsInitialized.set(false); return; } catch (RuntimeException e) { Log.e(LOG_TAG, "Runtime exception during initialization"); sendErrorToApp( MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, e.toString()); sIsInitialized.set(false); return; } if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return" + " an unknown error code"); } if (result != MbmsErrors.SUCCESS) { sendErrorToApp(result, "Error returned during initialization"); sIsInitialized.set(false); return; } try { streamingService.asBinder().linkToDeath(mDeathRecipient, 0); } catch (RemoteException e) { sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Middleware lost during initialization"); sIsInitialized.set(false); return; } mService.set(streamingService); } @Override public void onServiceDisconnected(ComponentName name) { sIsInitialized.set(false); mService.set(null); } }); } private void sendErrorToApp(int errorCode, String message) { try { mInternalCallback.onError(errorCode, message); } catch (RemoteException e) { // Ignore, should not happen locally. } } }