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 static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     20 
     21 import android.annotation.IntDef;
     22 import android.annotation.NonNull;
     23 import android.annotation.Nullable;
     24 import android.annotation.SdkConstant;
     25 import android.annotation.SystemApi;
     26 import android.annotation.TestApi;
     27 import android.content.ComponentName;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.ServiceConnection;
     31 import android.content.SharedPreferences;
     32 import android.net.Uri;
     33 import android.os.Handler;
     34 import android.os.IBinder;
     35 import android.os.RemoteException;
     36 import android.telephony.mbms.DownloadProgressListener;
     37 import android.telephony.mbms.DownloadRequest;
     38 import android.telephony.mbms.DownloadStatusListener;
     39 import android.telephony.mbms.FileInfo;
     40 import android.telephony.mbms.InternalDownloadProgressListener;
     41 import android.telephony.mbms.InternalDownloadSessionCallback;
     42 import android.telephony.mbms.InternalDownloadStatusListener;
     43 import android.telephony.mbms.MbmsDownloadReceiver;
     44 import android.telephony.mbms.MbmsDownloadSessionCallback;
     45 import android.telephony.mbms.MbmsErrors;
     46 import android.telephony.mbms.MbmsTempFileProvider;
     47 import android.telephony.mbms.MbmsUtils;
     48 import android.telephony.mbms.vendor.IMbmsDownloadService;
     49 import android.util.Log;
     50 
     51 import java.io.File;
     52 import java.io.IOException;
     53 import java.lang.annotation.Retention;
     54 import java.lang.annotation.RetentionPolicy;
     55 import java.util.Collections;
     56 import java.util.HashMap;
     57 import java.util.List;
     58 import java.util.Map;
     59 import java.util.concurrent.Executor;
     60 import java.util.concurrent.atomic.AtomicBoolean;
     61 import java.util.concurrent.atomic.AtomicReference;
     62 
     63 /**
     64  * This class provides functionality for file download over MBMS.
     65  */
     66 public class MbmsDownloadSession implements AutoCloseable {
     67     private static final String LOG_TAG = MbmsDownloadSession.class.getSimpleName();
     68 
     69     /**
     70      * Service action which must be handled by the middleware implementing the MBMS file download
     71      * interface.
     72      * @hide
     73      */
     74     @SystemApi
     75     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
     76     public static final String MBMS_DOWNLOAD_SERVICE_ACTION =
     77             "android.telephony.action.EmbmsDownload";
     78 
     79     /**
     80      * Metadata key that specifies the component name of the service to bind to for file-download.
     81      * @hide
     82      */
     83     @TestApi
     84     public static final String MBMS_DOWNLOAD_SERVICE_OVERRIDE_METADATA =
     85             "mbms-download-service-override";
     86 
     87     /**
     88      * Integer extra that Android will attach to the intent supplied via
     89      * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}
     90      * Indicates the result code of the download. One of
     91      * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, {@link #RESULT_CANCELLED},
     92      * {@link #RESULT_IO_ERROR}, {@link #RESULT_DOWNLOAD_FAILURE}, {@link #RESULT_OUT_OF_STORAGE},
     93      * {@link #RESULT_SERVICE_ID_NOT_DEFINED}, or {@link #RESULT_FILE_ROOT_UNREACHABLE}.
     94      *
     95      * This extra may also be used by the middleware when it is sending intents to the app.
     96      */
     97     public static final String EXTRA_MBMS_DOWNLOAD_RESULT =
     98             "android.telephony.extra.MBMS_DOWNLOAD_RESULT";
     99 
    100     /**
    101      * {@link FileInfo} extra that Android will attach to the intent supplied via
    102      * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}
    103      * Indicates the file for which the download result is for. Never null.
    104      *
    105      * This extra may also be used by the middleware when it is sending intents to the app.
    106      */
    107     public static final String EXTRA_MBMS_FILE_INFO = "android.telephony.extra.MBMS_FILE_INFO";
    108 
    109     /**
    110      * {@link Uri} extra that Android will attach to the intent supplied via
    111      * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}
    112      * Indicates the location of the successfully downloaded file within the directory that the
    113      * app provided via the builder.
    114      *
    115      * Will always be set to a non-null value if
    116      * {@link #EXTRA_MBMS_DOWNLOAD_RESULT} is set to {@link #RESULT_SUCCESSFUL}.
    117      */
    118     public static final String EXTRA_MBMS_COMPLETED_FILE_URI =
    119             "android.telephony.extra.MBMS_COMPLETED_FILE_URI";
    120 
    121     /**
    122      * Extra containing the {@link DownloadRequest} for which the download result or file
    123      * descriptor request is for. Must not be null.
    124      */
    125     public static final String EXTRA_MBMS_DOWNLOAD_REQUEST =
    126             "android.telephony.extra.MBMS_DOWNLOAD_REQUEST";
    127 
    128     /**
    129      * The default directory name for all MBMS temp files. If you call
    130      * {@link #download(DownloadRequest)} without first calling
    131      * {@link #setTempFileRootDirectory(File)}, this directory will be created for you under the
    132      * path returned by {@link Context#getFilesDir()}.
    133      */
    134     public static final String DEFAULT_TOP_LEVEL_TEMP_DIRECTORY = "androidMbmsTempFileRoot";
    135 
    136 
    137     /** @hide */
    138     @Retention(RetentionPolicy.SOURCE)
    139     @IntDef(value = {RESULT_SUCCESSFUL, RESULT_CANCELLED, RESULT_EXPIRED, RESULT_IO_ERROR,
    140             RESULT_SERVICE_ID_NOT_DEFINED, RESULT_DOWNLOAD_FAILURE, RESULT_OUT_OF_STORAGE,
    141             RESULT_FILE_ROOT_UNREACHABLE}, prefix = { "RESULT_" })
    142     public @interface DownloadResultCode{}
    143 
    144     /**
    145      * Indicates that the download was successful.
    146      */
    147     public static final int RESULT_SUCCESSFUL = 1;
    148 
    149     /**
    150      * Indicates that the download was cancelled via {@link #cancelDownload(DownloadRequest)}.
    151      */
    152     public static final int RESULT_CANCELLED = 2;
    153 
    154     /**
    155      * Indicates that the download will not be completed due to the expiration of its download
    156      * window on the carrier's network.
    157      */
    158     public static final int RESULT_EXPIRED = 3;
    159 
    160     /**
    161      * Indicates that the download will not be completed due to an I/O error incurred while
    162      * writing to temp files.
    163      *
    164      * This is likely a transient error and another {@link DownloadRequest} should be sent to try
    165      * the download again.
    166      */
    167     public static final int RESULT_IO_ERROR = 4;
    168 
    169     /**
    170      * Indicates that the Service ID specified in the {@link DownloadRequest} is incorrect due to
    171      * the Id being incorrect, stale, expired, or similar.
    172      */
    173     public static final int RESULT_SERVICE_ID_NOT_DEFINED = 5;
    174 
    175     /**
    176      * Indicates that there was an error while processing downloaded files, such as a file repair or
    177      * file decoding error and is not due to a file I/O error.
    178      *
    179      * This is likely a transient error and another {@link DownloadRequest} should be sent to try
    180      * the download again.
    181      */
    182     public static final int RESULT_DOWNLOAD_FAILURE = 6;
    183 
    184     /**
    185      * Indicates that the file system is full and the {@link DownloadRequest} can not complete.
    186      * Either space must be made on the current file system or the temp file root location must be
    187      * changed to a location that is not full to download the temp files.
    188      */
    189     public static final int RESULT_OUT_OF_STORAGE = 7;
    190 
    191     /**
    192      * Indicates that the file root that was set is currently unreachable. This can happen if the
    193      * temp files are set to be stored on external storage and the SD card was removed, for example.
    194      * The temp file root should be changed before sending another DownloadRequest.
    195      */
    196     public static final int RESULT_FILE_ROOT_UNREACHABLE = 8;
    197 
    198     /** @hide */
    199     @Retention(RetentionPolicy.SOURCE)
    200     @IntDef({STATUS_UNKNOWN, STATUS_ACTIVELY_DOWNLOADING, STATUS_PENDING_DOWNLOAD,
    201             STATUS_PENDING_REPAIR, STATUS_PENDING_DOWNLOAD_WINDOW})
    202     public @interface DownloadStatus {}
    203 
    204     /**
    205      * Indicates that the middleware has no information on the file.
    206      */
    207     public static final int STATUS_UNKNOWN = 0;
    208 
    209     /**
    210      * Indicates that the file is actively being downloaded.
    211      */
    212     public static final int STATUS_ACTIVELY_DOWNLOADING = 1;
    213 
    214     /**
    215      * Indicates that the file is awaiting the next download or repair operations. When a more
    216      * precise status is known, the status will change to either {@link #STATUS_PENDING_REPAIR} or
    217      * {@link #STATUS_PENDING_DOWNLOAD_WINDOW}.
    218      */
    219     public static final int STATUS_PENDING_DOWNLOAD = 2;
    220 
    221     /**
    222      * Indicates that the file is awaiting file repair after the download has ended.
    223      */
    224     public static final int STATUS_PENDING_REPAIR = 3;
    225 
    226     /**
    227      * Indicates that the file is waiting to download because its download window has not yet
    228      * started and is scheduled for a future time.
    229      */
    230     public static final int STATUS_PENDING_DOWNLOAD_WINDOW = 4;
    231 
    232     private static final String DESTINATION_SANITY_CHECK_FILE_NAME = "destinationSanityCheckFile";
    233 
    234     private static AtomicBoolean sIsInitialized = new AtomicBoolean(false);
    235 
    236     private final Context mContext;
    237     private int mSubscriptionId = INVALID_SUBSCRIPTION_ID;
    238     private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    239         @Override
    240         public void binderDied() {
    241             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification");
    242         }
    243     };
    244 
    245     private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null);
    246     private final InternalDownloadSessionCallback mInternalCallback;
    247     private final Map<DownloadStatusListener, InternalDownloadStatusListener>
    248             mInternalDownloadStatusListeners = new HashMap<>();
    249     private final Map<DownloadProgressListener, InternalDownloadProgressListener>
    250             mInternalDownloadProgressListeners = new HashMap<>();
    251 
    252     private MbmsDownloadSession(Context context, Executor executor, int subscriptionId,
    253             MbmsDownloadSessionCallback callback) {
    254         mContext = context;
    255         mSubscriptionId = subscriptionId;
    256         mInternalCallback = new InternalDownloadSessionCallback(callback, executor);
    257     }
    258 
    259     /**
    260      * Create a new {@link MbmsDownloadSession} using the system default data subscription ID.
    261      * See {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)}
    262      */
    263     public static MbmsDownloadSession create(@NonNull Context context,
    264             @NonNull Executor executor, @NonNull MbmsDownloadSessionCallback callback) {
    265         return create(context, executor, SubscriptionManager.getDefaultSubscriptionId(), callback);
    266     }
    267 
    268     /**
    269      * Create a new MbmsDownloadManager using the given subscription ID.
    270      *
    271      * Note that this call will bind a remote service and that may take a bit. The instance of
    272      * {@link MbmsDownloadSession} that is returned will not be ready for use until
    273      * {@link MbmsDownloadSessionCallback#onMiddlewareReady()} is called on the provided callback.
    274      * If you attempt to use the instance before it is ready, an {@link IllegalStateException}
    275      * will be thrown or an error will be delivered through
    276      * {@link MbmsDownloadSessionCallback#onError(int, String)}.
    277      *
    278      * This also may throw an {@link IllegalArgumentException}.
    279      *
    280      * You may only have one instance of {@link MbmsDownloadSession} per UID. If you call this
    281      * method while there is an active instance of {@link MbmsDownloadSession} in your process
    282      * (in other words, one that has not had {@link #close()} called on it), this method will
    283      * throw an {@link IllegalStateException}. If you call this method in a different process
    284      * running under the same UID, an error will be indicated via
    285      * {@link MbmsDownloadSessionCallback#onError(int, String)}.
    286      *
    287      * Note that initialization may fail asynchronously. If you wish to try again after you
    288      * receive such an asynchronous error, you must call {@link #close()} on the instance of
    289      * {@link MbmsDownloadSession} that you received before calling this method again.
    290      *
    291      * @param context The instance of {@link Context} to use
    292      * @param executor The executor on which you wish to execute callbacks.
    293      * @param subscriptionId The data subscription ID to use
    294      * @param callback A callback to get asynchronous error messages and file service updates.
    295      * @return A new instance of {@link MbmsDownloadSession}, or null if an error occurred during
    296      * setup.
    297      */
    298     public static @Nullable MbmsDownloadSession create(@NonNull Context context,
    299             @NonNull Executor executor, int subscriptionId,
    300             final @NonNull MbmsDownloadSessionCallback callback) {
    301         if (!sIsInitialized.compareAndSet(false, true)) {
    302             throw new IllegalStateException("Cannot have two active instances");
    303         }
    304         MbmsDownloadSession session =
    305                 new MbmsDownloadSession(context, executor, subscriptionId, callback);
    306         final int result = session.bindAndInitialize();
    307         if (result != MbmsErrors.SUCCESS) {
    308             sIsInitialized.set(false);
    309             executor.execute(new Runnable() {
    310                 @Override
    311                 public void run() {
    312                     callback.onError(result, null);
    313                 }
    314             });
    315             return null;
    316         }
    317         return session;
    318     }
    319 
    320     private int bindAndInitialize() {
    321         return MbmsUtils.startBinding(mContext, MBMS_DOWNLOAD_SERVICE_ACTION,
    322                 new ServiceConnection() {
    323                     @Override
    324                     public void onServiceConnected(ComponentName name, IBinder service) {
    325                         IMbmsDownloadService downloadService =
    326                                 IMbmsDownloadService.Stub.asInterface(service);
    327                         int result;
    328                         try {
    329                             result = downloadService.initialize(mSubscriptionId, mInternalCallback);
    330                         } catch (RemoteException e) {
    331                             Log.e(LOG_TAG, "Service died before initialization");
    332                             sIsInitialized.set(false);
    333                             return;
    334                         } catch (RuntimeException e) {
    335                             Log.e(LOG_TAG, "Runtime exception during initialization");
    336                             sendErrorToApp(
    337                                     MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE,
    338                                     e.toString());
    339                             sIsInitialized.set(false);
    340                             return;
    341                         }
    342                         if (result == MbmsErrors.UNKNOWN) {
    343                             // Unbind and throw an obvious error
    344                             close();
    345                             throw new IllegalStateException("Middleware must not return an"
    346                                     + " unknown error code");
    347                         }
    348                         if (result != MbmsErrors.SUCCESS) {
    349                             sendErrorToApp(result, "Error returned during initialization");
    350                             sIsInitialized.set(false);
    351                             return;
    352                         }
    353                         try {
    354                             downloadService.asBinder().linkToDeath(mDeathRecipient, 0);
    355                         } catch (RemoteException e) {
    356                             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST,
    357                                     "Middleware lost during initialization");
    358                             sIsInitialized.set(false);
    359                             return;
    360                         }
    361                         mService.set(downloadService);
    362                     }
    363 
    364                     @Override
    365                     public void onServiceDisconnected(ComponentName name) {
    366                         Log.w(LOG_TAG, "bindAndInitialize: Remote service disconnected");
    367                         sIsInitialized.set(false);
    368                         mService.set(null);
    369                     }
    370                 });
    371     }
    372 
    373     /**
    374      * An inspection API to retrieve the list of available
    375      * {@link android.telephony.mbms.FileServiceInfo}s currently being advertised.
    376      * The results are returned asynchronously via a call to
    377      * {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)}
    378      *
    379      * Asynchronous error codes via the {@link MbmsDownloadSessionCallback#onError(int, String)}
    380      * callback may include any of the errors that are not specific to the streaming use-case.
    381      *
    382      * May throw an {@link IllegalStateException} or {@link IllegalArgumentException}.
    383      *
    384      * @param classList A list of service classes which the app wishes to receive
    385      *                  {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)} callbacks
    386      *                  about. Subsequent calls to this method will replace this list of service
    387      *                  classes (i.e. the middleware will no longer send updates for services
    388      *                  matching classes only in the old list).
    389      *                  Values in this list should be negotiated with the wireless carrier prior
    390      *                  to using this API.
    391      */
    392     public void requestUpdateFileServices(@NonNull List<String> classList) {
    393         IMbmsDownloadService downloadService = mService.get();
    394         if (downloadService == null) {
    395             throw new IllegalStateException("Middleware not yet bound");
    396         }
    397         try {
    398             int returnCode = downloadService.requestUpdateFileServices(mSubscriptionId, classList);
    399             if (returnCode == MbmsErrors.UNKNOWN) {
    400                 // Unbind and throw an obvious error
    401                 close();
    402                 throw new IllegalStateException("Middleware must not return an unknown error code");
    403             }
    404             if (returnCode != MbmsErrors.SUCCESS) {
    405                 sendErrorToApp(returnCode, null);
    406             }
    407         } catch (RemoteException e) {
    408             Log.w(LOG_TAG, "Remote process died");
    409             mService.set(null);
    410             sIsInitialized.set(false);
    411             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    412         }
    413     }
    414 
    415     /**
    416      * Sets the temp file root for downloads.
    417      * All temp files created for the middleware to write to will be contained in the specified
    418      * directory. Applications that wish to specify a location only need to call this method once
    419      * as long their data is persisted in storage -- the argument will be stored both in a
    420      * local instance of {@link android.content.SharedPreferences} and by the middleware.
    421      *
    422      * If this method is not called at least once before calling
    423      * {@link #download(DownloadRequest)}, the framework
    424      * will default to a directory formed by the concatenation of the app's files directory and
    425      * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}.
    426      *
    427      * Before calling this method, the app must cancel all of its pending
    428      * {@link DownloadRequest}s via {@link #cancelDownload(DownloadRequest)}. If this is not done,
    429      * you will receive an asynchronous error with code
    430      * {@link MbmsErrors.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} unless the
    431      * provided directory is the same as what has been previously configured.
    432      *
    433      * The {@link File} supplied as a root temp file directory must already exist. If not, an
    434      * {@link IllegalArgumentException} will be thrown. In addition, as an additional sanity
    435      * check, an {@link IllegalArgumentException} will be thrown if you attempt to set the temp
    436      * file root directory to one of your data roots (the value of {@link Context#getDataDir()},
    437      * {@link Context#getFilesDir()}, or {@link Context#getCacheDir()}).
    438      * @param tempFileRootDirectory A directory to place temp files in.
    439      */
    440     public void setTempFileRootDirectory(@NonNull File tempFileRootDirectory) {
    441         IMbmsDownloadService downloadService = mService.get();
    442         if (downloadService == null) {
    443             throw new IllegalStateException("Middleware not yet bound");
    444         }
    445         try {
    446             validateTempFileRootSanity(tempFileRootDirectory);
    447         } catch (IOException e) {
    448             throw new IllegalStateException("Got IOException checking directory sanity");
    449         }
    450         String filePath;
    451         try {
    452             filePath = tempFileRootDirectory.getCanonicalPath();
    453         } catch (IOException e) {
    454             throw new IllegalArgumentException("Unable to canonicalize the provided path: " + e);
    455         }
    456 
    457         try {
    458             int result = downloadService.setTempFileRootDirectory(mSubscriptionId, filePath);
    459             if (result == MbmsErrors.UNKNOWN) {
    460                 // Unbind and throw an obvious error
    461                 close();
    462                 throw new IllegalStateException("Middleware must not return an unknown error code");
    463             }
    464             if (result != MbmsErrors.SUCCESS) {
    465                 sendErrorToApp(result, null);
    466                 return;
    467             }
    468         } catch (RemoteException e) {
    469             mService.set(null);
    470             sIsInitialized.set(false);
    471             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    472             return;
    473         }
    474 
    475         SharedPreferences prefs = mContext.getSharedPreferences(
    476                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
    477         prefs.edit().putString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, filePath).apply();
    478     }
    479 
    480     private void validateTempFileRootSanity(File tempFileRootDirectory) throws IOException {
    481         if (!tempFileRootDirectory.exists()) {
    482             throw new IllegalArgumentException("Provided directory does not exist");
    483         }
    484         if (!tempFileRootDirectory.isDirectory()) {
    485             throw new IllegalArgumentException("Provided File is not a directory");
    486         }
    487         String canonicalTempFilePath = tempFileRootDirectory.getCanonicalPath();
    488         if (mContext.getDataDir().getCanonicalPath().equals(canonicalTempFilePath)) {
    489             throw new IllegalArgumentException("Temp file root cannot be your data dir");
    490         }
    491         if (mContext.getCacheDir().getCanonicalPath().equals(canonicalTempFilePath)) {
    492             throw new IllegalArgumentException("Temp file root cannot be your cache dir");
    493         }
    494         if (mContext.getFilesDir().getCanonicalPath().equals(canonicalTempFilePath)) {
    495             throw new IllegalArgumentException("Temp file root cannot be your files dir");
    496         }
    497     }
    498     /**
    499      * Retrieves the currently configured temp file root directory. Returns the file that was
    500      * configured via {@link #setTempFileRootDirectory(File)} or the default directory
    501      * {@link #download(DownloadRequest)} was called without ever
    502      * setting the temp file root. If neither method has been called since the last time the app's
    503      * shared preferences were reset, returns {@code null}.
    504      *
    505      * @return A {@link File} pointing to the configured temp file directory, or null if not yet
    506      *         configured.
    507      */
    508     public @Nullable File getTempFileRootDirectory() {
    509         SharedPreferences prefs = mContext.getSharedPreferences(
    510                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
    511         String path = prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null);
    512         if (path != null) {
    513             return new File(path);
    514         }
    515         return null;
    516     }
    517 
    518     /**
    519      * Requests the download of a file or set of files that the carrier has indicated to be
    520      * available.
    521      *
    522      * May throw an {@link IllegalArgumentException}
    523      *
    524      * If {@link #setTempFileRootDirectory(File)} has not called after the app has been installed,
    525      * this method will create a directory at the default location defined at
    526      * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp
    527      * file root directory.
    528      *
    529      * If the {@link DownloadRequest} has a destination that is not on the same filesystem as the
    530      * temp file directory provided via {@link #getTempFileRootDirectory()}, an
    531      * {@link IllegalArgumentException} will be thrown.
    532      *
    533      * Asynchronous errors through the callback may include any error not specific to the
    534      * streaming use-case.
    535      *
    536      * If no error is delivered via the callback after calling this method, that means that the
    537      * middleware has successfully started the download or scheduled the download, if the download
    538      * is at a future time.
    539      * @param request The request that specifies what should be downloaded.
    540      */
    541     public void download(@NonNull DownloadRequest request) {
    542         IMbmsDownloadService downloadService = mService.get();
    543         if (downloadService == null) {
    544             throw new IllegalStateException("Middleware not yet bound");
    545         }
    546 
    547         // Check to see whether the app's set a temp root dir yet, and set it if not.
    548         SharedPreferences prefs = mContext.getSharedPreferences(
    549                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
    550         if (prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null) == null) {
    551             File tempRootDirectory = new File(mContext.getFilesDir(),
    552                     DEFAULT_TOP_LEVEL_TEMP_DIRECTORY);
    553             tempRootDirectory.mkdirs();
    554             setTempFileRootDirectory(tempRootDirectory);
    555         }
    556 
    557         checkDownloadRequestDestination(request);
    558 
    559         try {
    560             int result = downloadService.download(request);
    561             if (result == MbmsErrors.SUCCESS) {
    562                 writeDownloadRequestToken(request);
    563             } else {
    564                 if (result == MbmsErrors.UNKNOWN) {
    565                     // Unbind and throw an obvious error
    566                     close();
    567                     throw new IllegalStateException("Middleware must not return an unknown"
    568                             + " error code");
    569                 }
    570                 sendErrorToApp(result, null);
    571             }
    572         } catch (RemoteException e) {
    573             mService.set(null);
    574             sIsInitialized.set(false);
    575             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    576         }
    577     }
    578 
    579     /**
    580      * Returns a list of pending {@link DownloadRequest}s that originated from this application.
    581      * A pending request is one that was issued via
    582      * {@link #download(DownloadRequest)} but not cancelled through
    583      * {@link #cancelDownload(DownloadRequest)}.
    584      * @return A list, possibly empty, of {@link DownloadRequest}s
    585      */
    586     public @NonNull List<DownloadRequest> listPendingDownloads() {
    587         IMbmsDownloadService downloadService = mService.get();
    588         if (downloadService == null) {
    589             throw new IllegalStateException("Middleware not yet bound");
    590         }
    591 
    592         try {
    593             return downloadService.listPendingDownloads(mSubscriptionId);
    594         } catch (RemoteException e) {
    595             mService.set(null);
    596             sIsInitialized.set(false);
    597             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    598             return Collections.emptyList();
    599         }
    600     }
    601 
    602     /**
    603      * Registers a download status listener for a {@link DownloadRequest} previously requested via
    604      * {@link #download(DownloadRequest)}. This callback will only be called as long as both this
    605      * app and the middleware are both running -- if either one stops, no further calls on the
    606      * provided {@link DownloadStatusListener} will be enqueued.
    607      *
    608      * If the middleware is not aware of the specified download request,
    609      * this method will throw an {@link IllegalArgumentException}.
    610      *
    611      * If the operation encountered an error, the error code will be delivered via
    612      * {@link MbmsDownloadSessionCallback#onError}.
    613      *
    614      * Repeated calls to this method for the same {@link DownloadRequest} will replace the
    615      * previously registered listener.
    616      *
    617      * @param request The {@link DownloadRequest} that you want updates on.
    618      * @param executor The {@link Executor} on which calls to {@code listener } should be executed.
    619      * @param listener The listener that should be called when the middleware has information to
    620      *                 share on the status download.
    621      */
    622     public void addStatusListener(@NonNull DownloadRequest request,
    623             @NonNull Executor executor, @NonNull DownloadStatusListener listener) {
    624         IMbmsDownloadService downloadService = mService.get();
    625         if (downloadService == null) {
    626             throw new IllegalStateException("Middleware not yet bound");
    627         }
    628 
    629         InternalDownloadStatusListener internalListener =
    630                 new InternalDownloadStatusListener(listener, executor);
    631 
    632         try {
    633             int result = downloadService.addStatusListener(request, internalListener);
    634             if (result == MbmsErrors.UNKNOWN) {
    635                 // Unbind and throw an obvious error
    636                 close();
    637                 throw new IllegalStateException("Middleware must not return an unknown error code");
    638             }
    639             if (result != MbmsErrors.SUCCESS) {
    640                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    641                     throw new IllegalArgumentException("Unknown download request.");
    642                 }
    643                 sendErrorToApp(result, null);
    644                 return;
    645             }
    646         } catch (RemoteException e) {
    647             mService.set(null);
    648             sIsInitialized.set(false);
    649             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    650             return;
    651         }
    652         mInternalDownloadStatusListeners.put(listener, internalListener);
    653     }
    654 
    655     /**
    656      * Un-register a listener previously registered via
    657      * {@link #addStatusListener(DownloadRequest, Executor, DownloadStatusListener)}. After
    658      * this method is called, no further calls will be enqueued on the {@link Executor}
    659      * provided upon registration, even if this method throws an exception.
    660      *
    661      * If the middleware is not aware of the specified download request,
    662      * this method will throw an {@link IllegalArgumentException}.
    663      *
    664      * If the operation encountered an error, the error code will be delivered via
    665      * {@link MbmsDownloadSessionCallback#onError}.
    666      *
    667      * @param request The {@link DownloadRequest} provided during registration
    668      * @param listener The listener provided during registration.
    669      */
    670     public void removeStatusListener(@NonNull DownloadRequest request,
    671             @NonNull DownloadStatusListener listener) {
    672         try {
    673             IMbmsDownloadService downloadService = mService.get();
    674             if (downloadService == null) {
    675                 throw new IllegalStateException("Middleware not yet bound");
    676             }
    677 
    678             InternalDownloadStatusListener internalListener =
    679                     mInternalDownloadStatusListeners.get(listener);
    680             if (internalListener == null) {
    681                 throw new IllegalArgumentException("Provided listener was never registered");
    682             }
    683 
    684             try {
    685                 int result = downloadService.removeStatusListener(request, internalListener);
    686                 if (result == MbmsErrors.UNKNOWN) {
    687                     // Unbind and throw an obvious error
    688                     close();
    689                     throw new IllegalStateException("Middleware must not return an"
    690                             + " unknown error code");
    691                 }
    692                 if (result != MbmsErrors.SUCCESS) {
    693                     if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    694                         throw new IllegalArgumentException("Unknown download request.");
    695                     }
    696                     sendErrorToApp(result, null);
    697                     return;
    698                 }
    699             } catch (RemoteException e) {
    700                 mService.set(null);
    701                 sIsInitialized.set(false);
    702                 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    703                 return;
    704             }
    705         } finally {
    706             InternalDownloadStatusListener internalCallback =
    707                     mInternalDownloadStatusListeners.remove(listener);
    708             if (internalCallback != null) {
    709                 internalCallback.stop();
    710             }
    711         }
    712     }
    713 
    714     /**
    715      * Registers a progress listener for a {@link DownloadRequest} previously requested via
    716      * {@link #download(DownloadRequest)}. This listener will only be called as long as both this
    717      * app and the middleware are both running -- if either one stops, no further calls on the
    718      * provided {@link DownloadProgressListener} will be enqueued.
    719      *
    720      * If the middleware is not aware of the specified download request,
    721      * this method will throw an {@link IllegalArgumentException}.
    722      *
    723      * If the operation encountered an error, the error code will be delivered via
    724      * {@link MbmsDownloadSessionCallback#onError}.
    725      *
    726      * Repeated calls to this method for the same {@link DownloadRequest} will replace the
    727      * previously registered listener.
    728      *
    729      * @param request The {@link DownloadRequest} that you want updates on.
    730      * @param executor The {@link Executor} on which calls to {@code listener} should be executed.
    731      * @param listener The listener that should be called when the middleware has information to
    732      *                 share on the progress of the download.
    733      */
    734     public void addProgressListener(@NonNull DownloadRequest request,
    735             @NonNull Executor executor, @NonNull DownloadProgressListener listener) {
    736         IMbmsDownloadService downloadService = mService.get();
    737         if (downloadService == null) {
    738             throw new IllegalStateException("Middleware not yet bound");
    739         }
    740 
    741         InternalDownloadProgressListener internalListener =
    742                 new InternalDownloadProgressListener(listener, executor);
    743 
    744         try {
    745             int result = downloadService.addProgressListener(request, internalListener);
    746             if (result == MbmsErrors.UNKNOWN) {
    747                 // Unbind and throw an obvious error
    748                 close();
    749                 throw new IllegalStateException("Middleware must not return an unknown error code");
    750             }
    751             if (result != MbmsErrors.SUCCESS) {
    752                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    753                     throw new IllegalArgumentException("Unknown download request.");
    754                 }
    755                 sendErrorToApp(result, null);
    756                 return;
    757             }
    758         } catch (RemoteException e) {
    759             mService.set(null);
    760             sIsInitialized.set(false);
    761             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    762             return;
    763         }
    764         mInternalDownloadProgressListeners.put(listener, internalListener);
    765     }
    766 
    767     /**
    768      * Un-register a listener previously registered via
    769      * {@link #addProgressListener(DownloadRequest, Executor, DownloadProgressListener)}. After
    770      * this method is called, no further callbacks will be enqueued on the {@link Handler}
    771      * provided upon registration, even if this method throws an exception.
    772      *
    773      * If the middleware is not aware of the specified download request,
    774      * this method will throw an {@link IllegalArgumentException}.
    775      *
    776      * If the operation encountered an error, the error code will be delivered via
    777      * {@link MbmsDownloadSessionCallback#onError}.
    778      *
    779      * @param request The {@link DownloadRequest} provided during registration
    780      * @param listener The listener provided during registration.
    781      */
    782     public void removeProgressListener(@NonNull DownloadRequest request,
    783             @NonNull DownloadProgressListener listener) {
    784         try {
    785             IMbmsDownloadService downloadService = mService.get();
    786             if (downloadService == null) {
    787                 throw new IllegalStateException("Middleware not yet bound");
    788             }
    789 
    790             InternalDownloadProgressListener internalListener =
    791                     mInternalDownloadProgressListeners.get(listener);
    792             if (internalListener == null) {
    793                 throw new IllegalArgumentException("Provided listener was never registered");
    794             }
    795 
    796             try {
    797                 int result = downloadService.removeProgressListener(request, internalListener);
    798                 if (result == MbmsErrors.UNKNOWN) {
    799                     // Unbind and throw an obvious error
    800                     close();
    801                     throw new IllegalStateException("Middleware must not"
    802                             + " return an unknown error code");
    803                 }
    804                 if (result != MbmsErrors.SUCCESS) {
    805                     if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    806                         throw new IllegalArgumentException("Unknown download request.");
    807                     }
    808                     sendErrorToApp(result, null);
    809                     return;
    810                 }
    811             } catch (RemoteException e) {
    812                 mService.set(null);
    813                 sIsInitialized.set(false);
    814                 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    815                 return;
    816             }
    817         } finally {
    818             InternalDownloadProgressListener internalCallback =
    819                     mInternalDownloadProgressListeners.remove(listener);
    820             if (internalCallback != null) {
    821                 internalCallback.stop();
    822             }
    823         }
    824     }
    825 
    826     /**
    827      * Attempts to cancel the specified {@link DownloadRequest}.
    828      *
    829      * If the operation encountered an error, the error code will be delivered via
    830      * {@link MbmsDownloadSessionCallback#onError}.
    831      *
    832      * @param downloadRequest The download request that you wish to cancel.
    833      */
    834     public void cancelDownload(@NonNull DownloadRequest downloadRequest) {
    835         IMbmsDownloadService downloadService = mService.get();
    836         if (downloadService == null) {
    837             throw new IllegalStateException("Middleware not yet bound");
    838         }
    839 
    840         try {
    841             int result = downloadService.cancelDownload(downloadRequest);
    842             if (result == MbmsErrors.UNKNOWN) {
    843                 // Unbind and throw an obvious error
    844                 close();
    845                 throw new IllegalStateException("Middleware must not return an unknown error code");
    846             }
    847             if (result != MbmsErrors.SUCCESS) {
    848                 sendErrorToApp(result, null);
    849             } else {
    850                 deleteDownloadRequestToken(downloadRequest);
    851             }
    852         } catch (RemoteException e) {
    853             mService.set(null);
    854             sIsInitialized.set(false);
    855             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    856         }
    857     }
    858 
    859     /**
    860      * Requests information about the state of a file pending download.
    861      *
    862      * The state will be delivered as a callback via
    863      * {@link DownloadStatusListener#onStatusUpdated(DownloadRequest, FileInfo, int)}. If no such
    864      * callback has been registered via
    865      * {@link #addProgressListener(DownloadRequest, Executor, DownloadProgressListener)}, this
    866      * method will be a no-op.
    867      *
    868      * If the middleware has no record of the
    869      * file indicated by {@code fileInfo} being associated with {@code downloadRequest},
    870      * an {@link IllegalArgumentException} will be thrown.
    871      *
    872      * @param downloadRequest The download request to query.
    873      * @param fileInfo The particular file within the request to get information on.
    874      */
    875     public void requestDownloadState(DownloadRequest downloadRequest, FileInfo fileInfo) {
    876         IMbmsDownloadService downloadService = mService.get();
    877         if (downloadService == null) {
    878             throw new IllegalStateException("Middleware not yet bound");
    879         }
    880 
    881         try {
    882             int result = downloadService.requestDownloadState(downloadRequest, fileInfo);
    883             if (result == MbmsErrors.UNKNOWN) {
    884                 // Unbind and throw an obvious error
    885                 close();
    886                 throw new IllegalStateException("Middleware must not return an unknown error code");
    887             }
    888             if (result != MbmsErrors.SUCCESS) {
    889                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    890                     throw new IllegalArgumentException("Unknown download request.");
    891                 }
    892                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_FILE_INFO) {
    893                     throw new IllegalArgumentException("Unknown file.");
    894                 }
    895                 sendErrorToApp(result, null);
    896             }
    897         } catch (RemoteException e) {
    898             mService.set(null);
    899             sIsInitialized.set(false);
    900             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    901         }
    902     }
    903 
    904     /**
    905      * Resets the middleware's knowledge of previously-downloaded files in this download request.
    906      *
    907      * Normally, the middleware keeps track of the hashes of downloaded files and won't re-download
    908      * files whose server-reported hash matches one of the already-downloaded files. This means
    909      * that if the file is accidentally deleted by the user or by the app, the middleware will
    910      * not try to download it again.
    911      * This method will reset the middleware's cache of hashes for the provided
    912      * {@link DownloadRequest}, so that previously downloaded content will be downloaded again
    913      * when available.
    914      * This will not interrupt in-progress downloads.
    915      *
    916      * This is distinct from cancelling and re-issuing the download request -- if you cancel and
    917      * re-issue, the middleware will not clear its cache of download state information.
    918      *
    919      * If the middleware is not aware of the specified download request, an
    920      * {@link IllegalArgumentException} will be thrown.
    921      *
    922      * @param downloadRequest The request to re-download files for.
    923      */
    924     public void resetDownloadKnowledge(DownloadRequest downloadRequest) {
    925         IMbmsDownloadService downloadService = mService.get();
    926         if (downloadService == null) {
    927             throw new IllegalStateException("Middleware not yet bound");
    928         }
    929 
    930         try {
    931             int result = downloadService.resetDownloadKnowledge(downloadRequest);
    932             if (result == MbmsErrors.UNKNOWN) {
    933                 // Unbind and throw an obvious error
    934                 close();
    935                 throw new IllegalStateException("Middleware must not return an unknown error code");
    936             }
    937             if (result != MbmsErrors.SUCCESS) {
    938                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
    939                     throw new IllegalArgumentException("Unknown download request.");
    940                 }
    941                 sendErrorToApp(result, null);
    942             }
    943         } catch (RemoteException e) {
    944             mService.set(null);
    945             sIsInitialized.set(false);
    946             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
    947         }
    948     }
    949 
    950     /**
    951      * Terminates this instance.
    952      *
    953      * After this method returns,
    954      * no further callbacks originating from the middleware will be enqueued on the provided
    955      * instance of {@link MbmsDownloadSessionCallback}, but callbacks that have already been
    956      * enqueued will still be delivered.
    957      *
    958      * It is safe to call {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)} to
    959      * obtain another instance of {@link MbmsDownloadSession} immediately after this method
    960      * returns.
    961      *
    962      * May throw an {@link IllegalStateException}
    963      */
    964     @Override
    965     public void close() {
    966         try {
    967             IMbmsDownloadService downloadService = mService.get();
    968             if (downloadService == null) {
    969                 Log.i(LOG_TAG, "Service already dead");
    970                 return;
    971             }
    972             downloadService.dispose(mSubscriptionId);
    973         } catch (RemoteException e) {
    974             // Ignore
    975             Log.i(LOG_TAG, "Remote exception while disposing of service");
    976         } finally {
    977             mService.set(null);
    978             sIsInitialized.set(false);
    979             mInternalCallback.stop();
    980         }
    981     }
    982 
    983     private void writeDownloadRequestToken(DownloadRequest request) {
    984         File token = getDownloadRequestTokenPath(request);
    985         if (!token.getParentFile().exists()) {
    986             token.getParentFile().mkdirs();
    987         }
    988         if (token.exists()) {
    989             Log.w(LOG_TAG, "Download token " + token.getName() + " already exists");
    990             return;
    991         }
    992         try {
    993             if (!token.createNewFile()) {
    994                 throw new RuntimeException("Failed to create download token for request "
    995                         + request + ". Token location is " + token.getPath());
    996             }
    997         } catch (IOException e) {
    998             throw new RuntimeException("Failed to create download token for request " + request
    999                     + " due to IOException " + e + ". Attempted to write to " + token.getPath());
   1000         }
   1001     }
   1002 
   1003     private void deleteDownloadRequestToken(DownloadRequest request) {
   1004         File token = getDownloadRequestTokenPath(request);
   1005         if (!token.isFile()) {
   1006             Log.w(LOG_TAG, "Attempting to delete non-existent download token at " + token);
   1007             return;
   1008         }
   1009         if (!token.delete()) {
   1010             Log.w(LOG_TAG, "Couldn't delete download token at " + token);
   1011         }
   1012     }
   1013 
   1014     private void checkDownloadRequestDestination(DownloadRequest request) {
   1015         File downloadRequestDestination = new File(request.getDestinationUri().getPath());
   1016         if (!downloadRequestDestination.isDirectory()) {
   1017             throw new IllegalArgumentException("The destination path must be a directory");
   1018         }
   1019         // Check if the request destination is okay to use by attempting to rename an empty
   1020         // file to there.
   1021         File testFile = new File(MbmsTempFileProvider.getEmbmsTempFileDir(mContext),
   1022                 DESTINATION_SANITY_CHECK_FILE_NAME);
   1023         File testFileDestination = new File(downloadRequestDestination,
   1024                 DESTINATION_SANITY_CHECK_FILE_NAME);
   1025 
   1026         try {
   1027             if (!testFile.exists()) {
   1028                 testFile.createNewFile();
   1029             }
   1030             if (!testFile.renameTo(testFileDestination)) {
   1031                 throw new IllegalArgumentException("Destination provided in the download request " +
   1032                         "is invalid -- files in the temp file directory cannot be directly moved " +
   1033                         "there.");
   1034             }
   1035         } catch (IOException e) {
   1036             throw new IllegalStateException("Got IOException while testing out the destination: "
   1037                     + e);
   1038         } finally {
   1039             testFile.delete();
   1040             testFileDestination.delete();
   1041         }
   1042     }
   1043 
   1044     private File getDownloadRequestTokenPath(DownloadRequest request) {
   1045         File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext,
   1046                 request.getFileServiceId());
   1047         String downloadTokenFileName = request.getHash()
   1048                 + MbmsDownloadReceiver.DOWNLOAD_TOKEN_SUFFIX;
   1049         return new File(tempFileLocation, downloadTokenFileName);
   1050     }
   1051 
   1052     private void sendErrorToApp(int errorCode, String message) {
   1053         mInternalCallback.onError(errorCode, message);
   1054     }
   1055 }
   1056