Merge "EMBMS API tweaks"

This commit is contained in:
Hall Liu
2018-02-22 00:35:51 +00:00
committed by Gerrit Code Review
14 changed files with 384 additions and 248 deletions

View File

@@ -40415,12 +40415,12 @@ package android.telephony {
public class MbmsDownloadSession implements java.lang.AutoCloseable {
method public int cancelDownload(android.telephony.mbms.DownloadRequest);
method public void close();
method public static android.telephony.MbmsDownloadSession create(android.content.Context, android.telephony.mbms.MbmsDownloadSessionCallback, android.os.Handler);
method public static android.telephony.MbmsDownloadSession create(android.content.Context, android.telephony.mbms.MbmsDownloadSessionCallback, int, android.os.Handler);
method public static android.telephony.MbmsDownloadSession create(android.content.Context, java.util.concurrent.Executor, android.telephony.mbms.MbmsDownloadSessionCallback);
method public static android.telephony.MbmsDownloadSession create(android.content.Context, java.util.concurrent.Executor, int, android.telephony.mbms.MbmsDownloadSessionCallback);
method public int download(android.telephony.mbms.DownloadRequest);
method public java.io.File getTempFileRootDirectory();
method public java.util.List<android.telephony.mbms.DownloadRequest> listPendingDownloads();
method public int registerStateCallback(android.telephony.mbms.DownloadRequest, android.telephony.mbms.DownloadStateCallback, android.os.Handler);
method public int registerStateCallback(android.telephony.mbms.DownloadRequest, java.util.concurrent.Executor, android.telephony.mbms.DownloadStateCallback);
method public void requestDownloadState(android.telephony.mbms.DownloadRequest, android.telephony.mbms.FileInfo);
method public void requestUpdateFileServices(java.util.List<java.lang.String>);
method public void resetDownloadKnowledge(android.telephony.mbms.DownloadRequest);
@@ -40448,10 +40448,10 @@ package android.telephony {
public class MbmsStreamingSession implements java.lang.AutoCloseable {
method public void close();
method public static android.telephony.MbmsStreamingSession create(android.content.Context, android.telephony.mbms.MbmsStreamingSessionCallback, int, android.os.Handler);
method public static android.telephony.MbmsStreamingSession create(android.content.Context, android.telephony.mbms.MbmsStreamingSessionCallback, android.os.Handler);
method public static android.telephony.MbmsStreamingSession create(android.content.Context, java.util.concurrent.Executor, int, android.telephony.mbms.MbmsStreamingSessionCallback);
method public static android.telephony.MbmsStreamingSession create(android.content.Context, java.util.concurrent.Executor, android.telephony.mbms.MbmsStreamingSessionCallback);
method public void requestUpdateStreamingServices(java.util.List<java.lang.String>);
method public android.telephony.mbms.StreamingService startStreaming(android.telephony.mbms.StreamingServiceInfo, android.telephony.mbms.StreamingServiceCallback, android.os.Handler);
method public android.telephony.mbms.StreamingService startStreaming(android.telephony.mbms.StreamingServiceInfo, java.util.concurrent.Executor, android.telephony.mbms.StreamingServiceCallback);
}
public class NeighboringCellInfo implements android.os.Parcelable {
@@ -41279,20 +41279,23 @@ package android.telephony.gsm {
package android.telephony.mbms {
public final class DownloadRequest implements android.os.Parcelable {
method public static android.telephony.mbms.DownloadRequest copy(android.telephony.mbms.DownloadRequest);
method public int describeContents();
method public android.net.Uri getDestinationUri();
method public java.lang.String getFileServiceId();
method public static int getMaxAppIntentSize();
method public static int getMaxDestinationUriSize();
method public android.net.Uri getSourceUri();
method public int getSubscriptionId();
method public byte[] toByteArray();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.telephony.mbms.DownloadRequest> CREATOR;
}
public static class DownloadRequest.Builder {
ctor public DownloadRequest.Builder(android.net.Uri);
ctor public DownloadRequest.Builder(android.net.Uri, android.net.Uri);
method public android.telephony.mbms.DownloadRequest build();
method public static android.telephony.mbms.DownloadRequest.Builder fromDownloadRequest(android.telephony.mbms.DownloadRequest);
method public static android.telephony.mbms.DownloadRequest.Builder fromSerializedRequest(byte[]);
method public android.telephony.mbms.DownloadRequest.Builder setAppIntent(android.content.Intent);
method public android.telephony.mbms.DownloadRequest.Builder setServiceInfo(android.telephony.mbms.FileServiceInfo);
method public android.telephony.mbms.DownloadRequest.Builder setSubscriptionId(int);
@@ -41388,10 +41391,10 @@ package android.telephony.mbms {
method public java.util.Date getSessionStartTime();
}
public class StreamingService {
public class StreamingService implements java.lang.AutoCloseable {
method public void close();
method public android.telephony.mbms.StreamingServiceInfo getInfo();
method public android.net.Uri getPlaybackUri();
method public void stopStreaming();
field public static final int BROADCAST_METHOD = 1; // 0x1
field public static final int REASON_BY_USER_REQUEST = 1; // 0x1
field public static final int REASON_END_OF_SESSION = 2; // 0x2

View File

@@ -5287,12 +5287,7 @@ package android.telephony.ims.stub {
package android.telephony.mbms {
public final class DownloadRequest implements android.os.Parcelable {
method public byte[] getOpaqueData();
}
public static class DownloadRequest.Builder {
method public android.telephony.mbms.DownloadRequest.Builder setOpaqueData(byte[]);
method public android.telephony.mbms.DownloadRequest.Builder setServiceId(java.lang.String);
}

View File

@@ -30,7 +30,6 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.telephony.mbms.DownloadStateCallback;
import android.telephony.mbms.FileInfo;
@@ -53,6 +52,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -107,11 +107,8 @@ public class MbmsDownloadSession implements AutoCloseable {
/**
* {@link Uri} extra that Android will attach to the intent supplied via
* {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}
* Indicates the location of the successfully downloaded file within the temp file root set
* via {@link #setTempFileRootDirectory(File)}.
* While you may use this file in-place, it is highly encouraged that you move
* this file to a different location after receiving the download completion intent, as this
* file resides within the temp file directory.
* Indicates the location of the successfully downloaded file within the directory that the
* app provided via the builder.
*
* Will always be set to a non-null value if
* {@link #EXTRA_MBMS_DOWNLOAD_RESULT} is set to {@link #RESULT_SUCCESSFUL}.
@@ -220,6 +217,8 @@ public class MbmsDownloadSession implements AutoCloseable {
*/
public static final int STATUS_PENDING_DOWNLOAD_WINDOW = 4;
private static final String DESTINATION_SANITY_CHECK_FILE_NAME = "destinationSanityCheckFile";
private static AtomicBoolean sIsInitialized = new AtomicBoolean(false);
private final Context mContext;
@@ -236,23 +235,20 @@ public class MbmsDownloadSession implements AutoCloseable {
private final Map<DownloadStateCallback, InternalDownloadStateCallback>
mInternalDownloadCallbacks = new HashMap<>();
private MbmsDownloadSession(Context context, MbmsDownloadSessionCallback callback,
int subscriptionId, Handler handler) {
private MbmsDownloadSession(Context context, Executor executor, int subscriptionId,
MbmsDownloadSessionCallback callback) {
mContext = context;
mSubscriptionId = subscriptionId;
if (handler == null) {
handler = new Handler(Looper.getMainLooper());
}
mInternalCallback = new InternalDownloadSessionCallback(callback, handler);
mInternalCallback = new InternalDownloadSessionCallback(callback, executor);
}
/**
* Create a new {@link MbmsDownloadSession} using the system default data subscription ID.
* See {@link #create(Context, MbmsDownloadSessionCallback, int, Handler)}
* See {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)}
*/
public static MbmsDownloadSession create(@NonNull Context context,
@NonNull MbmsDownloadSessionCallback callback, @NonNull Handler handler) {
return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), handler);
@NonNull Executor executor, @NonNull MbmsDownloadSessionCallback callback) {
return create(context, executor, SubscriptionManager.getDefaultSubscriptionId(), callback);
}
/**
@@ -279,24 +275,24 @@ public class MbmsDownloadSession implements AutoCloseable {
* {@link MbmsDownloadSession} that you received before calling this method again.
*
* @param context The instance of {@link Context} to use
* @param callback A callback to get asynchronous error messages and file service updates.
* @param executor The executor on which you wish to execute callbacks.
* @param subscriptionId The data subscription ID to use
* @param handler The {@link Handler} on which callbacks should be enqueued.
* @param callback A callback to get asynchronous error messages and file service updates.
* @return A new instance of {@link MbmsDownloadSession}, or null if an error occurred during
* setup.
*/
public static @Nullable MbmsDownloadSession create(@NonNull Context context,
final @NonNull MbmsDownloadSessionCallback callback,
int subscriptionId, @NonNull Handler handler) {
@NonNull Executor executor, int subscriptionId,
final @NonNull MbmsDownloadSessionCallback callback) {
if (!sIsInitialized.compareAndSet(false, true)) {
throw new IllegalStateException("Cannot have two active instances");
}
MbmsDownloadSession session =
new MbmsDownloadSession(context, callback, subscriptionId, handler);
new MbmsDownloadSession(context, executor, subscriptionId, callback);
final int result = session.bindAndInitialize();
if (result != MbmsErrors.SUCCESS) {
sIsInitialized.set(false);
handler.post(new Runnable() {
executor.execute(new Runnable() {
@Override
public void run() {
callback.onError(result, null);
@@ -500,6 +496,10 @@ public class MbmsDownloadSession implements AutoCloseable {
* {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp
* file root directory.
*
* If the {@link DownloadRequest} has a destination that is not on the same filesystem as the
* temp file directory provided via {@link #getTempFileRootDirectory()}, an
* {@link IllegalArgumentException} will be thrown.
*
* Asynchronous errors through the callback may include any error not specific to the
* streaming use-case.
* @param request The request that specifies what should be downloaded.
@@ -522,6 +522,8 @@ public class MbmsDownloadSession implements AutoCloseable {
setTempFileRootDirectory(tempRootDirectory);
}
checkDownloadRequestDestination(request);
try {
int result = downloadService.download(request);
if (result == MbmsErrors.SUCCESS) {
@@ -568,21 +570,21 @@ public class MbmsDownloadSession implements AutoCloseable {
* this method will throw an {@link IllegalArgumentException}.
*
* @param request The {@link DownloadRequest} that you want updates on.
* @param executor The {@link Executor} on which calls to {@code callback} should be executed.
* @param callback The callback that should be called when the middleware has information to
* share on the download.
* @param handler The {@link Handler} on which calls to {@code callback} should be enqueued on.
* @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error,
* and some other error code otherwise.
*/
public int registerStateCallback(@NonNull DownloadRequest request,
@NonNull DownloadStateCallback callback, @NonNull Handler handler) {
@NonNull Executor executor, @NonNull DownloadStateCallback callback) {
IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) {
throw new IllegalStateException("Middleware not yet bound");
}
InternalDownloadStateCallback internalCallback =
new InternalDownloadStateCallback(callback, handler);
new InternalDownloadStateCallback(callback, executor);
try {
int result = downloadService.registerStateCallback(request, internalCallback,
@@ -604,7 +606,7 @@ public class MbmsDownloadSession implements AutoCloseable {
/**
* Un-register a callback previously registered via
* {@link #registerStateCallback(DownloadRequest, DownloadStateCallback, Handler)}. After
* {@link #registerStateCallback(DownloadRequest, Executor, DownloadStateCallback)}. After
* this method is called, no further callbacks will be enqueued on the {@link Handler}
* provided upon registration, even if this method throws an exception.
*
@@ -692,7 +694,7 @@ public class MbmsDownloadSession implements AutoCloseable {
* The state will be delivered as a callback via
* {@link DownloadStateCallback#onStateUpdated(DownloadRequest, FileInfo, int)}. If no such
* callback has been registered via
* {@link #registerStateCallback(DownloadRequest, DownloadStateCallback, Handler)}, this
* {@link #registerStateCallback(DownloadRequest, Executor, DownloadStateCallback)}, this
* method will be a no-op.
*
* If the middleware has no record of the
@@ -775,7 +777,7 @@ public class MbmsDownloadSession implements AutoCloseable {
* instance of {@link MbmsDownloadSessionCallback}, but callbacks that have already been
* enqueued will still be delivered.
*
* It is safe to call {@link #create(Context, MbmsDownloadSessionCallback, int, Handler)} to
* It is safe to call {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)} to
* obtain another instance of {@link MbmsDownloadSession} immediately after this method
* returns.
*
@@ -831,6 +833,36 @@ public class MbmsDownloadSession implements AutoCloseable {
}
}
private void checkDownloadRequestDestination(DownloadRequest request) {
File downloadRequestDestination = new File(request.getDestinationUri().getPath());
if (!downloadRequestDestination.isDirectory()) {
throw new IllegalArgumentException("The destination path must be a directory");
}
// Check if the request destination is okay to use by attempting to rename an empty
// file to there.
File testFile = new File(MbmsTempFileProvider.getEmbmsTempFileDir(mContext),
DESTINATION_SANITY_CHECK_FILE_NAME);
File testFileDestination = new File(downloadRequestDestination,
DESTINATION_SANITY_CHECK_FILE_NAME);
try {
if (!testFile.exists()) {
testFile.createNewFile();
}
if (!testFile.renameTo(testFileDestination)) {
throw new IllegalArgumentException("Destination provided in the download request " +
"is invalid -- files in the temp file directory cannot be directly moved " +
"there.");
}
} catch (IOException e) {
throw new IllegalStateException("Got IOException while testing out the destination: "
+ e);
} finally {
testFile.delete();
testFileDestination.delete();
}
}
private File getDownloadRequestTokenPath(DownloadRequest request) {
File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext,
request.getFileServiceId());

View File

@@ -24,9 +24,7 @@ import android.annotation.TestApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.telephony.mbms.InternalStreamingSessionCallback;
import android.telephony.mbms.InternalStreamingServiceCallback;
@@ -42,6 +40,7 @@ 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;
@@ -89,14 +88,11 @@ public class MbmsStreamingSession implements AutoCloseable {
private int mSubscriptionId = INVALID_SUBSCRIPTION_ID;
/** @hide */
private MbmsStreamingSession(Context context, MbmsStreamingSessionCallback callback,
int subscriptionId, Handler handler) {
private MbmsStreamingSession(Context context, Executor executor, int subscriptionId,
MbmsStreamingSessionCallback callback) {
mContext = context;
mSubscriptionId = subscriptionId;
if (handler == null) {
handler = new Handler(Looper.getMainLooper());
}
mInternalCallback = new InternalStreamingSessionCallback(callback, handler);
mInternalCallback = new InternalStreamingSessionCallback(callback, executor);
}
/**
@@ -117,25 +113,25 @@ public class MbmsStreamingSession implements AutoCloseable {
* {@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.
* @param subscriptionId The subscription ID to use.
* @param handler The handler you wish to receive callbacks on.
* @return An instance of {@link MbmsStreamingSession}, or null if an error occurred.
*/
public static @Nullable MbmsStreamingSession create(@NonNull Context context,
final @NonNull MbmsStreamingSessionCallback callback, int subscriptionId,
@NonNull Handler handler) {
@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, callback,
subscriptionId, handler);
MbmsStreamingSession session = new MbmsStreamingSession(context, executor,
subscriptionId, callback);
final int result = session.bindAndInitialize();
if (result != MbmsErrors.SUCCESS) {
sIsInitialized.set(false);
handler.post(new Runnable() {
executor.execute(new Runnable() {
@Override
public void run() {
callback.onError(result, null);
@@ -148,22 +144,22 @@ public class MbmsStreamingSession implements AutoCloseable {
/**
* Create a new {@link MbmsStreamingSession} using the system default data subscription ID.
* See {@link #create(Context, MbmsStreamingSessionCallback, int, Handler)}.
* See {@link #create(Context, Executor, int, MbmsStreamingSessionCallback)}.
*/
public static MbmsStreamingSession create(@NonNull Context context,
@NonNull MbmsStreamingSessionCallback callback, @NonNull Handler handler) {
return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), handler);
@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#stopStreaming()} had been called on them. After this method returns,
* {@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, MbmsStreamingSessionCallback, int, Handler)} to
* It is safe to call {@link #create(Context, Executor, int, MbmsStreamingSessionCallback)} to
* obtain another instance of {@link MbmsStreamingSession} immediately after this method
* returns.
*
@@ -237,20 +233,20 @@ public class MbmsStreamingSession implements AutoCloseable {
* {@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.
* @param handler A handler that calls to {@code callback} should be called on.
* @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,
StreamingServiceCallback callback, @NonNull Handler handler) {
@NonNull Executor executor, StreamingServiceCallback callback) {
IMbmsStreamingService streamingService = mService.get();
if (streamingService == null) {
throw new IllegalStateException("Middleware not yet bound");
}
InternalStreamingServiceCallback serviceCallback = new InternalStreamingServiceCallback(
callback, handler);
callback, executor);
StreamingService serviceForApp = new StreamingService(
mSubscriptionId, streamingService, this, serviceInfo, serviceCallback);

View File

@@ -27,10 +27,13 @@ import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -54,34 +57,116 @@ public final class DownloadRequest implements Parcelable {
public static final int MAX_DESTINATION_URI_SIZE = 50000;
/** @hide */
private static class OpaqueDataContainer implements Serializable {
private final String appIntent;
private final int version;
private static class SerializationDataContainer implements Externalizable {
private String fileServiceId;
private Uri source;
private Uri destination;
private int subscriptionId;
private String appIntent;
private int version;
public OpaqueDataContainer(String appIntent, int version) {
this.appIntent = appIntent;
this.version = version;
public SerializationDataContainer() {}
SerializationDataContainer(DownloadRequest request) {
fileServiceId = request.fileServiceId;
source = request.sourceUri;
destination = request.destinationUri;
subscriptionId = request.subscriptionId;
appIntent = request.serializedResultIntentForApp;
version = request.version;
}
@Override
public void writeExternal(ObjectOutput objectOutput) throws IOException {
objectOutput.write(version);
objectOutput.writeUTF(fileServiceId);
objectOutput.writeUTF(source.toString());
objectOutput.writeUTF(destination.toString());
objectOutput.write(subscriptionId);
objectOutput.writeUTF(appIntent);
}
@Override
public void readExternal(ObjectInput objectInput) throws IOException {
version = objectInput.read();
fileServiceId = objectInput.readUTF();
source = Uri.parse(objectInput.readUTF());
destination = Uri.parse(objectInput.readUTF());
subscriptionId = objectInput.read();
appIntent = objectInput.readUTF();
// Do version checks here -- future versions may have other fields.
}
}
public static class Builder {
private String fileServiceId;
private Uri source;
private Uri destination;
private int subscriptionId;
private String appIntent;
private int version = CURRENT_VERSION;
/**
* Constructs a {@link Builder} from a {@link DownloadRequest}
* @param other The {@link DownloadRequest} from which the data for the {@link Builder}
* should come.
* @return An instance of {@link Builder} pre-populated with data from the provided
* {@link DownloadRequest}.
*/
public static Builder fromDownloadRequest(DownloadRequest other) {
Builder result = new Builder(other.sourceUri, other.destinationUri)
.setServiceId(other.fileServiceId)
.setSubscriptionId(other.subscriptionId);
result.appIntent = other.serializedResultIntentForApp;
// Version of the result is going to be the current version -- as this class gets
// updated, new fields will be set to default values in here.
return result;
}
/**
* This method constructs a new instance of {@link Builder} based on the serialized data
* passed in.
* @param data A byte array, the contents of which should have been originally obtained
* from {@link DownloadRequest#toByteArray()}.
*/
public static Builder fromSerializedRequest(byte[] data) {
Builder builder;
try {
ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data));
SerializationDataContainer dataContainer =
(SerializationDataContainer) stream.readObject();
builder = new Builder(dataContainer.source, dataContainer.destination);
builder.version = dataContainer.version;
builder.appIntent = dataContainer.appIntent;
builder.fileServiceId = dataContainer.fileServiceId;
builder.subscriptionId = dataContainer.subscriptionId;
} catch (IOException e) {
// Really should never happen
Log.e(LOG_TAG, "Got IOException trying to parse opaque data");
throw new IllegalArgumentException(e);
} catch (ClassNotFoundException e) {
Log.e(LOG_TAG, "Got ClassNotFoundException trying to parse opaque data");
throw new IllegalArgumentException(e);
}
return builder;
}
/**
* Builds a new DownloadRequest.
* @param sourceUri the source URI for the DownloadRequest to be built. This URI should
* never be null.
* @param destinationUri The final location for the file(s) that are to be downloaded. It
* must be on the same filesystem as the temp file directory set via
* {@link android.telephony.MbmsDownloadSession#setTempFileRootDirectory(File)}.
* The provided path must be a directory that exists. An
* {@link IllegalArgumentException} will be thrown otherwise.
*/
public Builder(@NonNull Uri sourceUri) {
if (sourceUri == null) {
throw new IllegalArgumentException("Source URI must be non-null.");
public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri) {
if (sourceUri == null || destinationUri == null) {
throw new IllegalArgumentException("Source and destination URIs must be non-null.");
}
source = sourceUri;
destination = destinationUri;
}
/**
@@ -130,68 +215,34 @@ public final class DownloadRequest implements Parcelable {
return this;
}
/**
* For use by the middleware to set the byte array of opaque data. The opaque data
* includes information about the download request that is used by the client app and the
* manager code, but is irrelevant to the middleware.
* @param data A byte array, the contents of which should have been originally obtained
* from {@link DownloadRequest#getOpaqueData()}.
* @hide
*/
@SystemApi
public Builder setOpaqueData(byte[] data) {
try {
ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data));
OpaqueDataContainer dataContainer = (OpaqueDataContainer) stream.readObject();
version = dataContainer.version;
appIntent = dataContainer.appIntent;
} catch (IOException e) {
// Really should never happen
Log.e(LOG_TAG, "Got IOException trying to parse opaque data");
throw new IllegalArgumentException(e);
} catch (ClassNotFoundException e) {
Log.e(LOG_TAG, "Got ClassNotFoundException trying to parse opaque data");
throw new IllegalArgumentException(e);
}
return this;
}
public DownloadRequest build() {
return new DownloadRequest(fileServiceId, source, subscriptionId, appIntent, version);
return new DownloadRequest(fileServiceId, source, destination,
subscriptionId, appIntent, version);
}
}
private final String fileServiceId;
private final Uri sourceUri;
private final Uri destinationUri;
private final int subscriptionId;
private final String serializedResultIntentForApp;
private final int version;
private DownloadRequest(String fileServiceId,
Uri source, int sub,
Uri source, Uri destination, int sub,
String appIntent, int version) {
this.fileServiceId = fileServiceId;
sourceUri = source;
subscriptionId = sub;
destinationUri = destination;
serializedResultIntentForApp = appIntent;
this.version = version;
}
public static DownloadRequest copy(DownloadRequest other) {
return new DownloadRequest(other);
}
private DownloadRequest(DownloadRequest dr) {
fileServiceId = dr.fileServiceId;
sourceUri = dr.sourceUri;
subscriptionId = dr.subscriptionId;
serializedResultIntentForApp = dr.serializedResultIntentForApp;
version = dr.version;
}
private DownloadRequest(Parcel in) {
fileServiceId = in.readString();
sourceUri = in.readParcelable(getClass().getClassLoader());
destinationUri = in.readParcelable(getClass().getClassLoader());
subscriptionId = in.readInt();
serializedResultIntentForApp = in.readString();
version = in.readInt();
@@ -204,6 +255,7 @@ public final class DownloadRequest implements Parcelable {
public void writeToParcel(Parcel out, int flags) {
out.writeString(fileServiceId);
out.writeParcelable(sourceUri, flags);
out.writeParcelable(destinationUri, flags);
out.writeInt(subscriptionId);
out.writeString(serializedResultIntentForApp);
out.writeInt(version);
@@ -223,6 +275,13 @@ public final class DownloadRequest implements Parcelable {
return sourceUri;
}
/**
* @return The destination {@link Uri} of the downloaded file.
*/
public Uri getDestinationUri() {
return destinationUri;
}
/**
* @return The subscription ID on which to perform MBMS operations.
*/
@@ -244,19 +303,16 @@ public final class DownloadRequest implements Parcelable {
}
/**
* For use by the middleware only. The byte array returned from this method should be
* persisted and sent back to the app upon download completion or failure by passing it into
* {@link Builder#setOpaqueData(byte[])}.
* @return A byte array of opaque data to persist.
* @hide
* This method returns a byte array that may be persisted to disk and restored to a
* {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method
* may be recovered via {@link Builder#fromSerializedRequest(byte[])}.
* @return A byte array of data to persist.
*/
@SystemApi
public byte[] getOpaqueData() {
public byte[] toByteArray() {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
OpaqueDataContainer container = new OpaqueDataContainer(
serializedResultIntentForApp, version);
SerializationDataContainer container = new SerializationDataContainer(this);
stream.writeObject(container);
stream.flush();
return byteArrayOutputStream.toByteArray();
@@ -298,15 +354,6 @@ public final class DownloadRequest implements Parcelable {
return MAX_DESTINATION_URI_SIZE;
}
/**
* @hide
*/
public boolean isMultipartDownload() {
// TODO: figure out what qualifies a request as a multipart download request.
return getSourceUri().getLastPathSegment() != null &&
getSourceUri().getLastPathSegment().contains("*");
}
/**
* Retrieves the hash string that should be used as the filename when storing a token for
* this DownloadRequest.
@@ -320,8 +367,9 @@ public final class DownloadRequest implements Parcelable {
throw new RuntimeException("Could not get sha256 hash object");
}
if (version >= 1) {
// Hash the source URI and the app intent
// Hash the source, destination, and the app intent
digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8));
digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8));
if (serializedResultIntentForApp != null) {
digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8));
}
@@ -344,12 +392,13 @@ public final class DownloadRequest implements Parcelable {
version == request.version &&
Objects.equals(fileServiceId, request.fileServiceId) &&
Objects.equals(sourceUri, request.sourceUri) &&
Objects.equals(destinationUri, request.destinationUri) &&
Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp);
}
@Override
public int hashCode() {
return Objects.hash(fileServiceId, sourceUri,
return Objects.hash(fileServiceId, sourceUri, destinationUri,
subscriptionId, serializedResultIntentForApp, version);
}
}

View File

@@ -16,22 +16,23 @@
package android.telephony.mbms;
import android.os.Handler;
import android.os.Binder;
import android.os.RemoteException;
import java.util.List;
import java.util.concurrent.Executor;
/** @hide */
public class InternalDownloadSessionCallback extends IMbmsDownloadSessionCallback.Stub {
private final Handler mHandler;
private final Executor mExecutor;
private final MbmsDownloadSessionCallback mAppCallback;
private volatile boolean mIsStopped = false;
public InternalDownloadSessionCallback(MbmsDownloadSessionCallback appCallback,
Handler handler) {
Executor executor) {
mAppCallback = appCallback;
mHandler = handler;
mExecutor = executor;
}
@Override
@@ -40,10 +41,15 @@ public class InternalDownloadSessionCallback extends IMbmsDownloadSessionCallbac
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onError(errorCode, message);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onError(errorCode, message);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -54,10 +60,15 @@ public class InternalDownloadSessionCallback extends IMbmsDownloadSessionCallbac
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onFileServicesUpdated(services);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onFileServicesUpdated(services);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -68,18 +79,19 @@ public class InternalDownloadSessionCallback extends IMbmsDownloadSessionCallbac
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onMiddlewareReady();
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onMiddlewareReady();
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
public Handler getHandler() {
return mHandler;
}
public void stop() {
mIsStopped = true;
}

View File

@@ -16,20 +16,22 @@
package android.telephony.mbms;
import android.os.Handler;
import android.os.Binder;
import android.os.RemoteException;
import java.util.concurrent.Executor;
/**
* @hide
*/
public class InternalDownloadStateCallback extends IDownloadStateCallback.Stub {
private final Handler mHandler;
private final Executor mExecutor;
private final DownloadStateCallback mAppCallback;
private volatile boolean mIsStopped = false;
public InternalDownloadStateCallback(DownloadStateCallback appCallback, Handler handler) {
public InternalDownloadStateCallback(DownloadStateCallback appCallback, Executor executor) {
mAppCallback = appCallback;
mHandler = handler;
mExecutor = executor;
}
@Override
@@ -40,11 +42,16 @@ public class InternalDownloadStateCallback extends IDownloadStateCallback.Stub {
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onProgressUpdated(request, fileInfo, currentDownloadSize,
fullDownloadSize, currentDecodedSize, fullDecodedSize);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onProgressUpdated(request, fileInfo, currentDownloadSize,
fullDownloadSize, currentDecodedSize, fullDecodedSize);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -56,10 +63,15 @@ public class InternalDownloadStateCallback extends IDownloadStateCallback.Stub {
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onStateUpdated(request, fileInfo, state);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onStateUpdated(request, fileInfo, state);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}

View File

@@ -16,18 +16,21 @@
package android.telephony.mbms;
import android.os.Handler;
import android.os.Binder;
import android.os.RemoteException;
import java.util.concurrent.Executor;
/** @hide */
public class InternalStreamingServiceCallback extends IStreamingServiceCallback.Stub {
private final StreamingServiceCallback mAppCallback;
private final Handler mHandler;
private final Executor mExecutor;
private volatile boolean mIsStopped = false;
public InternalStreamingServiceCallback(StreamingServiceCallback appCallback, Handler handler) {
public InternalStreamingServiceCallback(StreamingServiceCallback appCallback,
Executor executor) {
mAppCallback = appCallback;
mHandler = handler;
mExecutor = executor;
}
@Override
@@ -36,10 +39,15 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback.
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onError(errorCode, message);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onError(errorCode, message);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -50,10 +58,15 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback.
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onStreamStateUpdated(state, reason);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onStreamStateUpdated(state, reason);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -64,10 +77,15 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback.
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onMediaDescriptionUpdated();
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onMediaDescriptionUpdated();
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -78,10 +96,15 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback.
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onBroadcastSignalStrengthUpdated(signalStrength);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onBroadcastSignalStrengthUpdated(signalStrength);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -92,10 +115,15 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback.
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onStreamMethodUpdated(methodType);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onStreamMethodUpdated(methodType);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}

View File

@@ -16,21 +16,22 @@
package android.telephony.mbms;
import android.os.Handler;
import android.os.Binder;
import android.os.RemoteException;
import java.util.List;
import java.util.concurrent.Executor;
/** @hide */
public class InternalStreamingSessionCallback extends IMbmsStreamingSessionCallback.Stub {
private final Handler mHandler;
private final Executor mExecutor;
private final MbmsStreamingSessionCallback mAppCallback;
private volatile boolean mIsStopped = false;
public InternalStreamingSessionCallback(MbmsStreamingSessionCallback appCallback,
Handler handler) {
Executor executor) {
mAppCallback = appCallback;
mHandler = handler;
mExecutor = executor;
}
@Override
@@ -39,10 +40,15 @@ public class InternalStreamingSessionCallback extends IMbmsStreamingSessionCallb
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onError(errorCode, message);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onError(errorCode, message);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -54,10 +60,15 @@ public class InternalStreamingSessionCallback extends IMbmsStreamingSessionCallb
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onStreamingServicesUpdated(services);
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onStreamingServicesUpdated(services);
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
@@ -68,18 +79,19 @@ public class InternalStreamingSessionCallback extends IMbmsStreamingSessionCallb
return;
}
mHandler.post(new Runnable() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mAppCallback.onMiddlewareReady();
long token = Binder.clearCallingIdentity();
try {
mAppCallback.onMiddlewareReady();
} finally {
Binder.restoreCallingIdentity(token);
}
}
});
}
public Handler getHandler() {
return mHandler;
}
public void stop() {
mIsStopped = true;
}

View File

@@ -21,8 +21,10 @@ import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.telephony.MbmsDownloadSession;
@@ -31,14 +33,11 @@ import android.util.Log;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -62,6 +61,8 @@ public class MbmsDownloadReceiver extends BroadcastReceiver {
/** @hide */
public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority";
private static final String EMBMS_INTENT_PERMISSION = "android.permission.SEND_EMBMS_INTENTS";
/**
* Indicates that the requested operation completed without error.
* @hide
@@ -137,6 +138,8 @@ public class MbmsDownloadReceiver extends BroadcastReceiver {
/** @hide */
@Override
public void onReceive(Context context, Intent intent) {
verifyPermissionIntegrity(context);
if (!verifyIntentContents(context, intent)) {
setResultCode(RESULT_MALFORMED_INTENT);
return;
@@ -260,20 +263,18 @@ public class MbmsDownloadReceiver extends BroadcastReceiver {
FileInfo completedFileInfo =
(FileInfo) intent.getParcelableExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO);
Path stagingDirectory = FileSystems.getDefault().getPath(
MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath(),
TEMP_FILE_STAGING_LOCATION);
Path appSpecifiedDestination = FileSystems.getDefault().getPath(
request.getDestinationUri().getPath());
Uri stagedFileLocation;
Uri finalLocation;
try {
stagedFileLocation = stageTempFile(finalTempFile, stagingDirectory);
finalLocation = moveToFinalLocation(finalTempFile, appSpecifiedDestination);
} catch (IOException e) {
Log.w(LOG_TAG, "Failed to move temp file to final destination");
setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR);
return;
}
intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI,
stagedFileLocation);
intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI, finalLocation);
intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, completedFileInfo);
context.sendBroadcast(intentForApp);
@@ -437,19 +438,22 @@ public class MbmsDownloadReceiver extends BroadcastReceiver {
}
/*
* Moves a tempfile located at fromPath to a new location in the staging directory.
* Moves a tempfile located at fromPath to its final home where the app wants it
*/
private static Uri stageTempFile(Uri fromPath, Path stagingDirectory) throws IOException {
private static Uri moveToFinalLocation(Uri fromPath, Path appSpecifiedPath) throws IOException {
if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) {
Log.w(LOG_TAG, "Moving source uri " + fromPath+ " does not have a file scheme");
Log.w(LOG_TAG, "Downloaded file location uri " + fromPath +
" does not have a file scheme");
return null;
}
Path fromFile = FileSystems.getDefault().getPath(fromPath.getPath());
if (!Files.isDirectory(stagingDirectory)) {
Files.createDirectory(stagingDirectory);
if (!Files.isDirectory(appSpecifiedPath)) {
Files.createDirectory(appSpecifiedPath);
}
Path result = Files.move(fromFile, stagingDirectory.resolve(fromFile.getFileName()));
// TODO: do we want to support directory trees within the download directory?
Path result = Files.move(fromFile, appSpecifiedPath.resolve(fromFile.getFileName()),
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
return Uri.fromFile(result.toFile());
}
@@ -513,39 +517,29 @@ public class MbmsDownloadReceiver extends BroadcastReceiver {
return mMiddlewarePackageNameCache;
}
private static boolean manualMove(File src, File dst) {
InputStream in = null;
OutputStream out = null;
try {
if (!dst.exists()) {
dst.createNewFile();
}
in = new FileInputStream(src);
out = new FileOutputStream(dst);
byte[] buffer = new byte[2048];
int len;
do {
len = in.read(buffer);
out.write(buffer, 0, len);
} while (len > 0);
} catch (IOException e) {
Log.w(LOG_TAG, "Manual file move failed due to exception " + e);
if (dst.exists()) {
dst.delete();
}
return false;
} finally {
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
} catch (IOException e) {
Log.w(LOG_TAG, "Error closing streams: " + e);
}
private void verifyPermissionIntegrity(Context context) {
PackageManager pm = context.getPackageManager();
Intent queryIntent = new Intent(context, MbmsDownloadReceiver.class);
List<ResolveInfo> infos = pm.queryBroadcastReceivers(queryIntent, 0);
if (infos.size() != 1) {
throw new IllegalStateException("Non-unique download receiver in your app");
}
ActivityInfo selfInfo = infos.get(0).activityInfo;
if (selfInfo == null) {
throw new IllegalStateException("Queried ResolveInfo does not contain a receiver");
}
if (MbmsUtils.getOverrideServiceName(context,
MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION) != null) {
// If an override was specified, just make sure that the permission isn't null.
if (selfInfo.permission == null) {
throw new IllegalStateException(
"MbmsDownloadReceiver must require some permission");
}
return;
}
if (!Objects.equals(EMBMS_INTENT_PERMISSION, selfInfo.permission)) {
throw new IllegalStateException("MbmsDownloadReceiver must require the " +
"SEND_EMBMS_INTENTS permission.");
}
return true;
}
}

View File

@@ -108,8 +108,8 @@ public class MbmsErrors {
/**
* Indicates that the app called
* {@link MbmsStreamingSession#startStreaming(
* StreamingServiceInfo, StreamingServiceCallback, android.os.Handler)}
* {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo,
* java.util.concurrent.Executor, StreamingServiceCallback)}
* more than once for the same {@link StreamingServiceInfo}.
*/
public static final int ERROR_DUPLICATE_START_STREAM = 303;

View File

@@ -22,11 +22,12 @@ import android.os.Handler;
import android.telephony.MbmsStreamingSession;
import java.util.List;
import java.util.concurrent.Executor;
/**
* A callback class that is used to receive information from the middleware on MBMS streaming
* services. An instance of this object should be passed into
* {@link MbmsStreamingSession#create(Context, MbmsStreamingSessionCallback, int, Handler)}.
* {@link MbmsStreamingSession#create(Context, Executor, int, MbmsStreamingSessionCallback)}.
*/
public class MbmsStreamingSessionCallback {
/**

View File

@@ -50,7 +50,7 @@ public class MbmsUtils {
return new ComponentName(ci.packageName, ci.name);
}
private static ComponentName getOverrideServiceName(Context context, String serviceAction) {
public static ComponentName getOverrideServiceName(Context context, String serviceAction) {
String metaDataKey = null;
switch (serviceAction) {
case MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION:

View File

@@ -29,11 +29,11 @@ import java.lang.annotation.RetentionPolicy;
/**
* Class used to represent a single MBMS stream. After a stream has been started with
* {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo,
* StreamingServiceCallback, android.os.Handler)},
* {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo, java.util.concurrent.Executor,
* StreamingServiceCallback)},
* this class is used to hold information about the stream and control it.
*/
public class StreamingService {
public class StreamingService implements AutoCloseable {
private static final String LOG_TAG = "MbmsStreamingService";
/**
@@ -41,7 +41,7 @@ public class StreamingService {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_STOPPED, STATE_STARTED, STATE_STALLED})
@IntDef(prefix = { "STATE_" }, value = {STATE_STOPPED, STATE_STARTED, STATE_STALLED})
public @interface StreamingState {}
public final static int STATE_STOPPED = 1;
public final static int STATE_STARTED = 2;
@@ -53,7 +53,8 @@ public class StreamingService {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({REASON_BY_USER_REQUEST, REASON_END_OF_SESSION, REASON_FREQUENCY_CONFLICT,
@IntDef(prefix = { "REASON_" },
value = {REASON_BY_USER_REQUEST, REASON_END_OF_SESSION, REASON_FREQUENCY_CONFLICT,
REASON_OUT_OF_MEMORY, REASON_NOT_CONNECTED_TO_HOMECARRIER_LTE,
REASON_LEFT_MBMS_BROADCAST_AREA, REASON_NONE})
public @interface StreamingStateChangeReason {}
@@ -64,9 +65,9 @@ public class StreamingService {
public static final int REASON_NONE = 0;
/**
* State changed due to a call to {@link #stopStreaming()} or
* State changed due to a call to {@link #close()} or
* {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo,
* StreamingServiceCallback, android.os.Handler)}
* java.util.concurrent.Executor, StreamingServiceCallback)}
*/
public static final int REASON_BY_USER_REQUEST = 1;
@@ -161,7 +162,8 @@ public class StreamingService {
*
* May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}
*/
public void stopStreaming() {
@Override
public void close() {
if (mService == null) {
throw new IllegalStateException("No streaming service attached");
}