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);
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 {
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);
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
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);
}
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;
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;
/**
* {@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}.
*/
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;
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);
}
/**
* {@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);
* {@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.
setTempFileRootDirectory(tempRootDirectory);
}
+ checkDownloadRequestDestination(request);
+
try {
int result = downloadService.download(request);
if (result == MbmsErrors.SUCCESS) {
* 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,
/**
* 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.
*
* 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
* 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.
*
}
}
+ 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());
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;
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;
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);
}
/**
* {@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);
/**
* 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.
*
* {@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);
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;
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;
}
/**
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();
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);
}
/**
+ * @return The destination {@link Uri} of the downloaded file.
+ */
+ public Uri getDestinationUri() {
+ return destinationUri;
+ }
+
+ /**
* @return The subscription ID on which to perform MBMS operations.
*/
public int getSubscriptionId() {
}
/**
- * 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();
}
/**
- * @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.
* @hide
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));
}
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);
}
}
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
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);
+ }
}
});
}
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);
+ }
}
});
}
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;
}
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
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);
+ }
}
});
}
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);
+ }
}
});
}
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
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);
+ }
}
});
}
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);
+ }
}
});
}
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);
+ }
}
});
}
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);
+ }
}
});
}
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);
+ }
}
});
}
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
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);
+ }
}
});
}
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);
+ }
}
});
}
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;
}
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;
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;
/** @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
/** @hide */
@Override
public void onReceive(Context context, Intent intent) {
+ verifyPermissionIntegrity(context);
+
if (!verifyIntentContents(context, intent)) {
setResultCode(RESULT_MALFORMED_INTENT);
return;
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);
}
/*
- * 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());
}
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;
}
}
/**
* 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;
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 {
/**
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:
/**
* 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";
/**
* @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;
* @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 {}
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;
*
* 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");
}