/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.telephony;

import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.telephony.mbms.DownloadProgressListener;
import android.telephony.mbms.DownloadRequest;
import android.telephony.mbms.DownloadStatusListener;
import android.telephony.mbms.FileInfo;
import android.telephony.mbms.InternalDownloadProgressListener;
import android.telephony.mbms.InternalDownloadSessionCallback;
import android.telephony.mbms.InternalDownloadStatusListener;
import android.telephony.mbms.MbmsDownloadReceiver;
import android.telephony.mbms.MbmsDownloadSessionCallback;
import android.telephony.mbms.MbmsErrors;
import android.telephony.mbms.MbmsTempFileProvider;
import android.telephony.mbms.MbmsUtils;
import android.telephony.mbms.vendor.IMbmsDownloadService;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

This class provides functionality for file download over MBMS.
/** * This class provides functionality for file download over MBMS. */
public class MbmsDownloadSession implements AutoCloseable { private static final String LOG_TAG = MbmsDownloadSession.class.getSimpleName();
Service action which must be handled by the middleware implementing the MBMS file download interface.
@hide
/** * Service action which must be handled by the middleware implementing the MBMS file download * interface. * @hide */
@SystemApi @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String MBMS_DOWNLOAD_SERVICE_ACTION = "android.telephony.action.EmbmsDownload";
Metadata key that specifies the component name of the service to bind to for file-download.
@hide
/** * Metadata key that specifies the component name of the service to bind to for file-download. * @hide */
@TestApi public static final String MBMS_DOWNLOAD_SERVICE_OVERRIDE_METADATA = "mbms-download-service-override";
Integer extra that Android will attach to the intent supplied via Builder.setAppIntent(Intent) Indicates the result code of the download. One of RESULT_SUCCESSFUL, RESULT_EXPIRED, RESULT_CANCELLED, RESULT_IO_ERROR, RESULT_DOWNLOAD_FAILURE, RESULT_OUT_OF_STORAGE, RESULT_SERVICE_ID_NOT_DEFINED, or RESULT_FILE_ROOT_UNREACHABLE. This extra may also be used by the middleware when it is sending intents to the app.
/** * Integer extra that Android will attach to the intent supplied via * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} * Indicates the result code of the download. One of * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, {@link #RESULT_CANCELLED}, * {@link #RESULT_IO_ERROR}, {@link #RESULT_DOWNLOAD_FAILURE}, {@link #RESULT_OUT_OF_STORAGE}, * {@link #RESULT_SERVICE_ID_NOT_DEFINED}, or {@link #RESULT_FILE_ROOT_UNREACHABLE}. * * This extra may also be used by the middleware when it is sending intents to the app. */
public static final String EXTRA_MBMS_DOWNLOAD_RESULT = "android.telephony.extra.MBMS_DOWNLOAD_RESULT";
FileInfo extra that Android will attach to the intent supplied via Builder.setAppIntent(Intent) Indicates the file for which the download result is for. Never null. This extra may also be used by the middleware when it is sending intents to the app.
/** * {@link FileInfo} extra that Android will attach to the intent supplied via * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} * Indicates the file for which the download result is for. Never null. * * This extra may also be used by the middleware when it is sending intents to the app. */
public static final String EXTRA_MBMS_FILE_INFO = "android.telephony.extra.MBMS_FILE_INFO";
Uri extra that Android will attach to the intent supplied via Builder.setAppIntent(Intent) 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 EXTRA_MBMS_DOWNLOAD_RESULT is set to RESULT_SUCCESSFUL.
/** * {@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 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 String EXTRA_MBMS_COMPLETED_FILE_URI = "android.telephony.extra.MBMS_COMPLETED_FILE_URI";
Extra containing the DownloadRequest for which the download result or file descriptor request is for. Must not be null.
/** * Extra containing the {@link DownloadRequest} for which the download result or file * descriptor request is for. Must not be null. */
public static final String EXTRA_MBMS_DOWNLOAD_REQUEST = "android.telephony.extra.MBMS_DOWNLOAD_REQUEST";
The default directory name for all MBMS temp files. If you call download(DownloadRequest) without first calling setTempFileRootDirectory(File), this directory will be created for you under the path returned by Context.getFilesDir().
/** * The default directory name for all MBMS temp files. If you call * {@link #download(DownloadRequest)} without first calling * {@link #setTempFileRootDirectory(File)}, this directory will be created for you under the * path returned by {@link Context#getFilesDir()}. */
public static final String DEFAULT_TOP_LEVEL_TEMP_DIRECTORY = "androidMbmsTempFileRoot";
@hide
/** @hide */
@Retention(RetentionPolicy.SOURCE) @IntDef(value = {RESULT_SUCCESSFUL, RESULT_CANCELLED, RESULT_EXPIRED, RESULT_IO_ERROR, RESULT_SERVICE_ID_NOT_DEFINED, RESULT_DOWNLOAD_FAILURE, RESULT_OUT_OF_STORAGE, RESULT_FILE_ROOT_UNREACHABLE}, prefix = { "RESULT_" }) public @interface DownloadResultCode{}
Indicates that the download was successful.
/** * Indicates that the download was successful. */
public static final int RESULT_SUCCESSFUL = 1;
Indicates that the download was cancelled via cancelDownload(DownloadRequest).
/** * Indicates that the download was cancelled via {@link #cancelDownload(DownloadRequest)}. */
public static final int RESULT_CANCELLED = 2;
Indicates that the download will not be completed due to the expiration of its download window on the carrier's network.
/** * Indicates that the download will not be completed due to the expiration of its download * window on the carrier's network. */
public static final int RESULT_EXPIRED = 3;
Indicates that the download will not be completed due to an I/O error incurred while writing to temp files. This is likely a transient error and another DownloadRequest should be sent to try the download again.
/** * Indicates that the download will not be completed due to an I/O error incurred while * writing to temp files. * * This is likely a transient error and another {@link DownloadRequest} should be sent to try * the download again. */
public static final int RESULT_IO_ERROR = 4;
Indicates that the Service ID specified in the DownloadRequest is incorrect due to the Id being incorrect, stale, expired, or similar.
/** * Indicates that the Service ID specified in the {@link DownloadRequest} is incorrect due to * the Id being incorrect, stale, expired, or similar. */
public static final int RESULT_SERVICE_ID_NOT_DEFINED = 5;
Indicates that there was an error while processing downloaded files, such as a file repair or file decoding error and is not due to a file I/O error. This is likely a transient error and another DownloadRequest should be sent to try the download again.
/** * Indicates that there was an error while processing downloaded files, such as a file repair or * file decoding error and is not due to a file I/O error. * * This is likely a transient error and another {@link DownloadRequest} should be sent to try * the download again. */
public static final int RESULT_DOWNLOAD_FAILURE = 6;
Indicates that the file system is full and the DownloadRequest can not complete. Either space must be made on the current file system or the temp file root location must be changed to a location that is not full to download the temp files.
/** * Indicates that the file system is full and the {@link DownloadRequest} can not complete. * Either space must be made on the current file system or the temp file root location must be * changed to a location that is not full to download the temp files. */
public static final int RESULT_OUT_OF_STORAGE = 7;
Indicates that the file root that was set is currently unreachable. This can happen if the temp files are set to be stored on external storage and the SD card was removed, for example. The temp file root should be changed before sending another DownloadRequest.
/** * Indicates that the file root that was set is currently unreachable. This can happen if the * temp files are set to be stored on external storage and the SD card was removed, for example. * The temp file root should be changed before sending another DownloadRequest. */
public static final int RESULT_FILE_ROOT_UNREACHABLE = 8;
@hide
/** @hide */
@Retention(RetentionPolicy.SOURCE) @IntDef({STATUS_UNKNOWN, STATUS_ACTIVELY_DOWNLOADING, STATUS_PENDING_DOWNLOAD, STATUS_PENDING_REPAIR, STATUS_PENDING_DOWNLOAD_WINDOW}) public @interface DownloadStatus {}
Indicates that the middleware has no information on the file.
/** * Indicates that the middleware has no information on the file. */
public static final int STATUS_UNKNOWN = 0;
Indicates that the file is actively being downloaded.
/** * Indicates that the file is actively being downloaded. */
public static final int STATUS_ACTIVELY_DOWNLOADING = 1;
Indicates that the file is awaiting the next download or repair operations. When a more precise status is known, the status will change to either STATUS_PENDING_REPAIR or STATUS_PENDING_DOWNLOAD_WINDOW.
/** * Indicates that the file is awaiting the next download or repair operations. When a more * precise status is known, the status will change to either {@link #STATUS_PENDING_REPAIR} or * {@link #STATUS_PENDING_DOWNLOAD_WINDOW}. */
public static final int STATUS_PENDING_DOWNLOAD = 2;
Indicates that the file is awaiting file repair after the download has ended.
/** * Indicates that the file is awaiting file repair after the download has ended. */
public static final int STATUS_PENDING_REPAIR = 3;
Indicates that the file is waiting to download because its download window has not yet started and is scheduled for a future time.
/** * Indicates that the file is waiting to download because its download window has not yet * started and is scheduled for a future time. */
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 int mSubscriptionId = INVALID_SUBSCRIPTION_ID; private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification"); } }; private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null); private final InternalDownloadSessionCallback mInternalCallback; private final Map<DownloadStatusListener, InternalDownloadStatusListener> mInternalDownloadStatusListeners = new HashMap<>(); private final Map<DownloadProgressListener, InternalDownloadProgressListener> mInternalDownloadProgressListeners = new HashMap<>(); private MbmsDownloadSession(Context context, Executor executor, int subscriptionId, MbmsDownloadSessionCallback callback) { mContext = context; mSubscriptionId = subscriptionId; mInternalCallback = new InternalDownloadSessionCallback(callback, executor); }
Create a new MbmsDownloadSession using the system default data subscription ID. See create(Context, Executor, int, MbmsDownloadSessionCallback)
/** * Create a new {@link MbmsDownloadSession} using the system default data subscription ID. * See {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)} */
public static MbmsDownloadSession create(@NonNull Context context, @NonNull Executor executor, @NonNull MbmsDownloadSessionCallback callback) { return create(context, executor, SubscriptionManager.getDefaultSubscriptionId(), callback); }
Create a new MbmsDownloadManager using the given subscription ID. Note that this call will bind a remote service and that may take a bit. The instance of MbmsDownloadSession that is returned will not be ready for use until MbmsDownloadSessionCallback.onMiddlewareReady() is called on the provided callback. If you attempt to use the instance before it is ready, an IllegalStateException will be thrown or an error will be delivered through MbmsDownloadSessionCallback.onError(int, String). This also may throw an IllegalArgumentException. You may only have one instance of MbmsDownloadSession per UID. If you call this method while there is an active instance of MbmsDownloadSession in your process (in other words, one that has not had close() called on it), this method will throw an IllegalStateException. If you call this method in a different process running under the same UID, an error will be indicated via MbmsDownloadSessionCallback.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 close() on the instance of MbmsDownloadSession that you received before calling this method again.
Params:
  • context – The instance of Context to use
  • executor – The executor on which you wish to execute callbacks.
  • subscriptionId – The data subscription ID to use
  • callback – A callback to get asynchronous error messages and file service updates.
Returns:A new instance of MbmsDownloadSession, or null if an error occurred during setup.
/** * Create a new MbmsDownloadManager using the given subscription ID. * * Note that this call will bind a remote service and that may take a bit. The instance of * {@link MbmsDownloadSession} that is returned will not be ready for use until * {@link MbmsDownloadSessionCallback#onMiddlewareReady()} is called on the provided callback. * If you attempt to use the instance before it is ready, an {@link IllegalStateException} * will be thrown or an error will be delivered through * {@link MbmsDownloadSessionCallback#onError(int, String)}. * * This also may throw an {@link IllegalArgumentException}. * * You may only have one instance of {@link MbmsDownloadSession} per UID. If you call this * method while there is an active instance of {@link MbmsDownloadSession} in your process * (in other words, one that has not had {@link #close()} called on it), this method will * throw an {@link IllegalStateException}. If you call this method in a different process * running under the same UID, an error will be indicated via * {@link MbmsDownloadSessionCallback#onError(int, String)}. * * Note that initialization may fail asynchronously. If you wish to try again after you * receive such an asynchronous error, you must call {@link #close()} on the instance of * {@link MbmsDownloadSession} that you received before calling this method again. * * @param context The instance of {@link Context} to use * @param executor The executor on which you wish to execute callbacks. * @param subscriptionId The data subscription ID to use * @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, @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, executor, subscriptionId, callback); final int result = session.bindAndInitialize(); if (result != MbmsErrors.SUCCESS) { sIsInitialized.set(false); executor.execute(new Runnable() { @Override public void run() { callback.onError(result, null); } }); return null; } return session; } private int bindAndInitialize() { return MbmsUtils.startBinding(mContext, MBMS_DOWNLOAD_SERVICE_ACTION, new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IMbmsDownloadService downloadService = IMbmsDownloadService.Stub.asInterface(service); int result; try { result = downloadService.initialize(mSubscriptionId, mInternalCallback); } 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"); sendErrorToApp( MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, e.toString()); sIsInitialized.set(false); return; } if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an" + " unknown error code"); } if (result != MbmsErrors.SUCCESS) { sendErrorToApp(result, "Error returned during initialization"); sIsInitialized.set(false); return; } try { downloadService.asBinder().linkToDeath(mDeathRecipient, 0); } catch (RemoteException e) { sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Middleware lost during initialization"); sIsInitialized.set(false); return; } mService.set(downloadService); } @Override public void onServiceDisconnected(ComponentName name) { Log.w(LOG_TAG, "bindAndInitialize: Remote service disconnected"); sIsInitialized.set(false); mService.set(null); } }); }
An inspection API to retrieve the list of available FileServiceInfos currently being advertised. The results are returned asynchronously via a call to MbmsDownloadSessionCallback.onFileServicesUpdated(List<FileServiceInfo>) Asynchronous error codes via the MbmsDownloadSessionCallback.onError(int, String) callback may include any of the errors that are not specific to the streaming use-case. May throw an IllegalStateException or IllegalArgumentException.
Params:
  • classList – A list of service classes which the app wishes to receive MbmsDownloadSessionCallback.onFileServicesUpdated(List<FileServiceInfo>) 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). Values in this list should be negotiated with the wireless carrier prior to using this API.
/** * 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 MbmsDownloadSessionCallback#onFileServicesUpdated(List)} * * Asynchronous error codes via the {@link MbmsDownloadSessionCallback#onError(int, String)} * callback may include any of the errors that are not specific to the streaming use-case. * * May throw an {@link IllegalStateException} or {@link IllegalArgumentException}. * * @param classList A list of service classes which the app wishes to receive * {@link MbmsDownloadSessionCallback#onFileServicesUpdated(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). * Values in this list should be negotiated with the wireless carrier prior * to using this API. */
public void requestUpdateFileServices(@NonNull List<String> classList) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } try { int returnCode = downloadService.requestUpdateFileServices(mSubscriptionId, classList); if (returnCode == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (returnCode != MbmsErrors.SUCCESS) { sendErrorToApp(returnCode, null); } } catch (RemoteException e) { Log.w(LOG_TAG, "Remote process died"); mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } }
Sets the temp file root for downloads. All temp files created for the middleware to write to will be contained in the specified directory. Applications that wish to specify a location only need to call this method once as long their data is persisted in storage -- the argument will be stored both in a local instance of SharedPreferences and by the middleware. If this method is not called at least once before calling download(DownloadRequest), the framework will default to a directory formed by the concatenation of the app's files directory and DEFAULT_TOP_LEVEL_TEMP_DIRECTORY. Before calling this method, the app must cancel all of its pending DownloadRequests via cancelDownload(DownloadRequest). If this is not done, you will receive an asynchronous error with code DownloadErrors.ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT unless the provided directory is the same as what has been previously configured. The File supplied as a root temp file directory must already exist. If not, an IllegalArgumentException will be thrown. In addition, as an additional sanity check, an IllegalArgumentException will be thrown if you attempt to set the temp file root directory to one of your data roots (the value of Context.getDataDir(), Context.getFilesDir(), or Context.getCacheDir()).
Params:
  • tempFileRootDirectory – A directory to place temp files in.
/** * Sets the temp file root for downloads. * All temp files created for the middleware to write to will be contained in the specified * directory. Applications that wish to specify a location only need to call this method once * as long their data is persisted in storage -- the argument will be stored both in a * 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)}, the framework * will default to a directory formed by the concatenation of the app's files directory and * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}. * * Before calling this method, the app must cancel all of its pending * {@link DownloadRequest}s via {@link #cancelDownload(DownloadRequest)}. If this is not done, * you will receive an asynchronous error with code * {@link MbmsErrors.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} unless the * provided directory is the same as what has been previously configured. * * The {@link File} supplied as a root temp file directory must already exist. If not, an * {@link IllegalArgumentException} will be thrown. In addition, as an additional sanity * check, an {@link IllegalArgumentException} will be thrown if you attempt to set the temp * file root directory to one of your data roots (the value of {@link Context#getDataDir()}, * {@link Context#getFilesDir()}, or {@link Context#getCacheDir()}). * @param tempFileRootDirectory A directory to place temp files in. */
public void setTempFileRootDirectory(@NonNull File tempFileRootDirectory) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } try { validateTempFileRootSanity(tempFileRootDirectory); } catch (IOException e) { throw new IllegalStateException("Got IOException checking directory sanity"); } String filePath; try { filePath = tempFileRootDirectory.getCanonicalPath(); } catch (IOException e) { throw new IllegalArgumentException("Unable to canonicalize the provided path: " + e); } try { int result = downloadService.setTempFileRootDirectory(mSubscriptionId, filePath); if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (result != MbmsErrors.SUCCESS) { sendErrorToApp(result, null); return; } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return; } SharedPreferences prefs = mContext.getSharedPreferences( MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); prefs.edit().putString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, filePath).apply(); } private void validateTempFileRootSanity(File tempFileRootDirectory) throws IOException { if (!tempFileRootDirectory.exists()) { throw new IllegalArgumentException("Provided directory does not exist"); } if (!tempFileRootDirectory.isDirectory()) { throw new IllegalArgumentException("Provided File is not a directory"); } String canonicalTempFilePath = tempFileRootDirectory.getCanonicalPath(); if (mContext.getDataDir().getCanonicalPath().equals(canonicalTempFilePath)) { throw new IllegalArgumentException("Temp file root cannot be your data dir"); } if (mContext.getCacheDir().getCanonicalPath().equals(canonicalTempFilePath)) { throw new IllegalArgumentException("Temp file root cannot be your cache dir"); } if (mContext.getFilesDir().getCanonicalPath().equals(canonicalTempFilePath)) { throw new IllegalArgumentException("Temp file root cannot be your files dir"); } }
Retrieves the currently configured temp file root directory. Returns the file that was configured via setTempFileRootDirectory(File) or the default directory download(DownloadRequest) was called without ever setting the temp file root. If neither method has been called since the last time the app's shared preferences were reset, returns null.
Returns:A File pointing to the configured temp file directory, or null if not yet configured.
/** * Retrieves the currently configured temp file root directory. Returns the file that was * configured via {@link #setTempFileRootDirectory(File)} or the default directory * {@link #download(DownloadRequest)} was called without ever * setting the temp file root. If neither method has been called since the last time the app's * shared preferences were reset, returns {@code null}. * * @return A {@link File} pointing to the configured temp file directory, or null if not yet * configured. */
public @Nullable File getTempFileRootDirectory() { SharedPreferences prefs = mContext.getSharedPreferences( MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); String path = prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null); if (path != null) { return new File(path); } return null; }
Requests the download of a file or set of files that the carrier has indicated to be available. May throw an IllegalArgumentException If setTempFileRootDirectory(File) has not called after the app has been installed, this method will create a directory at the default location defined at DEFAULT_TOP_LEVEL_TEMP_DIRECTORY and store that as the temp file root directory. If the DownloadRequest has a destination that is not on the same filesystem as the temp file directory provided via getTempFileRootDirectory(), an IllegalArgumentException will be thrown. Asynchronous errors through the callback may include any error not specific to the streaming use-case. If no error is delivered via the callback after calling this method, that means that the middleware has successfully started the download or scheduled the download, if the download is at a future time.
Params:
  • request – The request that specifies what should be downloaded.
/** * Requests the download of a file or set of files that the carrier has indicated to be * available. * * May throw an {@link IllegalArgumentException} * * If {@link #setTempFileRootDirectory(File)} has not called after the app has been installed, * this method will create a directory at the default location defined at * {@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. * * If no error is delivered via the callback after calling this method, that means that the * middleware has successfully started the download or scheduled the download, if the download * is at a future time. * @param request The request that specifies what should be downloaded. */
public void download(@NonNull DownloadRequest request) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } // Check to see whether the app's set a temp root dir yet, and set it if not. SharedPreferences prefs = mContext.getSharedPreferences( MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); if (prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null) == null) { File tempRootDirectory = new File(mContext.getFilesDir(), DEFAULT_TOP_LEVEL_TEMP_DIRECTORY); tempRootDirectory.mkdirs(); setTempFileRootDirectory(tempRootDirectory); } checkDownloadRequestDestination(request); try { int result = downloadService.download(request); if (result == MbmsErrors.SUCCESS) { writeDownloadRequestToken(request); } else { if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown" + " error code"); } sendErrorToApp(result, null); } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } }
Returns a list of pending DownloadRequests that originated from this application. A pending request is one that was issued via download(DownloadRequest) but not cancelled through cancelDownload(DownloadRequest).
Returns:A list, possibly empty, of DownloadRequests
/** * 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)} but not cancelled through * {@link #cancelDownload(DownloadRequest)}. * @return A list, possibly empty, of {@link DownloadRequest}s */
public @NonNull List<DownloadRequest> listPendingDownloads() { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } try { return downloadService.listPendingDownloads(mSubscriptionId); } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return Collections.emptyList(); } }
Registers a download status listener for a DownloadRequest previously requested via download(DownloadRequest). This callback will only be called as long as both this app and the middleware are both running -- if either one stops, no further calls on the provided DownloadStatusListener will be enqueued. If the middleware is not aware of the specified download request, this method will throw an IllegalArgumentException. If the operation encountered an error, the error code will be delivered via MbmsDownloadSessionCallback.onError. Repeated calls to this method for the same DownloadRequest will replace the previously registered listener.
Params:
  • request – The DownloadRequest that you want updates on.
  • executor – The Executor on which calls to listener should be executed.
  • listener – The listener that should be called when the middleware has information to share on the status download.
/** * Registers a download status listener for a {@link DownloadRequest} previously requested via * {@link #download(DownloadRequest)}. This callback will only be called as long as both this * app and the middleware are both running -- if either one stops, no further calls on the * provided {@link DownloadStatusListener} will be enqueued. * * If the middleware is not aware of the specified download request, * this method will throw an {@link IllegalArgumentException}. * * If the operation encountered an error, the error code will be delivered via * {@link MbmsDownloadSessionCallback#onError}. * * Repeated calls to this method for the same {@link DownloadRequest} will replace the * previously registered listener. * * @param request The {@link DownloadRequest} that you want updates on. * @param executor The {@link Executor} on which calls to {@code listener } should be executed. * @param listener The listener that should be called when the middleware has information to * share on the status download. */
public void addStatusListener(@NonNull DownloadRequest request, @NonNull Executor executor, @NonNull DownloadStatusListener listener) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } InternalDownloadStatusListener internalListener = new InternalDownloadStatusListener(listener, executor); try { int result = downloadService.addStatusListener(request, internalListener); if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (result != MbmsErrors.SUCCESS) { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } sendErrorToApp(result, null); return; } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return; } mInternalDownloadStatusListeners.put(listener, internalListener); }
Un-register a listener previously registered via addStatusListener(DownloadRequest, Executor, DownloadStatusListener). After this method is called, no further calls will be enqueued on the Executor provided upon registration, even if this method throws an exception. If the middleware is not aware of the specified download request, this method will throw an IllegalArgumentException. If the operation encountered an error, the error code will be delivered via MbmsDownloadSessionCallback.onError.
Params:
  • request – The DownloadRequest provided during registration
  • listener – The listener provided during registration.
/** * Un-register a listener previously registered via * {@link #addStatusListener(DownloadRequest, Executor, DownloadStatusListener)}. After * this method is called, no further calls will be enqueued on the {@link Executor} * provided upon registration, even if this method throws an exception. * * If the middleware is not aware of the specified download request, * this method will throw an {@link IllegalArgumentException}. * * If the operation encountered an error, the error code will be delivered via * {@link MbmsDownloadSessionCallback#onError}. * * @param request The {@link DownloadRequest} provided during registration * @param listener The listener provided during registration. */
public void removeStatusListener(@NonNull DownloadRequest request, @NonNull DownloadStatusListener listener) { try { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } InternalDownloadStatusListener internalListener = mInternalDownloadStatusListeners.get(listener); if (internalListener == null) { throw new IllegalArgumentException("Provided listener was never registered"); } try { int result = downloadService.removeStatusListener(request, internalListener); if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an" + " unknown error code"); } if (result != MbmsErrors.SUCCESS) { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } sendErrorToApp(result, null); return; } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return; } } finally { InternalDownloadStatusListener internalCallback = mInternalDownloadStatusListeners.remove(listener); if (internalCallback != null) { internalCallback.stop(); } } }
Registers a progress listener for a DownloadRequest previously requested via download(DownloadRequest). This listener will only be called as long as both this app and the middleware are both running -- if either one stops, no further calls on the provided DownloadProgressListener will be enqueued. If the middleware is not aware of the specified download request, this method will throw an IllegalArgumentException. If the operation encountered an error, the error code will be delivered via MbmsDownloadSessionCallback.onError. Repeated calls to this method for the same DownloadRequest will replace the previously registered listener.
Params:
  • request – The DownloadRequest that you want updates on.
  • executor – The Executor on which calls to listener should be executed.
  • listener – The listener that should be called when the middleware has information to share on the progress of the download.
/** * Registers a progress listener for a {@link DownloadRequest} previously requested via * {@link #download(DownloadRequest)}. This listener will only be called as long as both this * app and the middleware are both running -- if either one stops, no further calls on the * provided {@link DownloadProgressListener} will be enqueued. * * If the middleware is not aware of the specified download request, * this method will throw an {@link IllegalArgumentException}. * * If the operation encountered an error, the error code will be delivered via * {@link MbmsDownloadSessionCallback#onError}. * * Repeated calls to this method for the same {@link DownloadRequest} will replace the * previously registered listener. * * @param request The {@link DownloadRequest} that you want updates on. * @param executor The {@link Executor} on which calls to {@code listener} should be executed. * @param listener The listener that should be called when the middleware has information to * share on the progress of the download. */
public void addProgressListener(@NonNull DownloadRequest request, @NonNull Executor executor, @NonNull DownloadProgressListener listener) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } InternalDownloadProgressListener internalListener = new InternalDownloadProgressListener(listener, executor); try { int result = downloadService.addProgressListener(request, internalListener); if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (result != MbmsErrors.SUCCESS) { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } sendErrorToApp(result, null); return; } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return; } mInternalDownloadProgressListeners.put(listener, internalListener); }
Un-register a listener previously registered via addProgressListener(DownloadRequest, Executor, DownloadProgressListener). After this method is called, no further callbacks will be enqueued on the Handler provided upon registration, even if this method throws an exception. If the middleware is not aware of the specified download request, this method will throw an IllegalArgumentException. If the operation encountered an error, the error code will be delivered via MbmsDownloadSessionCallback.onError.
Params:
  • request – The DownloadRequest provided during registration
  • listener – The listener provided during registration.
/** * Un-register a listener previously registered via * {@link #addProgressListener(DownloadRequest, Executor, DownloadProgressListener)}. 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. * * If the middleware is not aware of the specified download request, * this method will throw an {@link IllegalArgumentException}. * * If the operation encountered an error, the error code will be delivered via * {@link MbmsDownloadSessionCallback#onError}. * * @param request The {@link DownloadRequest} provided during registration * @param listener The listener provided during registration. */
public void removeProgressListener(@NonNull DownloadRequest request, @NonNull DownloadProgressListener listener) { try { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } InternalDownloadProgressListener internalListener = mInternalDownloadProgressListeners.get(listener); if (internalListener == null) { throw new IllegalArgumentException("Provided listener was never registered"); } try { int result = downloadService.removeProgressListener(request, internalListener); if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not" + " return an unknown error code"); } if (result != MbmsErrors.SUCCESS) { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } sendErrorToApp(result, null); return; } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); return; } } finally { InternalDownloadProgressListener internalCallback = mInternalDownloadProgressListeners.remove(listener); if (internalCallback != null) { internalCallback.stop(); } } }
Attempts to cancel the specified DownloadRequest. If the operation encountered an error, the error code will be delivered via MbmsDownloadSessionCallback.onError.
Params:
  • downloadRequest – The download request that you wish to cancel.
/** * Attempts to cancel the specified {@link DownloadRequest}. * * If the operation encountered an error, the error code will be delivered via * {@link MbmsDownloadSessionCallback#onError}. * * @param downloadRequest The download request that you wish to cancel. */
public void cancelDownload(@NonNull DownloadRequest downloadRequest) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } try { int result = downloadService.cancelDownload(downloadRequest); if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (result != MbmsErrors.SUCCESS) { sendErrorToApp(result, null); } else { deleteDownloadRequestToken(downloadRequest); } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } }
Requests information about the state of a file pending download. The state will be delivered as a callback via DownloadStatusListener.onStatusUpdated(DownloadRequest, FileInfo, int). If no such callback has been registered via addProgressListener(DownloadRequest, Executor, DownloadProgressListener), this method will be a no-op. If the middleware has no record of the file indicated by fileInfo being associated with downloadRequest, an IllegalArgumentException will be thrown.
Params:
  • downloadRequest – The download request to query.
  • fileInfo – The particular file within the request to get information on.
/** * Requests information about the state of a file pending download. * * The state will be delivered as a callback via * {@link DownloadStatusListener#onStatusUpdated(DownloadRequest, FileInfo, int)}. If no such * callback has been registered via * {@link #addProgressListener(DownloadRequest, Executor, DownloadProgressListener)}, this * method will be a no-op. * * If the middleware has no record of the * file indicated by {@code fileInfo} being associated with {@code downloadRequest}, * an {@link IllegalArgumentException} will be thrown. * * @param downloadRequest The download request to query. * @param fileInfo The particular file within the request to get information on. */
public void requestDownloadState(DownloadRequest downloadRequest, FileInfo fileInfo) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } try { int result = downloadService.requestDownloadState(downloadRequest, fileInfo); if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (result != MbmsErrors.SUCCESS) { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_FILE_INFO) { throw new IllegalArgumentException("Unknown file."); } sendErrorToApp(result, null); } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } }
Resets the middleware's knowledge of previously-downloaded files in this download request. Normally, the middleware keeps track of the hashes of downloaded files and won't re-download files whose server-reported hash matches one of the already-downloaded files. This means that if the file is accidentally deleted by the user or by the app, the middleware will not try to download it again. This method will reset the middleware's cache of hashes for the provided DownloadRequest, so that previously downloaded content will be downloaded again when available. This will not interrupt in-progress downloads. This is distinct from cancelling and re-issuing the download request -- if you cancel and re-issue, the middleware will not clear its cache of download state information. If the middleware is not aware of the specified download request, an IllegalArgumentException will be thrown.
Params:
  • downloadRequest – The request to re-download files for.
/** * Resets the middleware's knowledge of previously-downloaded files in this download request. * * Normally, the middleware keeps track of the hashes of downloaded files and won't re-download * files whose server-reported hash matches one of the already-downloaded files. This means * that if the file is accidentally deleted by the user or by the app, the middleware will * not try to download it again. * This method will reset the middleware's cache of hashes for the provided * {@link DownloadRequest}, so that previously downloaded content will be downloaded again * when available. * This will not interrupt in-progress downloads. * * This is distinct from cancelling and re-issuing the download request -- if you cancel and * re-issue, the middleware will not clear its cache of download state information. * * If the middleware is not aware of the specified download request, an * {@link IllegalArgumentException} will be thrown. * * @param downloadRequest The request to re-download files for. */
public void resetDownloadKnowledge(DownloadRequest downloadRequest) { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { throw new IllegalStateException("Middleware not yet bound"); } try { int result = downloadService.resetDownloadKnowledge(downloadRequest); if (result == MbmsErrors.UNKNOWN) { // Unbind and throw an obvious error close(); throw new IllegalStateException("Middleware must not return an unknown error code"); } if (result != MbmsErrors.SUCCESS) { if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { throw new IllegalArgumentException("Unknown download request."); } sendErrorToApp(result, null); } } catch (RemoteException e) { mService.set(null); sIsInitialized.set(false); sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); } }
Terminates this instance. After this method returns, no further callbacks originating from the middleware will be enqueued on the provided instance of MbmsDownloadSessionCallback, but callbacks that have already been enqueued will still be delivered. It is safe to call create(Context, Executor, int, MbmsDownloadSessionCallback) to obtain another instance of MbmsDownloadSession immediately after this method returns. May throw an IllegalStateException
/** * Terminates this instance. * * After this method returns, * no further callbacks originating from the middleware will be enqueued on the provided * instance of {@link MbmsDownloadSessionCallback}, but callbacks that have already been * enqueued will still be delivered. * * It is safe to call {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)} to * obtain another instance of {@link MbmsDownloadSession} immediately after this method * returns. * * May throw an {@link IllegalStateException} */
@Override public void close() { try { IMbmsDownloadService downloadService = mService.get(); if (downloadService == null) { Log.i(LOG_TAG, "Service already dead"); return; } downloadService.dispose(mSubscriptionId); } catch (RemoteException e) { // Ignore Log.i(LOG_TAG, "Remote exception while disposing of service"); } finally { mService.set(null); sIsInitialized.set(false); mInternalCallback.stop(); } } private void writeDownloadRequestToken(DownloadRequest request) { File token = getDownloadRequestTokenPath(request); if (!token.getParentFile().exists()) { token.getParentFile().mkdirs(); } if (token.exists()) { Log.w(LOG_TAG, "Download token " + token.getName() + " already exists"); return; } try { if (!token.createNewFile()) { throw new RuntimeException("Failed to create download token for request " + request + ". Token location is " + token.getPath()); } } catch (IOException e) { throw new RuntimeException("Failed to create download token for request " + request + " due to IOException " + e + ". Attempted to write to " + token.getPath()); } } private void deleteDownloadRequestToken(DownloadRequest request) { File token = getDownloadRequestTokenPath(request); if (!token.isFile()) { Log.w(LOG_TAG, "Attempting to delete non-existent download token at " + token); return; } if (!token.delete()) { Log.w(LOG_TAG, "Couldn't delete download token at " + token); } } 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()); String downloadTokenFileName = request.getHash() + MbmsDownloadReceiver.DOWNLOAD_TOKEN_SUFFIX; return new File(tempFileLocation, downloadTokenFileName); } private void sendErrorToApp(int errorCode, String message) { mInternalCallback.onError(errorCode, message); } }