| /* |
| * 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 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.Looper; |
| import android.os.RemoteException; |
| import android.telephony.mbms.DownloadStateCallback; |
| import android.telephony.mbms.FileInfo; |
| import android.telephony.mbms.DownloadRequest; |
| import android.telephony.mbms.InternalDownloadSessionCallback; |
| import android.telephony.mbms.InternalDownloadStateCallback; |
| import android.telephony.mbms.MbmsDownloadSessionCallback; |
| import android.telephony.mbms.MbmsDownloadReceiver; |
| 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.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| |
| /** |
| * 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 |
| */ |
| @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 |
| */ |
| @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 |
| * {@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"; |
| |
| /** |
| * {@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"; |
| |
| /** |
| * {@link Uri} extra that Android will attach to the intent supplied via |
| * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} |
| * Indicates the location of the successfully downloaded file within the temp file root set |
| * via {@link #setTempFileRootDirectory(File)}. |
| * While you may use this file in-place, it is highly encouraged that you move |
| * this file to a different location after receiving the download completion intent, as this |
| * file resides within the temp file directory. |
| * |
| * 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 {@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 |
| * {@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"; |
| |
| /** |
| * Indicates that the download was successful. |
| */ |
| public static final int RESULT_SUCCESSFUL = 1; |
| |
| /** |
| * 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. |
| */ |
| 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 {@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 {@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 {@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 {@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. |
| */ |
| public static final int RESULT_FILE_ROOT_UNREACHABLE = 8; |
| |
| /** @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. |
| */ |
| public static final int STATUS_UNKNOWN = 0; |
| |
| /** |
| * Indicates that the file is actively downloading. |
| */ |
| public static final int STATUS_ACTIVELY_DOWNLOADING = 1; |
| |
| /** |
| * TODO: I don't know... |
| */ |
| public static final int STATUS_PENDING_DOWNLOAD = 2; |
| |
| /** |
| * Indicates that the file is being repaired after the download being interrupted. |
| */ |
| public static final int STATUS_PENDING_REPAIR = 3; |
| |
| /** |
| * Indicates that the file is waiting to download because its download window has not yet |
| * started. |
| */ |
| 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(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification"); |
| } |
| }; |
| |
| private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null); |
| private final InternalDownloadSessionCallback mInternalCallback; |
| private final Map<DownloadStateCallback, InternalDownloadStateCallback> |
| mInternalDownloadCallbacks = new HashMap<>(); |
| |
| private MbmsDownloadSession(Context context, MbmsDownloadSessionCallback callback, |
| int subscriptionId, Handler handler) { |
| mContext = context; |
| mSubscriptionId = subscriptionId; |
| if (handler == null) { |
| handler = new Handler(Looper.getMainLooper()); |
| } |
| mInternalCallback = new InternalDownloadSessionCallback(callback, handler); |
| } |
| |
| /** |
| * Create a new {@link MbmsDownloadSession} using the system default data subscription ID. |
| * See {@link #create(Context, MbmsDownloadSessionCallback, int, Handler)} |
| */ |
| public static MbmsDownloadSession create(@NonNull Context context, |
| @NonNull MbmsDownloadSessionCallback callback, @NonNull Handler handler) { |
| return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), handler); |
| } |
| |
| /** |
| * 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 callback A callback to get asynchronous error messages and file service updates. |
| * @param subscriptionId The data subscription ID to use |
| * @param handler The {@link Handler} on which callbacks should be enqueued. |
| * @return A new instance of {@link MbmsDownloadSession}, or null if an error occurred during |
| * setup. |
| */ |
| public static @Nullable MbmsDownloadSession create(@NonNull Context context, |
| final @NonNull MbmsDownloadSessionCallback callback, |
| int subscriptionId, @NonNull Handler handler) { |
| if (!sIsInitialized.compareAndSet(false, true)) { |
| throw new IllegalStateException("Cannot have two active instances"); |
| } |
| MbmsDownloadSession session = |
| new MbmsDownloadSession(context, callback, subscriptionId, handler); |
| final int result = session.bindAndInitialize(); |
| if (result != MbmsErrors.SUCCESS) { |
| sIsInitialized.set(false); |
| handler.post(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.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 |
| * {@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.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 {@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.SUCCESS) { |
| sendErrorToApp(result, null); |
| } |
| } 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 {@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 {@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. |
| * |
| * Asynchronous errors through the callback may include any error not specific to the |
| * streaming use-case. |
| * @param request The request that specifies what should be downloaded. |
| * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error, |
| * and some other error code otherwise. |
| */ |
| public int 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); |
| } |
| |
| try { |
| int result = downloadService.download(request); |
| if (result == MbmsErrors.SUCCESS) { |
| writeDownloadRequestToken(request); |
| } |
| return result; |
| } catch (RemoteException e) { |
| mService.set(null); |
| sIsInitialized.set(false); |
| return MbmsErrors.ERROR_MIDDLEWARE_LOST; |
| } |
| } |
| |
| /** |
| * 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 callback 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 DownloadStateCallback} will be enqueued. |
| * |
| * If the middleware is not aware of the specified download request, |
| * this method will throw an {@link IllegalArgumentException}. |
| * |
| * @param request The {@link DownloadRequest} that you want updates on. |
| * @param callback The callback that should be called when the middleware has information to |
| * share on the download. |
| * @param handler The {@link Handler} on which calls to {@code callback} should be enqueued on. |
| * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error, |
| * and some other error code otherwise. |
| */ |
| public int registerStateCallback(@NonNull DownloadRequest request, |
| @NonNull DownloadStateCallback callback, @NonNull Handler handler) { |
| IMbmsDownloadService downloadService = mService.get(); |
| if (downloadService == null) { |
| throw new IllegalStateException("Middleware not yet bound"); |
| } |
| |
| InternalDownloadStateCallback internalCallback = |
| new InternalDownloadStateCallback(callback, handler); |
| |
| try { |
| int result = downloadService.registerStateCallback(request, internalCallback, |
| callback.getCallbackFilterFlags()); |
| if (result != MbmsErrors.SUCCESS) { |
| if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { |
| throw new IllegalArgumentException("Unknown download request."); |
| } |
| return result; |
| } |
| } catch (RemoteException e) { |
| mService.set(null); |
| sIsInitialized.set(false); |
| return MbmsErrors.ERROR_MIDDLEWARE_LOST; |
| } |
| mInternalDownloadCallbacks.put(callback, internalCallback); |
| return MbmsErrors.SUCCESS; |
| } |
| |
| /** |
| * Un-register a callback previously registered via |
| * {@link #registerStateCallback(DownloadRequest, DownloadStateCallback, Handler)}. 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}. |
| * |
| * @param request The {@link DownloadRequest} provided during registration |
| * @param callback The callback provided during registration. |
| * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error, |
| * and some other error code otherwise. |
| */ |
| public int unregisterStateCallback(@NonNull DownloadRequest request, |
| @NonNull DownloadStateCallback callback) { |
| try { |
| IMbmsDownloadService downloadService = mService.get(); |
| if (downloadService == null) { |
| throw new IllegalStateException("Middleware not yet bound"); |
| } |
| |
| InternalDownloadStateCallback internalCallback = |
| mInternalDownloadCallbacks.get(callback); |
| if (internalCallback == null) { |
| throw new IllegalArgumentException("Provided callback was never registered"); |
| } |
| |
| try { |
| int result = downloadService.unregisterStateCallback(request, internalCallback); |
| if (result != MbmsErrors.SUCCESS) { |
| if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { |
| throw new IllegalArgumentException("Unknown download request."); |
| } |
| return result; |
| } |
| } catch (RemoteException e) { |
| mService.set(null); |
| sIsInitialized.set(false); |
| return MbmsErrors.ERROR_MIDDLEWARE_LOST; |
| } |
| } finally { |
| InternalDownloadStateCallback internalCallback = |
| mInternalDownloadCallbacks.remove(callback); |
| if (internalCallback != null) { |
| internalCallback.stop(); |
| } |
| } |
| return MbmsErrors.SUCCESS; |
| } |
| |
| /** |
| * Attempts to cancel the specified {@link DownloadRequest}. |
| * |
| * If the middleware is not aware of the specified download request, |
| * this method will throw an {@link IllegalArgumentException}. |
| * |
| * @param downloadRequest The download request that you wish to cancel. |
| * @return {@link MbmsErrors#SUCCESS} if the operation did not encounter a synchronous error, |
| * and some other error code otherwise. |
| */ |
| public int 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.SUCCESS) { |
| if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { |
| throw new IllegalArgumentException("Unknown download request."); |
| } |
| } else { |
| deleteDownloadRequestToken(downloadRequest); |
| } |
| return result; |
| } catch (RemoteException e) { |
| mService.set(null); |
| sIsInitialized.set(false); |
| return MbmsErrors.ERROR_MIDDLEWARE_LOST; |
| } |
| } |
| |
| /** |
| * Gets information about the status of a file pending download. |
| * |
| * If there was a problem communicating with the middleware or if it has no records of the |
| * file indicated by {@code fileInfo} being associated with {@code downloadRequest}, |
| * {@link #STATUS_UNKNOWN} will be returned. |
| * |
| * @param downloadRequest The download request to query. |
| * @param fileInfo The particular file within the request to get information on. |
| * @return The status of the download. |
| */ |
| @DownloadStatus |
| public int getDownloadStatus(DownloadRequest downloadRequest, FileInfo fileInfo) { |
| IMbmsDownloadService downloadService = mService.get(); |
| if (downloadService == null) { |
| throw new IllegalStateException("Middleware not yet bound"); |
| } |
| |
| try { |
| return downloadService.getDownloadStatus(downloadRequest, fileInfo); |
| } catch (RemoteException e) { |
| mService.set(null); |
| sIsInitialized.set(false); |
| sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); |
| return STATUS_UNKNOWN; |
| } |
| } |
| |
| /** |
| * 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.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 {@link MbmsDownloadSessionCallback}, but callbacks that have already been |
| * enqueued will still be delivered. |
| * |
| * It is safe to call {@link #create(Context, MbmsDownloadSessionCallback, int, Handler)} to |
| * 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); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException("Failed to create download token for request " + request |
| + " due to IOException " + e); |
| } |
| } |
| |
| 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 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) { |
| try { |
| mInternalCallback.onError(errorCode, message); |
| } catch (RemoteException e) { |
| // Ignore, should not happen locally. |
| } |
| } |
| } |