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 ServiceConnection mServiceConnection;
247     private final InternalDownloadSessionCallback mInternalCallback;
248     private final Map<DownloadStatusListener, InternalDownloadStatusListener>
249             mInternalDownloadStatusListeners = new HashMap<>();
250     private final Map<DownloadProgressListener, InternalDownloadProgressListener>
251             mInternalDownloadProgressListeners = new HashMap<>();
252 
MbmsDownloadSession(Context context, Executor executor, int subscriptionId, MbmsDownloadSessionCallback callback)253     private MbmsDownloadSession(Context context, Executor executor, int subscriptionId,
254             MbmsDownloadSessionCallback callback) {
255         mContext = context;
256         mSubscriptionId = subscriptionId;
257         mInternalCallback = new InternalDownloadSessionCallback(callback, executor);
258     }
259 
260     /**
261      * Create a new {@link MbmsDownloadSession} using the system default data subscription ID.
262      * See {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)}
263      */
create(@onNull Context context, @NonNull Executor executor, @NonNull MbmsDownloadSessionCallback callback)264     public static MbmsDownloadSession create(@NonNull Context context,
265             @NonNull Executor executor, @NonNull MbmsDownloadSessionCallback callback) {
266         return create(context, executor, SubscriptionManager.getDefaultSubscriptionId(), callback);
267     }
268 
269     /**
270      * Create a new MbmsDownloadManager using the given subscription ID.
271      *
272      * Note that this call will bind a remote service and that may take a bit. The instance of
273      * {@link MbmsDownloadSession} that is returned will not be ready for use until
274      * {@link MbmsDownloadSessionCallback#onMiddlewareReady()} is called on the provided callback.
275      * If you attempt to use the instance before it is ready, an {@link IllegalStateException}
276      * will be thrown or an error will be delivered through
277      * {@link MbmsDownloadSessionCallback#onError(int, String)}.
278      *
279      * This also may throw an {@link IllegalArgumentException}.
280      *
281      * You may only have one instance of {@link MbmsDownloadSession} per UID. If you call this
282      * method while there is an active instance of {@link MbmsDownloadSession} in your process
283      * (in other words, one that has not had {@link #close()} called on it), this method will
284      * throw an {@link IllegalStateException}. If you call this method in a different process
285      * running under the same UID, an error will be indicated via
286      * {@link MbmsDownloadSessionCallback#onError(int, String)}.
287      *
288      * Note that initialization may fail asynchronously. If you wish to try again after you
289      * receive such an asynchronous error, you must call {@link #close()} on the instance of
290      * {@link MbmsDownloadSession} that you received before calling this method again.
291      *
292      * @param context The instance of {@link Context} to use
293      * @param executor The executor on which you wish to execute callbacks.
294      * @param subscriptionId The data subscription ID to use
295      * @param callback A callback to get asynchronous error messages and file service updates.
296      * @return A new instance of {@link MbmsDownloadSession}, or null if an error occurred during
297      * setup.
298      */
create(@onNull Context context, @NonNull Executor executor, int subscriptionId, final @NonNull MbmsDownloadSessionCallback callback)299     public static @Nullable MbmsDownloadSession create(@NonNull Context context,
300             @NonNull Executor executor, int subscriptionId,
301             final @NonNull MbmsDownloadSessionCallback callback) {
302         if (!sIsInitialized.compareAndSet(false, true)) {
303             throw new IllegalStateException("Cannot have two active instances");
304         }
305         MbmsDownloadSession session =
306                 new MbmsDownloadSession(context, executor, subscriptionId, callback);
307         final int result = session.bindAndInitialize();
308         if (result != MbmsErrors.SUCCESS) {
309             sIsInitialized.set(false);
310             executor.execute(new Runnable() {
311                 @Override
312                 public void run() {
313                     callback.onError(result, null);
314                 }
315             });
316             return null;
317         }
318         return session;
319     }
320 
bindAndInitialize()321     private int bindAndInitialize() {
322         mServiceConnection = 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             @Override
372             public void onNullBinding(ComponentName name) {
373                 Log.w(LOG_TAG, "bindAndInitialize: Remote service returned null");
374                 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST,
375                         "Middleware service binding returned null");
376                 sIsInitialized.set(false);
377                 mService.set(null);
378                 mContext.unbindService(this);
379             }
380         };
381         return MbmsUtils.startBinding(mContext, MBMS_DOWNLOAD_SERVICE_ACTION, mServiceConnection);
382     }
383 
384     /**
385      * An inspection API to retrieve the list of available
386      * {@link android.telephony.mbms.FileServiceInfo}s currently being advertised.
387      * The results are returned asynchronously via a call to
388      * {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)}
389      *
390      * Asynchronous error codes via the {@link MbmsDownloadSessionCallback#onError(int, String)}
391      * callback may include any of the errors that are not specific to the streaming use-case.
392      *
393      * May throw an {@link IllegalStateException} or {@link IllegalArgumentException}.
394      *
395      * @param classList A list of service classes which the app wishes to receive
396      *                  {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)} callbacks
397      *                  about. Subsequent calls to this method will replace this list of service
398      *                  classes (i.e. the middleware will no longer send updates for services
399      *                  matching classes only in the old list).
400      *                  Values in this list should be negotiated with the wireless carrier prior
401      *                  to using this API.
402      */
requestUpdateFileServices(@onNull List<String> classList)403     public void requestUpdateFileServices(@NonNull List<String> classList) {
404         IMbmsDownloadService downloadService = mService.get();
405         if (downloadService == null) {
406             throw new IllegalStateException("Middleware not yet bound");
407         }
408         try {
409             int returnCode = downloadService.requestUpdateFileServices(mSubscriptionId, classList);
410             if (returnCode == MbmsErrors.UNKNOWN) {
411                 // Unbind and throw an obvious error
412                 close();
413                 throw new IllegalStateException("Middleware must not return an unknown error code");
414             }
415             if (returnCode != MbmsErrors.SUCCESS) {
416                 sendErrorToApp(returnCode, null);
417             }
418         } catch (RemoteException e) {
419             Log.w(LOG_TAG, "Remote process died");
420             mService.set(null);
421             sIsInitialized.set(false);
422             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
423         }
424     }
425 
426     /**
427      * Sets the temp file root for downloads.
428      * All temp files created for the middleware to write to will be contained in the specified
429      * directory. Applications that wish to specify a location only need to call this method once
430      * as long their data is persisted in storage -- the argument will be stored both in a
431      * local instance of {@link android.content.SharedPreferences} and by the middleware.
432      *
433      * If this method is not called at least once before calling
434      * {@link #download(DownloadRequest)}, the framework
435      * will default to a directory formed by the concatenation of the app's files directory and
436      * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}.
437      *
438      * Before calling this method, the app must cancel all of its pending
439      * {@link DownloadRequest}s via {@link #cancelDownload(DownloadRequest)}. If this is not done,
440      * you will receive an asynchronous error with code
441      * {@link MbmsErrors.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} unless the
442      * provided directory is the same as what has been previously configured.
443      *
444      * The {@link File} supplied as a root temp file directory must already exist. If not, an
445      * {@link IllegalArgumentException} will be thrown. In addition, as an additional sanity
446      * check, an {@link IllegalArgumentException} will be thrown if you attempt to set the temp
447      * file root directory to one of your data roots (the value of {@link Context#getDataDir()},
448      * {@link Context#getFilesDir()}, or {@link Context#getCacheDir()}).
449      * @param tempFileRootDirectory A directory to place temp files in.
450      */
setTempFileRootDirectory(@onNull File tempFileRootDirectory)451     public void setTempFileRootDirectory(@NonNull File tempFileRootDirectory) {
452         IMbmsDownloadService downloadService = mService.get();
453         if (downloadService == null) {
454             throw new IllegalStateException("Middleware not yet bound");
455         }
456         try {
457             validateTempFileRootSanity(tempFileRootDirectory);
458         } catch (IOException e) {
459             throw new IllegalStateException("Got IOException checking directory sanity");
460         }
461         String filePath;
462         try {
463             filePath = tempFileRootDirectory.getCanonicalPath();
464         } catch (IOException e) {
465             throw new IllegalArgumentException("Unable to canonicalize the provided path: " + e);
466         }
467 
468         try {
469             int result = downloadService.setTempFileRootDirectory(mSubscriptionId, filePath);
470             if (result == MbmsErrors.UNKNOWN) {
471                 // Unbind and throw an obvious error
472                 close();
473                 throw new IllegalStateException("Middleware must not return an unknown error code");
474             }
475             if (result != MbmsErrors.SUCCESS) {
476                 sendErrorToApp(result, null);
477                 return;
478             }
479         } catch (RemoteException e) {
480             mService.set(null);
481             sIsInitialized.set(false);
482             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
483             return;
484         }
485 
486         SharedPreferences prefs = mContext.getSharedPreferences(
487                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
488         prefs.edit().putString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, filePath).apply();
489     }
490 
validateTempFileRootSanity(File tempFileRootDirectory)491     private void validateTempFileRootSanity(File tempFileRootDirectory) throws IOException {
492         if (!tempFileRootDirectory.exists()) {
493             throw new IllegalArgumentException("Provided directory does not exist");
494         }
495         if (!tempFileRootDirectory.isDirectory()) {
496             throw new IllegalArgumentException("Provided File is not a directory");
497         }
498         String canonicalTempFilePath = tempFileRootDirectory.getCanonicalPath();
499         if (mContext.getDataDir().getCanonicalPath().equals(canonicalTempFilePath)) {
500             throw new IllegalArgumentException("Temp file root cannot be your data dir");
501         }
502         if (mContext.getCacheDir().getCanonicalPath().equals(canonicalTempFilePath)) {
503             throw new IllegalArgumentException("Temp file root cannot be your cache dir");
504         }
505         if (mContext.getFilesDir().getCanonicalPath().equals(canonicalTempFilePath)) {
506             throw new IllegalArgumentException("Temp file root cannot be your files dir");
507         }
508     }
509     /**
510      * Retrieves the currently configured temp file root directory. Returns the file that was
511      * configured via {@link #setTempFileRootDirectory(File)} or the default directory
512      * {@link #download(DownloadRequest)} was called without ever
513      * setting the temp file root. If neither method has been called since the last time the app's
514      * shared preferences were reset, returns {@code null}.
515      *
516      * @return A {@link File} pointing to the configured temp file directory, or null if not yet
517      *         configured.
518      */
getTempFileRootDirectory()519     public @Nullable File getTempFileRootDirectory() {
520         SharedPreferences prefs = mContext.getSharedPreferences(
521                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
522         String path = prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null);
523         if (path != null) {
524             return new File(path);
525         }
526         return null;
527     }
528 
529     /**
530      * Requests the download of a file or set of files that the carrier has indicated to be
531      * available.
532      *
533      * May throw an {@link IllegalArgumentException}
534      *
535      * If {@link #setTempFileRootDirectory(File)} has not called after the app has been installed,
536      * this method will create a directory at the default location defined at
537      * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp
538      * file root directory.
539      *
540      * If the {@link DownloadRequest} has a destination that is not on the same filesystem as the
541      * temp file directory provided via {@link #getTempFileRootDirectory()}, an
542      * {@link IllegalArgumentException} will be thrown.
543      *
544      * Asynchronous errors through the callback may include any error not specific to the
545      * streaming use-case.
546      *
547      * If no error is delivered via the callback after calling this method, that means that the
548      * middleware has successfully started the download or scheduled the download, if the download
549      * is at a future time.
550      * @param request The request that specifies what should be downloaded.
551      */
download(@onNull DownloadRequest request)552     public void download(@NonNull DownloadRequest request) {
553         IMbmsDownloadService downloadService = mService.get();
554         if (downloadService == null) {
555             throw new IllegalStateException("Middleware not yet bound");
556         }
557 
558         // Check to see whether the app's set a temp root dir yet, and set it if not.
559         SharedPreferences prefs = mContext.getSharedPreferences(
560                 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
561         if (prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null) == null) {
562             File tempRootDirectory = new File(mContext.getFilesDir(),
563                     DEFAULT_TOP_LEVEL_TEMP_DIRECTORY);
564             tempRootDirectory.mkdirs();
565             setTempFileRootDirectory(tempRootDirectory);
566         }
567 
568         checkDownloadRequestDestination(request);
569 
570         try {
571             int result = downloadService.download(request);
572             if (result == MbmsErrors.SUCCESS) {
573                 writeDownloadRequestToken(request);
574             } else {
575                 if (result == MbmsErrors.UNKNOWN) {
576                     // Unbind and throw an obvious error
577                     close();
578                     throw new IllegalStateException("Middleware must not return an unknown"
579                             + " error code");
580                 }
581                 sendErrorToApp(result, null);
582             }
583         } catch (RemoteException e) {
584             mService.set(null);
585             sIsInitialized.set(false);
586             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
587         }
588     }
589 
590     /**
591      * Returns a list of pending {@link DownloadRequest}s that originated from this application.
592      * A pending request is one that was issued via
593      * {@link #download(DownloadRequest)} but not cancelled through
594      * {@link #cancelDownload(DownloadRequest)}.
595      * @return A list, possibly empty, of {@link DownloadRequest}s
596      */
listPendingDownloads()597     public @NonNull List<DownloadRequest> listPendingDownloads() {
598         IMbmsDownloadService downloadService = mService.get();
599         if (downloadService == null) {
600             throw new IllegalStateException("Middleware not yet bound");
601         }
602 
603         try {
604             return downloadService.listPendingDownloads(mSubscriptionId);
605         } catch (RemoteException e) {
606             mService.set(null);
607             sIsInitialized.set(false);
608             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
609             return Collections.emptyList();
610         }
611     }
612 
613     /**
614      * Registers a download status listener for a {@link DownloadRequest} previously requested via
615      * {@link #download(DownloadRequest)}. This callback will only be called as long as both this
616      * app and the middleware are both running -- if either one stops, no further calls on the
617      * provided {@link DownloadStatusListener} will be enqueued.
618      *
619      * If the middleware is not aware of the specified download request,
620      * this method will throw an {@link IllegalArgumentException}.
621      *
622      * If the operation encountered an error, the error code will be delivered via
623      * {@link MbmsDownloadSessionCallback#onError}.
624      *
625      * Repeated calls to this method for the same {@link DownloadRequest} will replace the
626      * previously registered listener.
627      *
628      * @param request The {@link DownloadRequest} that you want updates on.
629      * @param executor The {@link Executor} on which calls to {@code listener } should be executed.
630      * @param listener The listener that should be called when the middleware has information to
631      *                 share on the status download.
632      */
addStatusListener(@onNull DownloadRequest request, @NonNull Executor executor, @NonNull DownloadStatusListener listener)633     public void addStatusListener(@NonNull DownloadRequest request,
634             @NonNull Executor executor, @NonNull DownloadStatusListener listener) {
635         IMbmsDownloadService downloadService = mService.get();
636         if (downloadService == null) {
637             throw new IllegalStateException("Middleware not yet bound");
638         }
639 
640         InternalDownloadStatusListener internalListener =
641                 new InternalDownloadStatusListener(listener, executor);
642 
643         try {
644             int result = downloadService.addStatusListener(request, internalListener);
645             if (result == MbmsErrors.UNKNOWN) {
646                 // Unbind and throw an obvious error
647                 close();
648                 throw new IllegalStateException("Middleware must not return an unknown error code");
649             }
650             if (result != MbmsErrors.SUCCESS) {
651                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
652                     throw new IllegalArgumentException("Unknown download request.");
653                 }
654                 sendErrorToApp(result, null);
655                 return;
656             }
657         } catch (RemoteException e) {
658             mService.set(null);
659             sIsInitialized.set(false);
660             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
661             return;
662         }
663         mInternalDownloadStatusListeners.put(listener, internalListener);
664     }
665 
666     /**
667      * Un-register a listener previously registered via
668      * {@link #addStatusListener(DownloadRequest, Executor, DownloadStatusListener)}. After
669      * this method is called, no further calls will be enqueued on the {@link Executor}
670      * provided upon registration, even if this method throws an exception.
671      *
672      * If the middleware is not aware of the specified download request,
673      * this method will throw an {@link IllegalArgumentException}.
674      *
675      * If the operation encountered an error, the error code will be delivered via
676      * {@link MbmsDownloadSessionCallback#onError}.
677      *
678      * @param request The {@link DownloadRequest} provided during registration
679      * @param listener The listener provided during registration.
680      */
removeStatusListener(@onNull DownloadRequest request, @NonNull DownloadStatusListener listener)681     public void removeStatusListener(@NonNull DownloadRequest request,
682             @NonNull DownloadStatusListener listener) {
683         try {
684             IMbmsDownloadService downloadService = mService.get();
685             if (downloadService == null) {
686                 throw new IllegalStateException("Middleware not yet bound");
687             }
688 
689             InternalDownloadStatusListener internalListener =
690                     mInternalDownloadStatusListeners.get(listener);
691             if (internalListener == null) {
692                 throw new IllegalArgumentException("Provided listener was never registered");
693             }
694 
695             try {
696                 int result = downloadService.removeStatusListener(request, internalListener);
697                 if (result == MbmsErrors.UNKNOWN) {
698                     // Unbind and throw an obvious error
699                     close();
700                     throw new IllegalStateException("Middleware must not return an"
701                             + " unknown error code");
702                 }
703                 if (result != MbmsErrors.SUCCESS) {
704                     if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
705                         throw new IllegalArgumentException("Unknown download request.");
706                     }
707                     sendErrorToApp(result, null);
708                     return;
709                 }
710             } catch (RemoteException e) {
711                 mService.set(null);
712                 sIsInitialized.set(false);
713                 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
714                 return;
715             }
716         } finally {
717             InternalDownloadStatusListener internalCallback =
718                     mInternalDownloadStatusListeners.remove(listener);
719             if (internalCallback != null) {
720                 internalCallback.stop();
721             }
722         }
723     }
724 
725     /**
726      * Registers a progress listener for a {@link DownloadRequest} previously requested via
727      * {@link #download(DownloadRequest)}. This listener will only be called as long as both this
728      * app and the middleware are both running -- if either one stops, no further calls on the
729      * provided {@link DownloadProgressListener} will be enqueued.
730      *
731      * If the middleware is not aware of the specified download request,
732      * this method will throw an {@link IllegalArgumentException}.
733      *
734      * If the operation encountered an error, the error code will be delivered via
735      * {@link MbmsDownloadSessionCallback#onError}.
736      *
737      * Repeated calls to this method for the same {@link DownloadRequest} will replace the
738      * previously registered listener.
739      *
740      * @param request The {@link DownloadRequest} that you want updates on.
741      * @param executor The {@link Executor} on which calls to {@code listener} should be executed.
742      * @param listener The listener that should be called when the middleware has information to
743      *                 share on the progress of the download.
744      */
addProgressListener(@onNull DownloadRequest request, @NonNull Executor executor, @NonNull DownloadProgressListener listener)745     public void addProgressListener(@NonNull DownloadRequest request,
746             @NonNull Executor executor, @NonNull DownloadProgressListener listener) {
747         IMbmsDownloadService downloadService = mService.get();
748         if (downloadService == null) {
749             throw new IllegalStateException("Middleware not yet bound");
750         }
751 
752         InternalDownloadProgressListener internalListener =
753                 new InternalDownloadProgressListener(listener, executor);
754 
755         try {
756             int result = downloadService.addProgressListener(request, internalListener);
757             if (result == MbmsErrors.UNKNOWN) {
758                 // Unbind and throw an obvious error
759                 close();
760                 throw new IllegalStateException("Middleware must not return an unknown error code");
761             }
762             if (result != MbmsErrors.SUCCESS) {
763                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
764                     throw new IllegalArgumentException("Unknown download request.");
765                 }
766                 sendErrorToApp(result, null);
767                 return;
768             }
769         } catch (RemoteException e) {
770             mService.set(null);
771             sIsInitialized.set(false);
772             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
773             return;
774         }
775         mInternalDownloadProgressListeners.put(listener, internalListener);
776     }
777 
778     /**
779      * Un-register a listener previously registered via
780      * {@link #addProgressListener(DownloadRequest, Executor, DownloadProgressListener)}. After
781      * this method is called, no further callbacks will be enqueued on the {@link Handler}
782      * provided upon registration, even if this method throws an exception.
783      *
784      * If the middleware is not aware of the specified download request,
785      * this method will throw an {@link IllegalArgumentException}.
786      *
787      * If the operation encountered an error, the error code will be delivered via
788      * {@link MbmsDownloadSessionCallback#onError}.
789      *
790      * @param request The {@link DownloadRequest} provided during registration
791      * @param listener The listener provided during registration.
792      */
removeProgressListener(@onNull DownloadRequest request, @NonNull DownloadProgressListener listener)793     public void removeProgressListener(@NonNull DownloadRequest request,
794             @NonNull DownloadProgressListener listener) {
795         try {
796             IMbmsDownloadService downloadService = mService.get();
797             if (downloadService == null) {
798                 throw new IllegalStateException("Middleware not yet bound");
799             }
800 
801             InternalDownloadProgressListener internalListener =
802                     mInternalDownloadProgressListeners.get(listener);
803             if (internalListener == null) {
804                 throw new IllegalArgumentException("Provided listener was never registered");
805             }
806 
807             try {
808                 int result = downloadService.removeProgressListener(request, internalListener);
809                 if (result == MbmsErrors.UNKNOWN) {
810                     // Unbind and throw an obvious error
811                     close();
812                     throw new IllegalStateException("Middleware must not"
813                             + " return an unknown error code");
814                 }
815                 if (result != MbmsErrors.SUCCESS) {
816                     if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
817                         throw new IllegalArgumentException("Unknown download request.");
818                     }
819                     sendErrorToApp(result, null);
820                     return;
821                 }
822             } catch (RemoteException e) {
823                 mService.set(null);
824                 sIsInitialized.set(false);
825                 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
826                 return;
827             }
828         } finally {
829             InternalDownloadProgressListener internalCallback =
830                     mInternalDownloadProgressListeners.remove(listener);
831             if (internalCallback != null) {
832                 internalCallback.stop();
833             }
834         }
835     }
836 
837     /**
838      * Attempts to cancel the specified {@link DownloadRequest}.
839      *
840      * If the operation encountered an error, the error code will be delivered via
841      * {@link MbmsDownloadSessionCallback#onError}.
842      *
843      * @param downloadRequest The download request that you wish to cancel.
844      */
cancelDownload(@onNull DownloadRequest downloadRequest)845     public void cancelDownload(@NonNull DownloadRequest downloadRequest) {
846         IMbmsDownloadService downloadService = mService.get();
847         if (downloadService == null) {
848             throw new IllegalStateException("Middleware not yet bound");
849         }
850 
851         try {
852             int result = downloadService.cancelDownload(downloadRequest);
853             if (result == MbmsErrors.UNKNOWN) {
854                 // Unbind and throw an obvious error
855                 close();
856                 throw new IllegalStateException("Middleware must not return an unknown error code");
857             }
858             if (result != MbmsErrors.SUCCESS) {
859                 sendErrorToApp(result, null);
860             } else {
861                 deleteDownloadRequestToken(downloadRequest);
862             }
863         } catch (RemoteException e) {
864             mService.set(null);
865             sIsInitialized.set(false);
866             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
867         }
868     }
869 
870     /**
871      * Requests information about the state of a file pending download.
872      *
873      * The state will be delivered as a callback via
874      * {@link DownloadStatusListener#onStatusUpdated(DownloadRequest, FileInfo, int)}. If no such
875      * callback has been registered via
876      * {@link #addProgressListener(DownloadRequest, Executor, DownloadProgressListener)}, this
877      * method will be a no-op.
878      *
879      * If the middleware has no record of the
880      * file indicated by {@code fileInfo} being associated with {@code downloadRequest},
881      * an {@link IllegalArgumentException} will be thrown.
882      *
883      * @param downloadRequest The download request to query.
884      * @param fileInfo The particular file within the request to get information on.
885      */
requestDownloadState(DownloadRequest downloadRequest, FileInfo fileInfo)886     public void requestDownloadState(DownloadRequest downloadRequest, FileInfo fileInfo) {
887         IMbmsDownloadService downloadService = mService.get();
888         if (downloadService == null) {
889             throw new IllegalStateException("Middleware not yet bound");
890         }
891 
892         try {
893             int result = downloadService.requestDownloadState(downloadRequest, fileInfo);
894             if (result == MbmsErrors.UNKNOWN) {
895                 // Unbind and throw an obvious error
896                 close();
897                 throw new IllegalStateException("Middleware must not return an unknown error code");
898             }
899             if (result != MbmsErrors.SUCCESS) {
900                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
901                     throw new IllegalArgumentException("Unknown download request.");
902                 }
903                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_FILE_INFO) {
904                     throw new IllegalArgumentException("Unknown file.");
905                 }
906                 sendErrorToApp(result, null);
907             }
908         } catch (RemoteException e) {
909             mService.set(null);
910             sIsInitialized.set(false);
911             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
912         }
913     }
914 
915     /**
916      * Resets the middleware's knowledge of previously-downloaded files in this download request.
917      *
918      * Normally, the middleware keeps track of the hashes of downloaded files and won't re-download
919      * files whose server-reported hash matches one of the already-downloaded files. This means
920      * that if the file is accidentally deleted by the user or by the app, the middleware will
921      * not try to download it again.
922      * This method will reset the middleware's cache of hashes for the provided
923      * {@link DownloadRequest}, so that previously downloaded content will be downloaded again
924      * when available.
925      * This will not interrupt in-progress downloads.
926      *
927      * This is distinct from cancelling and re-issuing the download request -- if you cancel and
928      * re-issue, the middleware will not clear its cache of download state information.
929      *
930      * If the middleware is not aware of the specified download request, an
931      * {@link IllegalArgumentException} will be thrown.
932      *
933      * @param downloadRequest The request to re-download files for.
934      */
resetDownloadKnowledge(DownloadRequest downloadRequest)935     public void resetDownloadKnowledge(DownloadRequest downloadRequest) {
936         IMbmsDownloadService downloadService = mService.get();
937         if (downloadService == null) {
938             throw new IllegalStateException("Middleware not yet bound");
939         }
940 
941         try {
942             int result = downloadService.resetDownloadKnowledge(downloadRequest);
943             if (result == MbmsErrors.UNKNOWN) {
944                 // Unbind and throw an obvious error
945                 close();
946                 throw new IllegalStateException("Middleware must not return an unknown error code");
947             }
948             if (result != MbmsErrors.SUCCESS) {
949                 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) {
950                     throw new IllegalArgumentException("Unknown download request.");
951                 }
952                 sendErrorToApp(result, null);
953             }
954         } catch (RemoteException e) {
955             mService.set(null);
956             sIsInitialized.set(false);
957             sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null);
958         }
959     }
960 
961     /**
962      * Terminates this instance.
963      *
964      * After this method returns,
965      * no further callbacks originating from the middleware will be enqueued on the provided
966      * instance of {@link MbmsDownloadSessionCallback}, but callbacks that have already been
967      * enqueued will still be delivered.
968      *
969      * It is safe to call {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)} to
970      * obtain another instance of {@link MbmsDownloadSession} immediately after this method
971      * returns.
972      *
973      * May throw an {@link IllegalStateException}
974      */
975     @Override
close()976     public void close() {
977         try {
978             IMbmsDownloadService downloadService = mService.get();
979             if (downloadService == null || mServiceConnection == null) {
980                 Log.i(LOG_TAG, "Service already dead");
981                 return;
982             }
983             downloadService.dispose(mSubscriptionId);
984             mContext.unbindService(mServiceConnection);
985         } catch (RemoteException e) {
986             // Ignore
987             Log.i(LOG_TAG, "Remote exception while disposing of service");
988         } finally {
989             mService.set(null);
990             sIsInitialized.set(false);
991             mServiceConnection = null;
992             mInternalCallback.stop();
993         }
994     }
995 
writeDownloadRequestToken(DownloadRequest request)996     private void writeDownloadRequestToken(DownloadRequest request) {
997         File token = getDownloadRequestTokenPath(request);
998         if (!token.getParentFile().exists()) {
999             token.getParentFile().mkdirs();
1000         }
1001         if (token.exists()) {
1002             Log.w(LOG_TAG, "Download token " + token.getName() + " already exists");
1003             return;
1004         }
1005         try {
1006             if (!token.createNewFile()) {
1007                 throw new RuntimeException("Failed to create download token for request "
1008                         + request + ". Token location is " + token.getPath());
1009             }
1010         } catch (IOException e) {
1011             throw new RuntimeException("Failed to create download token for request " + request
1012                     + " due to IOException " + e + ". Attempted to write to " + token.getPath());
1013         }
1014     }
1015 
deleteDownloadRequestToken(DownloadRequest request)1016     private void deleteDownloadRequestToken(DownloadRequest request) {
1017         File token = getDownloadRequestTokenPath(request);
1018         if (!token.isFile()) {
1019             Log.w(LOG_TAG, "Attempting to delete non-existent download token at " + token);
1020             return;
1021         }
1022         if (!token.delete()) {
1023             Log.w(LOG_TAG, "Couldn't delete download token at " + token);
1024         }
1025     }
1026 
checkDownloadRequestDestination(DownloadRequest request)1027     private void checkDownloadRequestDestination(DownloadRequest request) {
1028         File downloadRequestDestination = new File(request.getDestinationUri().getPath());
1029         if (!downloadRequestDestination.isDirectory()) {
1030             throw new IllegalArgumentException("The destination path must be a directory");
1031         }
1032         // Check if the request destination is okay to use by attempting to rename an empty
1033         // file to there.
1034         File testFile = new File(MbmsTempFileProvider.getEmbmsTempFileDir(mContext),
1035                 DESTINATION_SANITY_CHECK_FILE_NAME);
1036         File testFileDestination = new File(downloadRequestDestination,
1037                 DESTINATION_SANITY_CHECK_FILE_NAME);
1038 
1039         try {
1040             if (!testFile.exists()) {
1041                 testFile.createNewFile();
1042             }
1043             if (!testFile.renameTo(testFileDestination)) {
1044                 throw new IllegalArgumentException("Destination provided in the download request " +
1045                         "is invalid -- files in the temp file directory cannot be directly moved " +
1046                         "there.");
1047             }
1048         } catch (IOException e) {
1049             throw new IllegalStateException("Got IOException while testing out the destination: "
1050                     + e);
1051         } finally {
1052             testFile.delete();
1053             testFileDestination.delete();
1054         }
1055     }
1056 
getDownloadRequestTokenPath(DownloadRequest request)1057     private File getDownloadRequestTokenPath(DownloadRequest request) {
1058         File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext,
1059                 request.getFileServiceId());
1060         String downloadTokenFileName = request.getHash()
1061                 + MbmsDownloadReceiver.DOWNLOAD_TOKEN_SUFFIX;
1062         return new File(tempFileLocation, downloadTokenFileName);
1063     }
1064 
sendErrorToApp(int errorCode, String message)1065     private void sendErrorToApp(int errorCode, String message) {
1066         mInternalCallback.onError(errorCode, message);
1067     }
1068 }
1069