* Enforce that only one instance of each manager can be active.
* Add a death receipient for both managers to notify the app of binder
death
* Add documentation informing the app that it may not call create()
multiple times
* Fix a collision in streaming state reason codes
* Add documentation in DownloadRequest to indicate which methods should
be called by the middleware.
Change-Id: Ie15283b5c34fee736e8023dbd4f889c2ca95299e
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_FREQUENCY_CONFLICT = 3; // 0x3
- field public static final int REASON_LEFT_MBMS_BROADCAST_AREA = 5; // 0x5
+ field public static final int REASON_LEFT_MBMS_BROADCAST_AREA = 6; // 0x6
field public static final int REASON_NONE = 0; // 0x0
field public static final int REASON_NOT_CONNECTED_TO_HOMECARRIER_LTE = 5; // 0x5
field public static final int REASON_OUT_OF_MEMORY = 4; // 0x4
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_FREQUENCY_CONFLICT = 3; // 0x3
- field public static final int REASON_LEFT_MBMS_BROADCAST_AREA = 5; // 0x5
+ field public static final int REASON_LEFT_MBMS_BROADCAST_AREA = 6; // 0x6
field public static final int REASON_NONE = 0; // 0x0
field public static final int REASON_NOT_CONNECTED_TO_HOMECARRIER_LTE = 5; // 0x5
field public static final int REASON_OUT_OF_MEMORY = 4; // 0x4
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_FREQUENCY_CONFLICT = 3; // 0x3
- field public static final int REASON_LEFT_MBMS_BROADCAST_AREA = 5; // 0x5
+ field public static final int REASON_LEFT_MBMS_BROADCAST_AREA = 6; // 0x6
field public static final int REASON_NONE = 0; // 0x0
field public static final int REASON_NOT_CONNECTED_TO_HOMECARRIER_LTE = 5; // 0x5
field public static final int REASON_OUT_OF_MEMORY = 4; // 0x4
import android.telephony.mbms.FileInfo;
import android.telephony.mbms.DownloadRequest;
import android.telephony.mbms.IDownloadProgressListener;
-import android.telephony.mbms.IMbmsDownloadManagerCallback;
import android.telephony.mbms.MbmsDownloadManagerCallback;
import android.telephony.mbms.MbmsDownloadReceiver;
import android.telephony.mbms.MbmsException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
public static final int STATUS_PENDING_REPAIR = 3;
public static final int STATUS_PENDING_DOWNLOAD_WINDOW = 4;
+ private static AtomicBoolean sIsInitialized = new AtomicBoolean(false);
+
private final Context mContext;
private int mSubscriptionId = INVALID_SUBSCRIPTION_ID;
+ private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ sendErrorToApp(MbmsException.ERROR_MIDDLEWARE_LOST, "Received death notification");
+ }
+ };
private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null);
private final MbmsDownloadManagerCallback mCallback;
*
* Note that this call will bind a remote service and that may take a bit. The instance of
* {@link MbmsDownloadManager} that is returned will not be ready for use until
- * {@link IMbmsDownloadManagerCallback#middlewareReady()} is called on the provided callback.
+ * {@link MbmsDownloadManagerCallback#middlewareReady()} is called on the provided callback.
* If you attempt to use the manager before it is ready, a {@link MbmsException} will be thrown.
*
- * This also may throw an {@link IllegalArgumentException} or a {@link MbmsException}.
+ * This also may throw an {@link IllegalArgumentException} or an {@link IllegalStateException}.
+ *
+ * You may only have one instance of {@link MbmsDownloadManager} per UID. If you call this
+ * method while there is an active instance of {@link MbmsDownloadManager} in your process
+ * (in other words, one that has not had {@link #dispose()} called on it), this method will
+ * throw an {@link MbmsException}. If you call this method in a different process
+ * running under the same UID, an error will be indicated via
+ * {@link MbmsDownloadManagerCallback#error(int, String)}.
+ *
+ * Note that initialization may fail asynchronously. If you wish to try again after you
+ * receive such an asynchronous error, you must call dispose() on the instance of
+ * {@link MbmsDownloadManager} that you received before calling this method again.
*
* @param context The instance of {@link Context} to use
* @param listener A callback to get asynchronous error messages and file service updates.
public static MbmsDownloadManager create(Context context,
MbmsDownloadManagerCallback listener, int subscriptionId)
throws MbmsException {
+ if (!sIsInitialized.compareAndSet(false, true)) {
+ throw new MbmsException(MbmsException.InitializationErrors.ERROR_DUPLICATE_INITIALIZE);
+ }
MbmsDownloadManager mdm = new MbmsDownloadManager(context, listener, subscriptionId);
- mdm.bindAndInitialize();
+ try {
+ mdm.bindAndInitialize();
+ } catch (MbmsException e) {
+ sIsInitialized.set(false);
+ throw e;
+ }
return mdm;
}
result = downloadService.initialize(mSubscriptionId, mCallback);
} catch (RemoteException e) {
Log.e(LOG_TAG, "Service died before initialization");
+ sIsInitialized.set(false);
return;
} catch (RuntimeException e) {
Log.e(LOG_TAG, "Runtime exception during initialization");
- mCallback.error(
+ sendErrorToApp(
MbmsException.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE,
e.toString());
+ sIsInitialized.set(false);
return;
}
if (result != MbmsException.SUCCESS) {
- mCallback.error(result, "Error returned during initialization");
+ sendErrorToApp(result, "Error returned during initialization");
+ sIsInitialized.set(false);
+ return;
+ }
+ try {
+ downloadService.asBinder().linkToDeath(mDeathRecipient, 0);
+ } catch (RemoteException e) {
+ sendErrorToApp(MbmsException.ERROR_MIDDLEWARE_LOST,
+ "Middleware lost during initialization");
+ sIsInitialized.set(false);
return;
}
mService.set(downloadService);
@Override
public void onServiceDisconnected(ComponentName name) {
+ sIsInitialized.set(false);
mService.set(null);
}
});
* An inspection API to retrieve the list of available
* {@link android.telephony.mbms.FileServiceInfo}s currently being advertised.
* The results are returned asynchronously via a call to
- * {@link IMbmsDownloadManagerCallback#fileServicesUpdated(List)}
+ * {@link MbmsDownloadManagerCallback#fileServicesUpdated(List)}
*
* The serviceClasses argument lets the app filter on types of programming and is opaque data
* negotiated beforehand between the app and the carrier.
* {@link MbmsException.StreamingErrors#ERROR_UNABLE_TO_START_SERVICE}
*
* @param classList A list of service classes which the app wishes to receive
- * {@link IMbmsDownloadManagerCallback#fileServicesUpdated(List)} callbacks
+ * {@link MbmsDownloadManagerCallback#fileServicesUpdated(List)} callbacks
* about. Subsequent calls to this method will replace this list of service
* classes (i.e. the middleware will no longer send updates for services
* matching classes only in the old list).
* local instance of {@link android.content.SharedPreferences} and by the middleware.
*
* If this method is not called at least once before calling
- * {@link #download(DownloadRequest, IDownloadCallback)}, the framework
+ * {@link #download(DownloadRequest, IDownloadProgressListener)}, the framework
* will default to a directory formed by the concatenation of the app's files directory and
* {@link android.telephony.mbms.MbmsTempFileProvider#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}.
*
/**
* Returns a list of pending {@link DownloadRequest}s that originated from this application.
* A pending request is one that was issued via
- * {@link #download(DownloadRequest, IDownloadCallback)} but not cancelled through
+ * {@link #download(DownloadRequest, IDownloadProgressListener)} but not cancelled through
* {@link #cancelDownload(DownloadRequest)}.
* @return A list, possibly empty, of {@link DownloadRequest}s
*/
return;
}
downloadService.dispose(mSubscriptionId);
- mService.set(null);
} catch (RemoteException e) {
// Ignore
Log.i(LOG_TAG, "Remote exception while disposing of service");
+ } finally {
+ mService.set(null);
+ sIsInitialized.set(false);
}
}
}
}
}
+
+ private void sendErrorToApp(int errorCode, String message) {
+ try {
+ mCallback.error(errorCode, message);
+ } catch (RemoteException e) {
+ // Ignore, should not happen locally.
+ }
+ }
}
import android.util.Log;
import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
public static final String MBMS_STREAMING_SERVICE_ACTION =
"android.telephony.action.EmbmsStreaming";
+ private static AtomicBoolean sIsInitialized = new AtomicBoolean(false);
+
private AtomicReference<IMbmsStreamingService> mService = new AtomicReference<>(null);
+ private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ sIsInitialized.set(false);
+ sendErrorToApp(MbmsException.ERROR_MIDDLEWARE_LOST, "Received death notification");
+ }
+ };
+
private InternalStreamingManagerCallback mInternalCallback;
private final Context mContext;
* main thread. This may throw an {@link MbmsException}, indicating errors that may happen
* during the initialization or binding process.
*
+ *
+ * You may only have one instance of {@link MbmsStreamingManager} per UID. If you call this
+ * method while there is an active instance of {@link MbmsStreamingManager} in your process
+ * (in other words, one that has not had {@link #dispose()} called on it), this method will
+ * throw an {@link MbmsException}. If you call this method in a different process
+ * running under the same UID, an error will be indicated via
+ * {@link MbmsStreamingManagerCallback#onError(int, String)}.
+ *
+ * Note that initialization may fail asynchronously. If you wish to try again after you
+ * receive such an asynchronous error, you must call dispose() on the instance of
+ * {@link MbmsStreamingManager} that you received before calling this method again.
+ *
* @param context The {@link Context} to use.
* @param callback A callback object on which you wish to receive results of asynchronous
* operations.
public static MbmsStreamingManager create(Context context,
MbmsStreamingManagerCallback callback, int subscriptionId, Handler handler)
throws MbmsException {
+ if (!sIsInitialized.compareAndSet(false, true)) {
+ throw new MbmsException(MbmsException.InitializationErrors.ERROR_DUPLICATE_INITIALIZE);
+ }
MbmsStreamingManager manager = new MbmsStreamingManager(context, callback,
subscriptionId, handler);
- manager.bindAndInitialize();
+ try {
+ manager.bindAndInitialize();
+ } catch (MbmsException e) {
+ sIsInitialized.set(false);
+ throw e;
+ }
return manager;
}
* May throw an {@link IllegalStateException}
*/
public void dispose() {
- IMbmsStreamingService streamingService = mService.get();
- if (streamingService == null) {
- // Ignore and return, assume already disposed.
- return;
- }
try {
+ IMbmsStreamingService streamingService = mService.get();
+ if (streamingService == null) {
+ // Ignore and return, assume already disposed.
+ return;
+ }
streamingService.dispose(mSubscriptionId);
} catch (RemoteException e) {
// Ignore for now
+ } finally {
+ mService.set(null);
+ sIsInitialized.set(false);
}
- mService.set(null);
}
/**
} catch (RemoteException e) {
Log.w(LOG_TAG, "Remote process died");
mService.set(null);
+ sIsInitialized.set(false);
throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST);
}
}
} catch (RemoteException e) {
Log.w(LOG_TAG, "Remote process died");
mService.set(null);
+ sIsInitialized.set(false);
throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_LOST);
}
mSubscriptionId);
} catch (RemoteException e) {
Log.e(LOG_TAG, "Service died before initialization");
+ sendErrorToApp(
+ MbmsException.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE,
+ e.toString());
+ sIsInitialized.set(false);
return;
} catch (RuntimeException e) {
Log.e(LOG_TAG, "Runtime exception during initialization");
- try {
- mInternalCallback.error(
- MbmsException.InitializationErrors
- .ERROR_UNABLE_TO_INITIALIZE,
- e.toString());
- } catch (RemoteException e1) {
- // ignore
- }
+ sendErrorToApp(
+ MbmsException.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE,
+ e.toString());
+ sIsInitialized.set(false);
return;
}
if (result != MbmsException.SUCCESS) {
- try {
- mInternalCallback.error(
- result, "Error returned during initialization");
- } catch (RemoteException e) {
- // ignore
- }
+ sendErrorToApp(result, "Error returned during initialization");
+ sIsInitialized.set(false);
+ return;
+ }
+ try {
+ streamingService.asBinder().linkToDeath(mDeathRecipient, 0);
+ } catch (RemoteException e) {
+ sendErrorToApp(MbmsException.ERROR_MIDDLEWARE_LOST,
+ "Middleware lost during initialization");
+ sIsInitialized.set(false);
return;
}
mService.set(streamingService);
@Override
public void onServiceDisconnected(ComponentName name) {
+ sIsInitialized.set(false);
mService.set(null);
}
});
}
+
+ private void sendErrorToApp(int errorCode, String message) {
+ try {
+ mInternalCallback.error(errorCode, message);
+ } catch (RemoteException e) {
+ // Ignore, should not happen locally.
+ }
+ }
}
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
private String appIntent;
private int version = CURRENT_VERSION;
+ /**
+ * Sets the service from which the download request to be built will download from.
+ * @param serviceInfo
+ * @return
+ */
public Builder setServiceInfo(FileServiceInfo serviceInfo) {
fileServiceId = serviceInfo.getServiceId();
return this;
}
/**
+ * Set the service ID for the download request. For use by the middleware only.
* @hide
* TODO: systemapi
*/
return this;
}
+ /**
+ * Sets the source URI for the download request to be built.
+ * @param source
+ * @return
+ */
public Builder setSource(Uri source) {
this.source = source;
return this;
}
+ /**
+ * Sets the destination URI for the download request to be built. The middleware should
+ * not set this directly.
+ * @param dest A URI obtained from {@link Uri#fromFile(File)}, denoting the requested
+ * final destination of the download.
+ * @return
+ */
public Builder setDest(Uri dest) {
if (dest.toString().length() > MAX_DESTINATION_URI_SIZE) {
throw new IllegalArgumentException("Destination uri must not exceed length " +
return this;
}
- public Builder setSubscriptionId(int sub) {
- this.subscriptionId = sub;
+ /**
+ * Set the subscription ID on which the file(s) should be downloaded.
+ * @param subscriptionId
+ * @return
+ */
+ public Builder setSubscriptionId(int subscriptionId) {
+ this.subscriptionId = subscriptionId;
return this;
}
+ /**
+ * Set the {@link Intent} that should be sent when the download completes or fails. This
+ * should be an intent with a explicit {@link android.content.ComponentName} targeted to a
+ * {@link android.content.BroadcastReceiver} in the app's package.
+ *
+ * The middleware should not use this method.
+ * @param intent
+ * @return
+ */
public Builder setAppIntent(Intent intent) {
this.appIntent = intent.toUri(0);
if (this.appIntent.length() > MAX_APP_INTENT_SIZE) {
}
/**
- * For use by middleware only
+ * 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()}.
+ * @return
* TODO: systemapi
* @hide
*/
out.writeInt(version);
}
+ /**
+ * @return The ID of the file service to download from.
+ */
public String getFileServiceId() {
return fileServiceId;
}
+ /**
+ * @return The source URI to download from
+ */
public Uri getSourceUri() {
return sourceUri;
}
+ /**
+ * For use by the client app only.
+ * @return The URI of the final destination of the download.
+ */
public Uri getDestinationUri() {
return destinationUri;
}
+ /**
+ * @return The subscription ID on which to perform MBMS operations.
+ */
public int getSubscriptionId() {
return subscriptionId;
}
+ /**
+ * For internal use -- returns the intent to send to the app after download completion or
+ * failure.
+ * @hide
+ */
public Intent getIntentForApp() {
try {
return Intent.parseUri(serializedResultIntentForApp, 0);
}
/**
+ * 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
* TODO: systemapi
*/
package android.telephony.mbms;
+import android.os.RemoteException;
+import android.telephony.MbmsDownloadManager;
+
import java.util.List;
/**
*/
public class MbmsDownloadManagerCallback extends IMbmsDownloadManagerCallback.Stub {
- public final static int ERROR_CARRIER_NOT_SUPPORTED = 1;
- public final static int ERROR_UNABLE_TO_INITIALIZE = 2;
- public final static int ERROR_UNABLE_TO_ALLOCATE_MEMORY = 3;
-
-
- public void error(int errorCode, String message) {
+ @Override
+ public void error(int errorCode, String message) throws RemoteException {
// default implementation empty
}
* @param services a List of FileServiceInfos
*
*/
- public void fileServicesUpdated(List<FileServiceInfo> services) {
+ @Override
+ public void fileServicesUpdated(List<FileServiceInfo> services) throws RemoteException {
// default implementation empty
}
* or {@link MbmsException.GeneralErrors#ERROR_MIDDLEWARE_NOT_YET_READY}
*/
@Override
- public void middlewareReady() {
+ public void middlewareReady() throws RemoteException {
// default implementation empty
}
}
import android.content.Context;
import android.os.RemoteException;
+import android.telephony.MbmsStreamingManager;
import java.util.List;
/**
* State changed due to the device leaving the where this stream is being broadcast.
*/
- public static final int REASON_LEFT_MBMS_BROADCAST_AREA = 5;
+ public static final int REASON_LEFT_MBMS_BROADCAST_AREA = 6;
/**
* The method of transmission currently used for a stream,
import android.os.RemoteException;
import android.telephony.mbms.DownloadRequest;
import android.telephony.mbms.FileInfo;
+import android.telephony.mbms.FileServiceInfo;
import android.telephony.mbms.IDownloadProgressListener;
import android.telephony.mbms.IMbmsDownloadManagerCallback;
+import android.telephony.mbms.MbmsDownloadManagerCallback;
import android.telephony.mbms.MbmsException;
import java.util.List;
* or {@link MbmsException#SUCCESS}. Non-successful error codes will be passed to the app via
* {@link IMbmsDownloadManagerCallback#error(int, String)}.
*
- * @param listener The callback to use to communicate with the app.
+ * @param callback The callback to use to communicate with the app.
* @param subscriptionId The subscription ID to use.
*/
+ public int initialize(int subscriptionId, MbmsDownloadManagerCallback callback)
+ throws RemoteException {
+ return 0;
+ }
+
+ /**
+ * Actual AIDL implementation -- hides the callback AIDL from the API.
+ * @hide
+ */
@Override
public int initialize(int subscriptionId,
- IMbmsDownloadManagerCallback listener) throws RemoteException {
- return 0;
+ IMbmsDownloadManagerCallback callback) throws RemoteException {
+ return initialize(subscriptionId, new MbmsDownloadManagerCallback() {
+ @Override
+ public void error(int errorCode, String message) throws RemoteException {
+ callback.error(errorCode, message);
+ }
+
+ @Override
+ public void fileServicesUpdated(List<FileServiceInfo> services) throws RemoteException {
+ callback.fileServicesUpdated(services);
+ }
+
+ @Override
+ public void middlewareReady() throws RemoteException {
+ callback.middlewareReady();
+ }
+ });
}
/**
/**
* Returns a list of pending {@link DownloadRequest}s that originated from the calling
* application, identified by its uid. A pending request is one that was issued via
- * {@link #download(DownloadRequest, IDownloadCallback)} but not cancelled through
+ * {@link #download(DownloadRequest, IDownloadProgressListener)} but not cancelled through
* {@link #cancelDownload(DownloadRequest)}.
* The middleware must return a non-null result synchronously or throw an exception
* inheriting from {@link RuntimeException}.
/**
* Initialize streaming service for this app and subId, registering the listener.
*
- * May throw an {@link IllegalArgumentException} or an {@link IllegalStateException}, which
+ * May throw an {@link IllegalArgumentException} or a {@link SecurityException}, which
* will be intercepted and passed to the app as
* {@link android.telephony.mbms.MbmsException.InitializationErrors#ERROR_UNABLE_TO_INITIALIZE}
*