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