Home | History | Annotate | Download | only in telephony
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.telephony;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.annotation.SdkConstant;
     23 import android.annotation.SystemApi;
     24 import android.content.ComponentName;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.ServiceConnection;
     28 import android.content.SharedPreferences;
     29 import android.net.Uri;
     30 import android.os.Handler;
     31 import android.os.IBinder;
     32 import android.os.Looper;
     33 import android.os.RemoteException;
     34 import android.telephony.mbms.DownloadStateCallback;
     35 import android.telephony.mbms.FileInfo;
     36 import android.telephony.mbms.DownloadRequest;
     37 import android.telephony.mbms.InternalDownloadSessionCallback;
     38 import android.telephony.mbms.InternalDownloadStateCallback;
     39 import android.telephony.mbms.MbmsDownloadSessionCallback;
     40 import android.telephony.mbms.MbmsDownloadReceiver;
     41 import android.telephony.mbms.MbmsErrors;
     42 import android.telephony.mbms.MbmsTempFileProvider;
     43 import android.telephony.mbms.MbmsUtils;
     44 import android.telephony.mbms.vendor.IMbmsDownloadService;
     45 import android.util.Log;
     46 
     47 import java.io.File;
     48 import java.io.IOException;
     49 import java.lang.annotation.Retention;
     50 import java.lang.annotation.RetentionPolicy;
     51 import java.util.Collections;
     52 import java.util.HashMap;
     53 import java.util.List;
     54 import java.util.Map;
     55 import java.util.concurrent.atomic.AtomicBoolean;
     56 import java.util.concurrent.atomic.AtomicReference;
     57 
     58 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     59 
     60 /**
     61  * This class provides functionality for file download over MBMS.
     62  * @hide
     63  */
     64 public class MbmsDownloadSession implements AutoCloseable {
     65     private static final String LOG_TAG = MbmsDownloadSession.class.getSimpleName();
     66 
     67     /**
     68      * Service action which must be handled by the middleware implementing the MBMS file download
     69      * interface.
     70      * @hide
     71      */
     72     //@SystemApi
     73     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
     74     public static final String MBMS_DOWNLOAD_SERVICE_ACTION =
     75             "android.telephony.action.EmbmsDownload";
     76 
     77     /**
     78      * Integer extra that Android will attach to the intent supplied via
     79      * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}
     80      * Indicates the result code of the download. One of
     81      * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, {@link #RESULT_CANCELLED}, or
     82      * {@link #RESULT_IO_ERROR}.
     83      *
     84      * This extra may also be used by the middleware when it is sending intents to the app.
     85      */
     86     public static final String EXTRA_MBMS_DOWNLOAD_RESULT =
     87             "android.telephony.extra.MBMS_DOWNLOAD_RESULT";
     88 
     89     /**
     90      * {@link FileInfo} extra that Android will attach to the intent supplied via
     91      * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}
     92      * Indicates the file for which the download result is for. Never null.
     93      *
     94      * This extra may also be used by the middleware when it is sending intents to the app.
     95      */
     96     public static final String EXTRA_MBMS_FILE_INFO = "android.telephony.extra.MBMS_FILE_INFO";
     97 
     98     /**
     99      * {@link Uri} extra that Android will attach to the intent supplied via
    100      * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}
    101      * Indicates the location of the successfully downloaded file within the temp file root set
    102      * via {@link #setTempFileRootDirectory(File)}.
    103      * While you may use this file in-place, it is highly encouraged that you move
    104      * this file to a different location after receiving the download completion intent, as this
    105      * file resides within the temp file directory.
    106      *
    107      * Will always be set to a non-null value if
    108      * {@link #EXTRA_MBMS_DOWNLOAD_RESULT} is set to {@link #RESULT_SUCCESSFUL}.
    109      */
    110     public static final String EXTRA_MBMS_COMPLETED_FILE_URI =
    111             "android.telephony.extra.MBMS_COMPLETED_FILE_URI";
    112 
    113     /**
    114      * Extra containing the {@link DownloadRequest} for which the download result or file
    115      * descriptor request is for. Must not be null.
    116      */
    117     public static final String EXTRA_MBMS_DOWNLOAD_REQUEST =
    118             "android.telephony.extra.MBMS_DOWNLOAD_REQUEST";
    119 
    120     /**
    121      * The default directory name for all MBMS temp files. If you call
    122      * {@link #download(DownloadRequest)} without first calling
    123      * {@link #setTempFileRootDirectory(File)}, this directory will be created for you under the
    124      * path returned by {@link Context#getFilesDir()}.
    125      */
    126     public static final String DEFAULT_TOP_LEVEL_TEMP_DIRECTORY = "androidMbmsTempFileRoot";
    127 
    128     /**
    129      * Indicates that the download was successful.
    130      */
    131     public static final int RESULT_SUCCESSFUL = 1;
    132 
    133     /**
    134      * Indicates that the download was cancelled via {@link #cancelDownload(DownloadRequest)}.
    135      */
    136     public static final int RESULT_CANCELLED = 2;
    137 
    138     /**
    139      * Indicates that the download will not be completed due to the expiration of its download
    140      * window on the carrier's network.
    141      */
    142     public static final int RESULT_EXPIRED = 3;
    143 
    144     /**
    145      * Indicates that the download will not be completed due to an I/O error incurred while
    146      * writing to temp files. This commonly indicates that the device is out of storage space,
    147      * but may indicate other conditions as well (such as an SD card being removed).
    148      */
    149     public static final int RESULT_IO_ERROR = 4;
    150     // TODO - more results!
    151 
    152     /** @hide */
    153     @Retention(RetentionPolicy.SOURCE)
    154     @IntDef({STATUS_UNKNOWN, STATUS_ACTIVELY_DOWNLOADING, STATUS_PENDING_DOWNLOAD,
    155             STATUS_PENDING_REPAIR, STATUS_PENDING_DOWNLOAD_WINDOW})
    156     public @interface DownloadStatus {}
    157 
    158     /**
    159      * Indicates that the middleware has no information on the file.
    160      */
    161     public static final int STATUS_UNKNOWN = 0;
    162 
    163     /**
    164      * Indicates that the file is actively downloading.
    165      */
    166     public static final int STATUS_ACTIVELY_DOWNLOADING = 1;
    167 
    168     /**
    169      * TODO: I don't know...
    170      */
    171     public static final int STATUS_PENDING_DOWNLOAD = 2;
    172 
    173     /**
    174      * Indicates that the file is being repaired after the download being interrupted.
    175      */
    176     public static final int STATUS_PENDING_REPAIR = 3;
    177 
    178     /**
    179      * Indicates that the file is waiting to download because its download window has not yet
    180      * started.
    181      */
    182     public static final int STATUS_PENDING_DOWNLOAD_WINDOW = 4;
    183 
    184     private static AtomicBoolean sIsInitialized = new AtomicBoolean(false);
    185 
    186     private final Context mContext;
    187     private int mSubscriptionId = INVALID_SUBSCRIPTION_ID;
    188     private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    189         @Override
    190         public void binderDied() {
    191             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification");
    192         }
    193     };
    194 
    195     private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null);
    196     private final InternalDownloadSessionCallback mInternalCallback;
    197     private final Map<DownloadStateCallback, InternalDownloadStateCallback>
    198             mInternalDownloadCallbacks = new HashMap<>();
    199 
    200     private MbmsDownloadSession(Context context, MbmsDownloadSessionCallback callback,
    201             int subscriptionId, Handler handler) {
    202         mContext = context;
    203         mSubscriptionId = subscriptionId;
    204         if (handler == null) {
    205             handler = new Handler(Looper.getMainLooper());
    206         }
    207         mInternalCallback = new InternalDownloadSessionCallback(callback, handler);
    208     }
    209 
    210     /**
    211      * Create a new {@link MbmsDownloadSession} using the system default data subscription ID.
    212      * See {@link #create(Context, MbmsDownloadSessionCallback, int, Handler)}
    213      */
    214     public static MbmsDownloadSession create(@NonNull Context context,
    215             @NonNull MbmsDownloadSessionCallback callback, @NonNull Handler handler) {
    216         return create(context, callback, SubscriptionManager.getDefaultSubscriptionId(), handler);
    217     }
    218 
    219     /**
    220      * Create a new MbmsDownloadManager using the given subscription ID.
    221      *
    222      * Note that this call will bind a remote service and that may take a bit. The instance of
    223      * {@link MbmsDownloadSession} that is returned will not be ready for use until
    224      * {@link MbmsDownloadSessionCallback#onMiddlewareReady()} is called on the provided callback.
    225      * If you attempt to use the instance before it is ready, an {@link IllegalStateException}
    226      * will be thrown or an error will be delivered through
    227      * {@link MbmsDownloadSessionCallback#onError(int, String)}.
    228      *
    229      * This also may throw an {@link IllegalArgumentException}.
    230      *
    231      * You may only have one instance of {@link MbmsDownloadSession} per UID. If you call this
    232      * method while there is an active instance of {@link MbmsDownloadSession} in your process
    233      * (in other words, one that has not had {@link #close()} called on it), this method will
    234      * throw an {@link IllegalStateException}. If you call this method in a different process
    235      * running under the same UID, an error will be indicated via
    236      * {@link MbmsDownloadSessionCallback#onError(int, String)}.
    237      *
    238      * Note that initialization may fail asynchronously. If you wish to try again after you
    239      * receive such an asynchronous error, you must call {@link #close()} on the instance of
    240      * {@link MbmsDownloadSession} that you received before calling this method again.
    241      *
    242      * @param context The instance of {@link Context} to use
    243      * @param callback A callback to get asynchronous error messages and file service updates.
    244      * @param subscriptionId The data subscription ID to use
    245      * @param handler The {@link Handler} on which callbacks should be enqueued.
    246      * @return A new instance of {@link MbmsDownloadSession}, or null if an error occurred during
    247      * setup.
    248      */
    249     public static @Nullable MbmsDownloadSession create(@NonNull Context context,
    250             final @NonNull MbmsDownloadSessionCallback callback,
    251             int subscriptionId, @NonNull Handler handler) {
    252         if (!sIsInitialized.compareAndSet(false, true)) {
    253             throw new IllegalStateException("Cannot have two active instances");
    254         }
    255         MbmsDownloadSession session =
    256                 new MbmsDownloadSession(context, callback, subscriptionId, handler);
    257         final int result = session.bindAndInitialize();
    258         if (result != MbmsErrors.SUCCESS) {
    259             sIsInitialized.set(false);
    260             handler.post(new Runnable() {
    261                 @Override
    262                 public void run() {
    263                     callback.onError(result, null);
    264                 }
    265             });
    266             return null;
    267         }
    268         return session;
    269     }
    270 
    271     private int bindAndInitialize() {
    272         return MbmsUtils.startBinding(mContext, MBMS_DOWNLOAD_SERVICE_ACTION,
    273                 new ServiceConnection() {
    274                     @Override
    275                     public void onServiceConnected(ComponentName name, IBinder service) {
    276                         IMbmsDownloadService downloadService =
    277                                 IMbmsDownloadService.Stub.asInterface(service);
    278                         int result;
    279                         try {
    280                             result = downloadService.initialize(mSubscriptionId, mInternalCallback);
    281                         } catch (RemoteException e) {
    282                             Log.e(LOG_TAG, "Service died before initialization");
    283                             sIsInitialized.set(false);
    284                             return;
    285                         } catch (RuntimeException e) {
    286                             Log.e(LOG_TAG, "Runtime exception during initialization");
    287                             sendErrorToApp(
    288                                     MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE,
    289                                     e.toString());
    290                             sIsInitialized.set(false);
    291                             return;
    292                         }
    293                         if (result != MbmsErrors.SUCCESS) {
    294                             sendErrorToApp(result, "Error returned during initialization");
    295                             sIsInitialized.set(false);
    296                             return;
    297                         }
    298                         try {
    299                             downloadService.asBinder().linkToDeath(mDeathRecipient, 0);
    300                         } catch (RemoteException e) {
    301                             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST,
    302                                     "Middleware lost during initialization");
    303                             sIsInitialized.set(false);
    304                             return;
    305                         }
    306                         mService.set(downloadService);
    307                     }
    308 
    309                     @Override
    310                     public void onServiceDisconnected(ComponentName name) {
    311                         sIsInitialized.set(false);
    312                         mService.set(null);
    313                     }
    314                 });
    315     }
    316 
    317     /**
    318      * An inspection API to retrieve the list of available
    319      * {@link android.telephony.mbms.FileServiceInfo}s currently being advertised.
    320      * The results are returned asynchronously via a call to
    321      * {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)}
    322      *
    323      * Asynchronous error codes via the {@link MbmsDownloadSessionCallback#onError(int, String)}
    324      * callback may include any of the errors that are not specific to the streaming use-case.
    325      *
    326      * May throw an {@link IllegalStateException} or {@link IllegalArgumentException}.
    327      *
    328      * @param classList A list of service classes which the app wishes to receive
    329      *                  {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)} callbacks
    330      *                  about. Subsequent calls to this method will replace this list of service
    331      *                  classes (i.e. the middleware will no longer send updates for services
    332      *                  matching classes only in the old list).
    333      *                  Values in this list should be negotiated with the wireless carrier prior
    334      *                  to using this API.
    335      */
    336     public void requestUpdateFileServices(@NonNull List<String> classList) {
    337         IMbmsDownloadService downloadService = mService.get();
    338         if (downloadService == null) {
    339             throw new IllegalStateException("Middleware not yet bound");
    340         }
    341         try {
    342             int returnCode = downloadService.requestUpdateFileServices(mSubscriptionId, classList);
    343             if (returnCode != MbmsErrors.SUCCESS) {
    344                 sendErrorToApp(returnCode, null);
    345             }
    346         } catch (RemoteException e) {
    347             Log.w(LOG_TAG, "Remote process died");
    348             mService.set(null);
    349             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    350         }
    351     }
    352 
    353     /**
    354      * Sets the temp file root for downloads.
    355      * All temp files created for the middleware to write to will be contained in the specified
    356      * directory. Applications that wish to specify a location only need to call this method once
    357      * as long their data is persisted in storage -- the argument will be stored both in a
    358      * local instance of {@link android.content.SharedPreferences} and by the middleware.
    359      *
    360      * If this method is not called at least once before calling
    361      * {@link #download(DownloadRequest)}, the framework
    362      * will default to a directory formed by the concatenation of the app's files directory and
    363      * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}.
    364      *
    365      * Before calling this method, the app must cancel all of its pending
    366      * {@link DownloadRequest}s via {@link #cancelDownload(DownloadRequest)}. If this is not done,
    367      * you will receive an asynchronous error with code
    368      * {@link MbmsErrors.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} unless the
    369      * provided directory is the same as what has been previously configured.
    370      *
    371      * The {@link File} supplied as a root temp file directory must already exist. If not, an
    372      * {@link IllegalArgumentException} will be thrown. In addition, as an additional sanity
    373      * check, an {@link IllegalArgumentException} will be thrown if you attempt to set the temp
    374      * file root directory to one of your data roots (the value of {@link Context#getDataDir()},
    375      * {@link Context#getFilesDir()}, or {@link Context#getCacheDir()}).
    376      * @param tempFileRootDirectory A directory to place temp files in.
    377      */
    378     public void setTempFileRootDirectory(@NonNull File tempFileRootDirectory) {
    379         IMbmsDownloadService downloadService = mService.get();
    380         if (downloadService == null) {
    381             throw new IllegalStateException("Middleware not yet bound");
    382         }
    383         try {
    384             validateTempFileRootSanity(tempFileRootDirectory);
    385         } catch (IOException e) {
    386             throw new IllegalStateException("Got IOException checking directory sanity");
    387         }
    388         String filePath;
    389         try {
    390             filePath = tempFileRootDirectory.getCanonicalPath();
    391         } catch (IOException e) {
    392             throw new IllegalArgumentException("Unable to canonicalize the provided path: " + e);
    393         }
    394 
    395         try {
    396             int result = downloadService.setTempFileRootDirectory(mSubscriptionId, filePath);
    397             if (result != MbmsErrors.SUCCESS) {
    398                 sendErrorToApp(result, null);
    399             }
    400         } catch (RemoteException e) {
    401             mService.set(null);
    402             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    403             return;
    404         }
    405 
    406         SharedPreferences prefs = mContext.getSharedPreferences(
    407                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
    408         prefs.edit().putString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, filePath).apply();
    409     }
    410 
    411     private void validateTempFileRootSanity(File tempFileRootDirectory) throws IOException {
    412         if (!tempFileRootDirectory.exists()) {
    413             throw new IllegalArgumentException("Provided directory does not exist");
    414         }
    415         if (!tempFileRootDirectory.isDirectory()) {
    416             throw new IllegalArgumentException("Provided File is not a directory");
    417         }
    418         String canonicalTempFilePath = tempFileRootDirectory.getCanonicalPath();
    419         if (mContext.getDataDir().getCanonicalPath().equals(canonicalTempFilePath)) {
    420             throw new IllegalArgumentException("Temp file root cannot be your data dir");
    421         }
    422         if (mContext.getCacheDir().getCanonicalPath().equals(canonicalTempFilePath)) {
    423             throw new IllegalArgumentException("Temp file root cannot be your cache dir");
    424         }
    425         if (mContext.getFilesDir().getCanonicalPath().equals(canonicalTempFilePath)) {
    426             throw new IllegalArgumentException("Temp file root cannot be your files dir");
    427         }
    428     }
    429     /**
    430      * Retrieves the currently configured temp file root directory. Returns the file that was
    431      * configured via {@link #setTempFileRootDirectory(File)} or the default directory
    432      * {@link #download(DownloadRequest)} was called without ever
    433      * setting the temp file root. If neither method has been called since the last time the app's
    434      * shared preferences were reset, returns {@code null}.
    435      *
    436      * @return A {@link File} pointing to the configured temp file directory, or null if not yet
    437      *         configured.
    438      */
    439     public @Nullable File getTempFileRootDirectory() {
    440         SharedPreferences prefs = mContext.getSharedPreferences(
    441                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
    442         String path = prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null);
    443         if (path != null) {
    444             return new File(path);
    445         }
    446         return null;
    447     }
    448 
    449     /**
    450      * Requests the download of a file or set of files that the carrier has indicated to be
    451      * available.
    452      *
    453      * May throw an {@link IllegalArgumentException}
    454      *
    455      * If {@link #setTempFileRootDirectory(File)} has not called after the app has been installed,
    456      * this method will create a directory at the default location defined at
    457      * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp
    458      * file root directory.
    459      *
    460      * Asynchronous errors through the callback may include any error not specific to the
    461      * streaming use-case.
    462      * @param request The request that specifies what should be downloaded.
    463      */
    464     public void download(@NonNull DownloadRequest request) {
    465         IMbmsDownloadService downloadService = mService.get();
    466         if (downloadService == null) {
    467             throw new IllegalStateException("Middleware not yet bound");
    468         }
    469 
    470         // Check to see whether the app's set a temp root dir yet, and set it if not.
    471         SharedPreferences prefs = mContext.getSharedPreferences(
    472                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
    473         if (prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null) == null) {
    474             File tempRootDirectory = new File(mContext.getFilesDir(),
    475                     DEFAULT_TOP_LEVEL_TEMP_DIRECTORY);
    476             tempRootDirectory.mkdirs();
    477             setTempFileRootDirectory(tempRootDirectory);
    478         }
    479 
    480         writeDownloadRequestToken(request);
    481         try {
    482             downloadService.download(request);
    483         } catch (RemoteException e) {
    484             mService.set(null);
    485             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    486         }
    487     }
    488 
    489     /**
    490      * Returns a list of pending {@link DownloadRequest}s that originated from this application.
    491      * A pending request is one that was issued via
    492      * {@link #download(DownloadRequest)} but not cancelled through
    493      * {@link #cancelDownload(DownloadRequest)}.
    494      * @return A list, possibly empty, of {@link DownloadRequest}s
    495      */
    496     public @NonNull List<DownloadRequest> listPendingDownloads() {
    497         IMbmsDownloadService downloadService = mService.get();
    498         if (downloadService == null) {
    499             throw new IllegalStateException("Middleware not yet bound");
    500         }
    501 
    502         try {
    503             return downloadService.listPendingDownloads(mSubscriptionId);
    504         } catch (RemoteException e) {
    505             mService.set(null);
    506             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    507             return Collections.emptyList();
    508         }
    509     }
    510 
    511     /**
    512      * Registers a callback for a {@link DownloadRequest} previously requested via
    513      * {@link #download(DownloadRequest)}. This callback will only be called as long as both this
    514      * app and the middleware are both running -- if either one stops, no further calls on the
    515      * provided {@link DownloadStateCallback} will be enqueued.
    516      *
    517      * If the middleware is not aware of the specified download request,
    518      * this method will throw an {@link IllegalArgumentException}.
    519      *
    520      * @param request The {@link DownloadRequest} that you want updates on.
    521      * @param callback The callback that should be called when the middleware has information to
    522      *                 share on the download.
    523      * @param handler The {@link Handler} on which calls to {@code callback} should be enqueued on.
    524      */
    525     public void registerStateCallback(@NonNull DownloadRequest request,
    526             @NonNull DownloadStateCallback callback, @NonNull Handler handler) {
    527         IMbmsDownloadService downloadService = mService.get();
    528         if (downloadService == null) {
    529             throw new IllegalStateException("Middleware not yet bound");
    530         }
    531 
    532         InternalDownloadStateCallback internalCallback =
    533                 new InternalDownloadStateCallback(callback, handler);
    534 
    535         try {
    536             int result = downloadService.registerStateCallback(request, internalCallback,
    537                     callback.getCallbackFilterFlags());
    538             if (result != MbmsErrors.SUCCESS) {
    539                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    540                     throw new IllegalArgumentException("Unknown download request.");
    541                 }
    542                 sendErrorToApp(result, null);
    543                 return;
    544             }
    545         } catch (RemoteException e) {
    546             mService.set(null);
    547             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    548             return;
    549         }
    550         mInternalDownloadCallbacks.put(callback, internalCallback);
    551     }
    552 
    553     /**
    554      * Un-register a callback previously registered via
    555      * {@link #registerStateCallback(DownloadRequest, DownloadStateCallback, Handler)}. After
    556      * this method is called, no further callbacks will be enqueued on the {@link Handler}
    557      * provided upon registration, even if this method throws an exception.
    558      *
    559      * If the middleware is not aware of the specified download request,
    560      * this method will throw an {@link IllegalArgumentException}.
    561      *
    562      * @param request The {@link DownloadRequest} provided during registration
    563      * @param callback The callback provided during registration.
    564      */
    565     public void unregisterStateCallback(@NonNull DownloadRequest request,
    566             @NonNull DownloadStateCallback callback) {
    567         try {
    568             IMbmsDownloadService downloadService = mService.get();
    569             if (downloadService == null) {
    570                 throw new IllegalStateException("Middleware not yet bound");
    571             }
    572 
    573             InternalDownloadStateCallback internalCallback =
    574                     mInternalDownloadCallbacks.get(callback);
    575 
    576             try {
    577                 int result = downloadService.unregisterStateCallback(request, internalCallback);
    578                 if (result != MbmsErrors.SUCCESS) {
    579                     if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    580                         throw new IllegalArgumentException("Unknown download request.");
    581                     }
    582                     sendErrorToApp(result, null);
    583                 }
    584             } catch (RemoteException e) {
    585                 mService.set(null);
    586                 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    587             }
    588         } finally {
    589             InternalDownloadStateCallback internalCallback =
    590                     mInternalDownloadCallbacks.remove(callback);
    591             if (internalCallback != null) {
    592                 internalCallback.stop();
    593             }
    594         }
    595     }
    596 
    597     /**
    598      * Attempts to cancel the specified {@link DownloadRequest}.
    599      *
    600      * If the middleware is not aware of the specified download request,
    601      * this method will throw an {@link IllegalArgumentException}.
    602      *
    603      * @param downloadRequest The download request that you wish to cancel.
    604      */
    605     public void cancelDownload(@NonNull DownloadRequest downloadRequest) {
    606         IMbmsDownloadService downloadService = mService.get();
    607         if (downloadService == null) {
    608             throw new IllegalStateException("Middleware not yet bound");
    609         }
    610 
    611         try {
    612             int result = downloadService.cancelDownload(downloadRequest);
    613             if (result != MbmsErrors.SUCCESS) {
    614                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    615                     throw new IllegalArgumentException("Unknown download request.");
    616                 }
    617                 sendErrorToApp(result, null);
    618                 return;
    619             }
    620         } catch (RemoteException e) {
    621             mService.set(null);
    622             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    623             return;
    624         }
    625         deleteDownloadRequestToken(downloadRequest);
    626     }
    627 
    628     /**
    629      * Gets information about the status of a file pending download.
    630      *
    631      * If there was a problem communicating with the middleware or if it has no records of the
    632      * file indicated by {@code fileInfo} being associated with {@code downloadRequest},
    633      * {@link #STATUS_UNKNOWN} will be returned.
    634      *
    635      * @param downloadRequest The download request to query.
    636      * @param fileInfo The particular file within the request to get information on.
    637      * @return The status of the download.
    638      */
    639     @DownloadStatus
    640     public int getDownloadStatus(DownloadRequest downloadRequest, FileInfo fileInfo) {
    641         IMbmsDownloadService downloadService = mService.get();
    642         if (downloadService == null) {
    643             throw new IllegalStateException("Middleware not yet bound");
    644         }
    645 
    646         try {
    647             return downloadService.getDownloadStatus(downloadRequest, fileInfo);
    648         } catch (RemoteException e) {
    649             mService.set(null);
    650             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    651             return STATUS_UNKNOWN;
    652         }
    653     }
    654 
    655     /**
    656      * Resets the middleware's knowledge of previously-downloaded files in this download request.
    657      *
    658      * Normally, the middleware keeps track of the hashes of downloaded files and won't re-download
    659      * files whose server-reported hash matches one of the already-downloaded files. This means
    660      * that if the file is accidentally deleted by the user or by the app, the middleware will
    661      * not try to download it again.
    662      * This method will reset the middleware's cache of hashes for the provided
    663      * {@link DownloadRequest}, so that previously downloaded content will be downloaded again
    664      * when available.
    665      * This will not interrupt in-progress downloads.
    666      *
    667      * This is distinct from cancelling and re-issuing the download request -- if you cancel and
    668      * re-issue, the middleware will not clear its cache of download state information.
    669      *
    670      * If the middleware is not aware of the specified download request, an
    671      * {@link IllegalArgumentException} will be thrown.
    672      *
    673      * @param downloadRequest The request to re-download files for.
    674      */
    675     public void resetDownloadKnowledge(DownloadRequest downloadRequest) {
    676         IMbmsDownloadService downloadService = mService.get();
    677         if (downloadService == null) {
    678             throw new IllegalStateException("Middleware not yet bound");
    679         }
    680 
    681         try {
    682             int result = downloadService.resetDownloadKnowledge(downloadRequest);
    683             if (result != MbmsErrors.SUCCESS) {
    684                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    685                     throw new IllegalArgumentException("Unknown download request.");
    686                 }
    687                 sendErrorToApp(result, null);
    688             }
    689         } catch (RemoteException e) {
    690             mService.set(null);
    691             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    692         }
    693     }
    694 
    695     /**
    696      * Terminates this instance.
    697      *
    698      * After this method returns,
    699      * no further callbacks originating from the middleware will be enqueued on the provided
    700      * instance of {@link MbmsDownloadSessionCallback}, but callbacks that have already been
    701      * enqueued will still be delivered.
    702      *
    703      * It is safe to call {@link #create(Context, MbmsDownloadSessionCallback, int, Handler)} to
    704      * obtain another instance of {@link MbmsDownloadSession} immediately after this method
    705      * returns.
    706      *
    707      * May throw an {@link IllegalStateException}
    708      */
    709     @Override
    710     public void close() {
    711         try {
    712             IMbmsDownloadService downloadService = mService.get();
    713             if (downloadService == null) {
    714                 Log.i(LOG_TAG, "Service already dead");
    715                 return;
    716             }
    717             downloadService.dispose(mSubscriptionId);
    718         } catch (RemoteException e) {
    719             // Ignore
    720             Log.i(LOG_TAG, "Remote exception while disposing of service");
    721         } finally {
    722             mService.set(null);
    723             sIsInitialized.set(false);
    724             mInternalCallback.stop();
    725         }
    726     }
    727 
    728     private void writeDownloadRequestToken(DownloadRequest request) {
    729         File token = getDownloadRequestTokenPath(request);
    730         if (!token.getParentFile().exists()) {
    731             token.getParentFile().mkdirs();
    732         }
    733         if (token.exists()) {
    734             Log.w(LOG_TAG, "Download token " + token.getName() + " already exists");
    735             return;
    736         }
    737         try {
    738             if (!token.createNewFile()) {
    739                 throw new RuntimeException("Failed to create download token for request "
    740                         + request);
    741             }
    742         } catch (IOException e) {
    743             throw new RuntimeException("Failed to create download token for request " + request
    744                     + " due to IOException " + e);
    745         }
    746     }
    747 
    748     private void deleteDownloadRequestToken(DownloadRequest request) {
    749         File token = getDownloadRequestTokenPath(request);
    750         if (!token.isFile()) {
    751             Log.w(LOG_TAG, "Attempting to delete non-existent download token at " + token);
    752             return;
    753         }
    754         if (!token.delete()) {
    755             Log.w(LOG_TAG, "Couldn't delete download token at " + token);
    756         }
    757     }
    758 
    759     private File getDownloadRequestTokenPath(DownloadRequest request) {
    760         File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext,
    761                 request.getFileServiceId());
    762         String downloadTokenFileName = request.getHash()
    763                 + MbmsDownloadReceiver.DOWNLOAD_TOKEN_SUFFIX;
    764         return new File(tempFileLocation, downloadTokenFileName);
    765     }
    766 
    767     private void sendErrorToApp(int errorCode, String message) {
    768         try {
    769             mInternalCallback.onError(errorCode, message);
    770         } catch (RemoteException e) {
    771             // Ignore, should not happen locally.
    772         }
    773     }
    774 }
    775