/*
 * 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.
/** * 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
/** * 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
/** * 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<IMbmsStreamingService> 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<StreamingService> mKnownActiveStreamingServices = new ArraySet<>(); private final Context mContext; private int mSubscriptionId = INVALID_SUBSCRIPTION_ID;
@hide
/** @hide */
private MbmsStreamingSession(Context context, Executor executor, int subscriptionId, MbmsStreamingSessionCallback callback) { mContext = context; mSubscriptionId = subscriptionId; mInternalCallback = new InternalStreamingSessionCallback(callback, executor); }
Create a new 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 MbmsStreamingSession per UID. If you call this method while there is an active instance of MbmsStreamingSession in your process (in other words, one that has not had close() called on it), this method will throw an IllegalStateException. If you call this method in a different process running under the same UID, an error will be indicated via 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 close() on the instance of MbmsStreamingSession that you received before calling this method again.
Params:
  • context – The Context to use.
  • executor – The executor on which you wish to execute callbacks.
  • subscriptionId – The subscription ID to use.
  • callback – A callback object on which you wish to receive results of asynchronous operations.
Returns:An instance of MbmsStreamingSession, or null if an error occurred.
/** * 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 MbmsStreamingSession using the system default data subscription ID. See create(Context, Executor, int, MbmsStreamingSessionCallback).
/** * 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 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 MbmsStreamingSessionCallback, but callbacks that have already been enqueued will still be delivered. It is safe to call create(Context, Executor, int, MbmsStreamingSessionCallback) to obtain another instance of MbmsStreamingSession immediately after this method returns. May throw an IllegalStateException
/** * 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 MbmsStreamingSessionCallback.onStreamingServicesUpdated(List<StreamingServiceInfo>) on the callback provided upon creation. Multiple calls replace the list of service classes of interest. May throw an IllegalArgumentException or an IllegalStateException.
Params:
  • 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.
/** * 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<String> 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 StreamingServiceCallback.onStreamStateUpdated(int, int) May throw an IllegalArgumentException or an IllegalStateException Asynchronous errors through the callback include any of the errors in GeneralErrors or StreamingErrors.
Params:
  • serviceInfo – The information about the service to stream.
  • executor – The executor on which you wish to execute callbacks for this stream.
  • callback – A callback that'll be called when something about the stream changes.
Returns:An instance of StreamingService through which the stream can be controlled. May be null if an error occurred.
/** * 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
/** @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. } } }