• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
MbmsDownloadSession(Context context, Executor executor, int subscriptionId, MbmsDownloadSessionCallback callback)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      */
create(@onNull Context context, @NonNull Executor executor, @NonNull MbmsDownloadSessionCallback callback)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      */
create(@onNull Context context, @NonNull Executor executor, int subscriptionId, final @NonNull MbmsDownloadSessionCallback callback)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 
bindAndInitialize()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