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 { public class MbmsDownloadSession implements java.lang.AutoCloseable {
method public int cancelDownload(android.telephony.mbms.DownloadRequest); method public int cancelDownload(android.telephony.mbms.DownloadRequest);
method public void close(); 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, java.util.concurrent.Executor, android.telephony.mbms.MbmsDownloadSessionCallback);
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, int, android.telephony.mbms.MbmsDownloadSessionCallback);
method public int download(android.telephony.mbms.DownloadRequest); method public int download(android.telephony.mbms.DownloadRequest);
method public java.io.File getTempFileRootDirectory(); method public java.io.File getTempFileRootDirectory();
method public java.util.List<android.telephony.mbms.DownloadRequest> listPendingDownloads(); 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 requestDownloadState(android.telephony.mbms.DownloadRequest, android.telephony.mbms.FileInfo);
method public void requestUpdateFileServices(java.util.List<java.lang.String>); method public void requestUpdateFileServices(java.util.List<java.lang.String>);
method public void resetDownloadKnowledge(android.telephony.mbms.DownloadRequest); method public void resetDownloadKnowledge(android.telephony.mbms.DownloadRequest);
@@ -40448,10 +40448,10 @@ package android.telephony {
public class MbmsStreamingSession implements java.lang.AutoCloseable { public class MbmsStreamingSession implements java.lang.AutoCloseable {
method public void close(); 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, java.util.concurrent.Executor, int, android.telephony.mbms.MbmsStreamingSessionCallback);
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, android.telephony.mbms.MbmsStreamingSessionCallback);
method public void requestUpdateStreamingServices(java.util.List<java.lang.String>); 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 { public class NeighboringCellInfo implements android.os.Parcelable {
@@ -41279,20 +41279,23 @@ package android.telephony.gsm {
package android.telephony.mbms { package android.telephony.mbms {
public final class DownloadRequest implements android.os.Parcelable { 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 int describeContents();
method public android.net.Uri getDestinationUri();
method public java.lang.String getFileServiceId(); method public java.lang.String getFileServiceId();
method public static int getMaxAppIntentSize(); method public static int getMaxAppIntentSize();
method public static int getMaxDestinationUriSize(); method public static int getMaxDestinationUriSize();
method public android.net.Uri getSourceUri(); method public android.net.Uri getSourceUri();
method public int getSubscriptionId(); method public int getSubscriptionId();
method public byte[] toByteArray();
method public void writeToParcel(android.os.Parcel, int); method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.telephony.mbms.DownloadRequest> CREATOR; field public static final android.os.Parcelable.Creator<android.telephony.mbms.DownloadRequest> CREATOR;
} }
public static class DownloadRequest.Builder { 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 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 setAppIntent(android.content.Intent);
method public android.telephony.mbms.DownloadRequest.Builder setServiceInfo(android.telephony.mbms.FileServiceInfo); method public android.telephony.mbms.DownloadRequest.Builder setServiceInfo(android.telephony.mbms.FileServiceInfo);
method public android.telephony.mbms.DownloadRequest.Builder setSubscriptionId(int); method public android.telephony.mbms.DownloadRequest.Builder setSubscriptionId(int);
@@ -41388,10 +41391,10 @@ package android.telephony.mbms {
method public java.util.Date getSessionStartTime(); 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.telephony.mbms.StreamingServiceInfo getInfo();
method public android.net.Uri getPlaybackUri(); method public android.net.Uri getPlaybackUri();
method public void stopStreaming();
field public static final int BROADCAST_METHOD = 1; // 0x1 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_BY_USER_REQUEST = 1; // 0x1
field public static final int REASON_END_OF_SESSION = 2; // 0x2 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 { package android.telephony.mbms {
public final class DownloadRequest implements android.os.Parcelable {
method public byte[] getOpaqueData();
}
public static class DownloadRequest.Builder { 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); 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.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException; import android.os.RemoteException;
import android.telephony.mbms.DownloadStateCallback; import android.telephony.mbms.DownloadStateCallback;
import android.telephony.mbms.FileInfo; import android.telephony.mbms.FileInfo;
@@ -53,6 +52,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; 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 Uri} extra that Android will attach to the intent supplied via
* {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}
* Indicates the location of the successfully downloaded file within the temp file root set * Indicates the location of the successfully downloaded file within the directory that the
* via {@link #setTempFileRootDirectory(File)}. * app provided via the builder.
* 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.
* *
* Will always be set to a non-null value if * Will always be set to a non-null value if
* {@link #EXTRA_MBMS_DOWNLOAD_RESULT} is set to {@link #RESULT_SUCCESSFUL}. * {@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; 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 static AtomicBoolean sIsInitialized = new AtomicBoolean(false);
private final Context mContext; private final Context mContext;
@@ -236,23 +235,20 @@ public class MbmsDownloadSession implements AutoCloseable {
private final Map<DownloadStateCallback, InternalDownloadStateCallback> private final Map<DownloadStateCallback, InternalDownloadStateCallback>
mInternalDownloadCallbacks = new HashMap<>(); mInternalDownloadCallbacks = new HashMap<>();
private MbmsDownloadSession(Context context, MbmsDownloadSessionCallback callback, private MbmsDownloadSession(Context context, Executor executor, int subscriptionId,
int subscriptionId, Handler handler) { MbmsDownloadSessionCallback callback) {
mContext = context; mContext = context;
mSubscriptionId = subscriptionId; mSubscriptionId = subscriptionId;
if (handler == null) { mInternalCallback = new InternalDownloadSessionCallback(callback, executor);
handler = new Handler(Looper.getMainLooper());
}
mInternalCallback = new InternalDownloadSessionCallback(callback, handler);
} }
/** /**
* Create a new {@link MbmsDownloadSession} using the system default data subscription ID. * 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, public static MbmsDownloadSession create(@NonNull Context context,
@NonNull MbmsDownloadSessionCallback callback, @NonNull Handler handler) { @NonNull Executor executor, @NonNull MbmsDownloadSessionCallback callback) {
return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), handler); 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. * {@link MbmsDownloadSession} that you received before calling this method again.
* *
* @param context The instance of {@link Context} to use * @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 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 * @return A new instance of {@link MbmsDownloadSession}, or null if an error occurred during
* setup. * setup.
*/ */
public static @Nullable MbmsDownloadSession create(@NonNull Context context, public static @Nullable MbmsDownloadSession create(@NonNull Context context,
final @NonNull MbmsDownloadSessionCallback callback, @NonNull Executor executor, int subscriptionId,
int subscriptionId, @NonNull Handler handler) { final @NonNull MbmsDownloadSessionCallback callback) {
if (!sIsInitialized.compareAndSet(false, true)) { if (!sIsInitialized.compareAndSet(false, true)) {
throw new IllegalStateException("Cannot have two active instances"); throw new IllegalStateException("Cannot have two active instances");
} }
MbmsDownloadSession session = MbmsDownloadSession session =
new MbmsDownloadSession(context, callback, subscriptionId, handler); new MbmsDownloadSession(context, executor, subscriptionId, callback);
final int result = session.bindAndInitialize(); final int result = session.bindAndInitialize();
if (result != MbmsErrors.SUCCESS) { if (result != MbmsErrors.SUCCESS) {
sIsInitialized.set(false); sIsInitialized.set(false);
handler.post(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
callback.onError(result, null); 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 * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp
* file root directory. * 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 * Asynchronous errors through the callback may include any error not specific to the
* streaming use-case. * streaming use-case.
* @param request The request that specifies what should be downloaded. * @param request The request that specifies what should be downloaded.
@@ -522,6 +522,8 @@ public class MbmsDownloadSession implements AutoCloseable {
setTempFileRootDirectory(tempRootDirectory); setTempFileRootDirectory(tempRootDirectory);
} }
checkDownloadRequestDestination(request);
try { try {
int result = downloadService.download(request); int result = downloadService.download(request);
if (result == MbmsErrors.SUCCESS) { if (result == MbmsErrors.SUCCESS) {
@@ -568,21 +570,21 @@ public class MbmsDownloadSession implements AutoCloseable {
* this method will throw an {@link IllegalArgumentException}. * this method will throw an {@link IllegalArgumentException}.
* *
* @param request The {@link DownloadRequest} that you want updates on. * @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 * @param callback The callback that should be called when the middleware has information to
* share on the download. * 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, * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error,
* and some other error code otherwise. * and some other error code otherwise.
*/ */
public int registerStateCallback(@NonNull DownloadRequest request, public int registerStateCallback(@NonNull DownloadRequest request,
@NonNull DownloadStateCallback callback, @NonNull Handler handler) { @NonNull Executor executor, @NonNull DownloadStateCallback callback) {
IMbmsDownloadService downloadService = mService.get(); IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) { if (downloadService == null) {
throw new IllegalStateException("Middleware not yet bound"); throw new IllegalStateException("Middleware not yet bound");
} }
InternalDownloadStateCallback internalCallback = InternalDownloadStateCallback internalCallback =
new InternalDownloadStateCallback(callback, handler); new InternalDownloadStateCallback(callback, executor);
try { try {
int result = downloadService.registerStateCallback(request, internalCallback, int result = downloadService.registerStateCallback(request, internalCallback,
@@ -604,7 +606,7 @@ public class MbmsDownloadSession implements AutoCloseable {
/** /**
* Un-register a callback previously registered via * 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} * this method is called, no further callbacks will be enqueued on the {@link Handler}
* provided upon registration, even if this method throws an exception. * 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 * The state will be delivered as a callback via
* {@link DownloadStateCallback#onStateUpdated(DownloadRequest, FileInfo, int)}. If no such * {@link DownloadStateCallback#onStateUpdated(DownloadRequest, FileInfo, int)}. If no such
* callback has been registered via * callback has been registered via
* {@link #registerStateCallback(DownloadRequest, DownloadStateCallback, Handler)}, this * {@link #registerStateCallback(DownloadRequest, Executor, DownloadStateCallback)}, this
* method will be a no-op. * method will be a no-op.
* *
* If the middleware has no record of the * 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 * instance of {@link MbmsDownloadSessionCallback}, but callbacks that have already been
* enqueued will still be delivered. * 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 * obtain another instance of {@link MbmsDownloadSession} immediately after this method
* returns. * 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) { private File getDownloadRequestTokenPath(DownloadRequest request) {
File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext, File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext,
request.getFileServiceId()); request.getFileServiceId());

View File

@@ -24,9 +24,7 @@ import android.annotation.TestApi;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException; import android.os.RemoteException;
import android.telephony.mbms.InternalStreamingSessionCallback; import android.telephony.mbms.InternalStreamingSessionCallback;
import android.telephony.mbms.InternalStreamingServiceCallback; import android.telephony.mbms.InternalStreamingServiceCallback;
@@ -42,6 +40,7 @@ import android.util.Log;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -89,14 +88,11 @@ public class MbmsStreamingSession implements AutoCloseable {
private int mSubscriptionId = INVALID_SUBSCRIPTION_ID; private int mSubscriptionId = INVALID_SUBSCRIPTION_ID;
/** @hide */ /** @hide */
private MbmsStreamingSession(Context context, MbmsStreamingSessionCallback callback, private MbmsStreamingSession(Context context, Executor executor, int subscriptionId,
int subscriptionId, Handler handler) { MbmsStreamingSessionCallback callback) {
mContext = context; mContext = context;
mSubscriptionId = subscriptionId; mSubscriptionId = subscriptionId;
if (handler == null) { mInternalCallback = new InternalStreamingSessionCallback(callback, executor);
handler = new Handler(Looper.getMainLooper());
}
mInternalCallback = new InternalStreamingSessionCallback(callback, handler);
} }
/** /**
@@ -117,25 +113,25 @@ public class MbmsStreamingSession implements AutoCloseable {
* {@link MbmsStreamingSession} that you received before calling this method again. * {@link MbmsStreamingSession} that you received before calling this method again.
* *
* @param context The {@link Context} to use. * @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 * @param callback A callback object on which you wish to receive results of asynchronous
* operations. * 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. * @return An instance of {@link MbmsStreamingSession}, or null if an error occurred.
*/ */
public static @Nullable MbmsStreamingSession create(@NonNull Context context, public static @Nullable MbmsStreamingSession create(@NonNull Context context,
final @NonNull MbmsStreamingSessionCallback callback, int subscriptionId, @NonNull Executor executor, int subscriptionId,
@NonNull Handler handler) { final @NonNull MbmsStreamingSessionCallback callback) {
if (!sIsInitialized.compareAndSet(false, true)) { if (!sIsInitialized.compareAndSet(false, true)) {
throw new IllegalStateException("Cannot create two instances of MbmsStreamingSession"); throw new IllegalStateException("Cannot create two instances of MbmsStreamingSession");
} }
MbmsStreamingSession session = new MbmsStreamingSession(context, callback, MbmsStreamingSession session = new MbmsStreamingSession(context, executor,
subscriptionId, handler); subscriptionId, callback);
final int result = session.bindAndInitialize(); final int result = session.bindAndInitialize();
if (result != MbmsErrors.SUCCESS) { if (result != MbmsErrors.SUCCESS) {
sIsInitialized.set(false); sIsInitialized.set(false);
handler.post(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
callback.onError(result, null); 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. * 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, public static MbmsStreamingSession create(@NonNull Context context,
@NonNull MbmsStreamingSessionCallback callback, @NonNull Handler handler) { @NonNull Executor executor, @NonNull MbmsStreamingSessionCallback callback) {
return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), handler); return create(context, executor, SubscriptionManager.getDefaultSubscriptionId(), callback);
} }
/** /**
* Terminates this instance. Also terminates * Terminates this instance. Also terminates
* any streaming services spawned from this instance as if * 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 * no further callbacks originating from the middleware will be enqueued on the provided
* instance of {@link MbmsStreamingSessionCallback}, but callbacks that have already been * instance of {@link MbmsStreamingSessionCallback}, but callbacks that have already been
* enqueued will still be delivered. * 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 * obtain another instance of {@link MbmsStreamingSession} immediately after this method
* returns. * returns.
* *
@@ -237,20 +233,20 @@ public class MbmsStreamingSession implements AutoCloseable {
* {@link MbmsErrors.StreamingErrors}. * {@link MbmsErrors.StreamingErrors}.
* *
* @param serviceInfo The information about the service to stream. * @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 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. * @return An instance of {@link StreamingService} through which the stream can be controlled.
* May be {@code null} if an error occurred. * May be {@code null} if an error occurred.
*/ */
public @Nullable StreamingService startStreaming(StreamingServiceInfo serviceInfo, public @Nullable StreamingService startStreaming(StreamingServiceInfo serviceInfo,
StreamingServiceCallback callback, @NonNull Handler handler) { @NonNull Executor executor, StreamingServiceCallback callback) {
IMbmsStreamingService streamingService = mService.get(); IMbmsStreamingService streamingService = mService.get();
if (streamingService == null) { if (streamingService == null) {
throw new IllegalStateException("Middleware not yet bound"); throw new IllegalStateException("Middleware not yet bound");
} }
InternalStreamingServiceCallback serviceCallback = new InternalStreamingServiceCallback( InternalStreamingServiceCallback serviceCallback = new InternalStreamingServiceCallback(
callback, handler); callback, executor);
StreamingService serviceForApp = new StreamingService( StreamingService serviceForApp = new StreamingService(
mSubscriptionId, streamingService, this, serviceInfo, serviceCallback); mSubscriptionId, streamingService, this, serviceInfo, serviceCallback);

View File

@@ -27,10 +27,13 @@ import android.util.Log;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
@@ -54,34 +57,116 @@ public final class DownloadRequest implements Parcelable {
public static final int MAX_DESTINATION_URI_SIZE = 50000; public static final int MAX_DESTINATION_URI_SIZE = 50000;
/** @hide */ /** @hide */
private static class OpaqueDataContainer implements Serializable { private static class SerializationDataContainer implements Externalizable {
private final String appIntent; private String fileServiceId;
private final int version; private Uri source;
private Uri destination;
private int subscriptionId;
private String appIntent;
private int version;
public OpaqueDataContainer(String appIntent, int version) { public SerializationDataContainer() {}
this.appIntent = appIntent;
this.version = version; 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 { public static class Builder {
private String fileServiceId; private String fileServiceId;
private Uri source; private Uri source;
private Uri destination;
private int subscriptionId; private int subscriptionId;
private String appIntent; private String appIntent;
private int version = CURRENT_VERSION; 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. * Builds a new DownloadRequest.
* @param sourceUri the source URI for the DownloadRequest to be built. This URI should * @param sourceUri the source URI for the DownloadRequest to be built. This URI should
* never be null. * 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) { public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri) {
if (sourceUri == null) { if (sourceUri == null || destinationUri == null) {
throw new IllegalArgumentException("Source URI must be non-null."); throw new IllegalArgumentException("Source and destination URIs must be non-null.");
} }
source = sourceUri; source = sourceUri;
destination = destinationUri;
} }
/** /**
@@ -130,68 +215,34 @@ public final class DownloadRequest implements Parcelable {
return this; 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() { 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 String fileServiceId;
private final Uri sourceUri; private final Uri sourceUri;
private final Uri destinationUri;
private final int subscriptionId; private final int subscriptionId;
private final String serializedResultIntentForApp; private final String serializedResultIntentForApp;
private final int version; private final int version;
private DownloadRequest(String fileServiceId, private DownloadRequest(String fileServiceId,
Uri source, int sub, Uri source, Uri destination, int sub,
String appIntent, int version) { String appIntent, int version) {
this.fileServiceId = fileServiceId; this.fileServiceId = fileServiceId;
sourceUri = source; sourceUri = source;
subscriptionId = sub; subscriptionId = sub;
destinationUri = destination;
serializedResultIntentForApp = appIntent; serializedResultIntentForApp = appIntent;
this.version = version; 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) { private DownloadRequest(Parcel in) {
fileServiceId = in.readString(); fileServiceId = in.readString();
sourceUri = in.readParcelable(getClass().getClassLoader()); sourceUri = in.readParcelable(getClass().getClassLoader());
destinationUri = in.readParcelable(getClass().getClassLoader());
subscriptionId = in.readInt(); subscriptionId = in.readInt();
serializedResultIntentForApp = in.readString(); serializedResultIntentForApp = in.readString();
version = in.readInt(); version = in.readInt();
@@ -204,6 +255,7 @@ public final class DownloadRequest implements Parcelable {
public void writeToParcel(Parcel out, int flags) { public void writeToParcel(Parcel out, int flags) {
out.writeString(fileServiceId); out.writeString(fileServiceId);
out.writeParcelable(sourceUri, flags); out.writeParcelable(sourceUri, flags);
out.writeParcelable(destinationUri, flags);
out.writeInt(subscriptionId); out.writeInt(subscriptionId);
out.writeString(serializedResultIntentForApp); out.writeString(serializedResultIntentForApp);
out.writeInt(version); out.writeInt(version);
@@ -223,6 +275,13 @@ public final class DownloadRequest implements Parcelable {
return sourceUri; 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. * @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 * This method returns a byte array that may be persisted to disk and restored to a
* persisted and sent back to the app upon download completion or failure by passing it into * {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method
* {@link Builder#setOpaqueData(byte[])}. * may be recovered via {@link Builder#fromSerializedRequest(byte[])}.
* @return A byte array of opaque data to persist. * @return A byte array of data to persist.
* @hide
*/ */
@SystemApi public byte[] toByteArray() {
public byte[] getOpaqueData() {
try { try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream); ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
OpaqueDataContainer container = new OpaqueDataContainer( SerializationDataContainer container = new SerializationDataContainer(this);
serializedResultIntentForApp, version);
stream.writeObject(container); stream.writeObject(container);
stream.flush(); stream.flush();
return byteArrayOutputStream.toByteArray(); return byteArrayOutputStream.toByteArray();
@@ -298,15 +354,6 @@ public final class DownloadRequest implements Parcelable {
return MAX_DESTINATION_URI_SIZE; 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 * Retrieves the hash string that should be used as the filename when storing a token for
* this DownloadRequest. * this DownloadRequest.
@@ -320,8 +367,9 @@ public final class DownloadRequest implements Parcelable {
throw new RuntimeException("Could not get sha256 hash object"); throw new RuntimeException("Could not get sha256 hash object");
} }
if (version >= 1) { 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(sourceUri.toString().getBytes(StandardCharsets.UTF_8));
digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8));
if (serializedResultIntentForApp != null) { if (serializedResultIntentForApp != null) {
digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8)); digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8));
} }
@@ -344,12 +392,13 @@ public final class DownloadRequest implements Parcelable {
version == request.version && version == request.version &&
Objects.equals(fileServiceId, request.fileServiceId) && Objects.equals(fileServiceId, request.fileServiceId) &&
Objects.equals(sourceUri, request.sourceUri) && Objects.equals(sourceUri, request.sourceUri) &&
Objects.equals(destinationUri, request.destinationUri) &&
Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp); Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(fileServiceId, sourceUri, return Objects.hash(fileServiceId, sourceUri, destinationUri,
subscriptionId, serializedResultIntentForApp, version); subscriptionId, serializedResultIntentForApp, version);
} }
} }

View File

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

View File

@@ -16,20 +16,22 @@
package android.telephony.mbms; package android.telephony.mbms;
import android.os.Handler; import android.os.Binder;
import android.os.RemoteException; import android.os.RemoteException;
import java.util.concurrent.Executor;
/** /**
* @hide * @hide
*/ */
public class InternalDownloadStateCallback extends IDownloadStateCallback.Stub { public class InternalDownloadStateCallback extends IDownloadStateCallback.Stub {
private final Handler mHandler; private final Executor mExecutor;
private final DownloadStateCallback mAppCallback; private final DownloadStateCallback mAppCallback;
private volatile boolean mIsStopped = false; private volatile boolean mIsStopped = false;
public InternalDownloadStateCallback(DownloadStateCallback appCallback, Handler handler) { public InternalDownloadStateCallback(DownloadStateCallback appCallback, Executor executor) {
mAppCallback = appCallback; mAppCallback = appCallback;
mHandler = handler; mExecutor = executor;
} }
@Override @Override
@@ -40,11 +42,16 @@ public class InternalDownloadStateCallback extends IDownloadStateCallback.Stub {
return; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
mAppCallback.onProgressUpdated(request, fileInfo, currentDownloadSize, long token = Binder.clearCallingIdentity();
fullDownloadSize, currentDecodedSize, fullDecodedSize); try {
mAppCallback.onProgressUpdated(request, fileInfo, currentDownloadSize,
fullDownloadSize, currentDecodedSize, fullDecodedSize);
} finally {
Binder.restoreCallingIdentity(token);
}
} }
}); });
} }
@@ -56,10 +63,15 @@ public class InternalDownloadStateCallback extends IDownloadStateCallback.Stub {
return; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { 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; package android.telephony.mbms;
import android.os.Handler; import android.os.Binder;
import android.os.RemoteException; import android.os.RemoteException;
import java.util.concurrent.Executor;
/** @hide */ /** @hide */
public class InternalStreamingServiceCallback extends IStreamingServiceCallback.Stub { public class InternalStreamingServiceCallback extends IStreamingServiceCallback.Stub {
private final StreamingServiceCallback mAppCallback; private final StreamingServiceCallback mAppCallback;
private final Handler mHandler; private final Executor mExecutor;
private volatile boolean mIsStopped = false; private volatile boolean mIsStopped = false;
public InternalStreamingServiceCallback(StreamingServiceCallback appCallback, Handler handler) { public InternalStreamingServiceCallback(StreamingServiceCallback appCallback,
Executor executor) {
mAppCallback = appCallback; mAppCallback = appCallback;
mHandler = handler; mExecutor = executor;
} }
@Override @Override
@@ -36,10 +39,15 @@ public class InternalStreamingServiceCallback extends IStreamingServiceCallback.
return; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { 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; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { 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; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { 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; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { 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; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { 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; package android.telephony.mbms;
import android.os.Handler; import android.os.Binder;
import android.os.RemoteException; import android.os.RemoteException;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor;
/** @hide */ /** @hide */
public class InternalStreamingSessionCallback extends IMbmsStreamingSessionCallback.Stub { public class InternalStreamingSessionCallback extends IMbmsStreamingSessionCallback.Stub {
private final Handler mHandler; private final Executor mExecutor;
private final MbmsStreamingSessionCallback mAppCallback; private final MbmsStreamingSessionCallback mAppCallback;
private volatile boolean mIsStopped = false; private volatile boolean mIsStopped = false;
public InternalStreamingSessionCallback(MbmsStreamingSessionCallback appCallback, public InternalStreamingSessionCallback(MbmsStreamingSessionCallback appCallback,
Handler handler) { Executor executor) {
mAppCallback = appCallback; mAppCallback = appCallback;
mHandler = handler; mExecutor = executor;
} }
@Override @Override
@@ -39,10 +40,15 @@ public class InternalStreamingSessionCallback extends IMbmsStreamingSessionCallb
return; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { 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; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { 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; return;
} }
mHandler.post(new Runnable() { mExecutor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
mAppCallback.onMiddlewareReady(); long token = Binder.clearCallingIdentity();
try {
mAppCallback.onMiddlewareReady();
} finally {
Binder.restoreCallingIdentity(token);
}
} }
}); });
} }
public Handler getHandler() {
return mHandler;
}
public void stop() { public void stop() {
mIsStopped = true; mIsStopped = true;
} }

View File

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

View File

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

View File

@@ -22,11 +22,12 @@ import android.os.Handler;
import android.telephony.MbmsStreamingSession; import android.telephony.MbmsStreamingSession;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor;
/** /**
* A callback class that is used to receive information from the middleware on MBMS streaming * 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 * 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 { public class MbmsStreamingSessionCallback {
/** /**

View File

@@ -50,7 +50,7 @@ public class MbmsUtils {
return new ComponentName(ci.packageName, ci.name); 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; String metaDataKey = null;
switch (serviceAction) { switch (serviceAction) {
case MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION: 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 * Class used to represent a single MBMS stream. After a stream has been started with
* {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo, * {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo, java.util.concurrent.Executor,
* StreamingServiceCallback, android.os.Handler)}, * StreamingServiceCallback)},
* this class is used to hold information about the stream and control it. * 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"; private static final String LOG_TAG = "MbmsStreamingService";
/** /**
@@ -41,7 +41,7 @@ public class StreamingService {
* @hide * @hide
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_STOPPED, STATE_STARTED, STATE_STALLED}) @IntDef(prefix = { "STATE_" }, value = {STATE_STOPPED, STATE_STARTED, STATE_STALLED})
public @interface StreamingState {} public @interface StreamingState {}
public final static int STATE_STOPPED = 1; public final static int STATE_STOPPED = 1;
public final static int STATE_STARTED = 2; public final static int STATE_STARTED = 2;
@@ -53,7 +53,8 @@ public class StreamingService {
* @hide * @hide
*/ */
@Retention(RetentionPolicy.SOURCE) @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_OUT_OF_MEMORY, REASON_NOT_CONNECTED_TO_HOMECARRIER_LTE,
REASON_LEFT_MBMS_BROADCAST_AREA, REASON_NONE}) REASON_LEFT_MBMS_BROADCAST_AREA, REASON_NONE})
public @interface StreamingStateChangeReason {} public @interface StreamingStateChangeReason {}
@@ -64,9 +65,9 @@ public class StreamingService {
public static final int REASON_NONE = 0; 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, * {@link MbmsStreamingSession#startStreaming(StreamingServiceInfo,
* StreamingServiceCallback, android.os.Handler)} * java.util.concurrent.Executor, StreamingServiceCallback)}
*/ */
public static final int REASON_BY_USER_REQUEST = 1; 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} * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}
*/ */
public void stopStreaming() { @Override
public void close() {
if (mService == null) { if (mService == null) {
throw new IllegalStateException("No streaming service attached"); throw new IllegalStateException("No streaming service attached");
} }