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