blob: cd465d22d3312c0c133682aa3e7a80b9a1858de3 [file] [log] [blame]
/*
* 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<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 */
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<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
* {@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.
}
}
}