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 android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SdkConstant; 23 import android.annotation.SystemApi; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.ServiceConnection; 28 import android.content.SharedPreferences; 29 import android.net.Uri; 30 import android.os.Handler; 31 import android.os.IBinder; 32 import android.os.Looper; 33 import android.os.RemoteException; 34 import android.telephony.mbms.DownloadStateCallback; 35 import android.telephony.mbms.FileInfo; 36 import android.telephony.mbms.DownloadRequest; 37 import android.telephony.mbms.InternalDownloadSessionCallback; 38 import android.telephony.mbms.InternalDownloadStateCallback; 39 import android.telephony.mbms.MbmsDownloadSessionCallback; 40 import android.telephony.mbms.MbmsDownloadReceiver; 41 import android.telephony.mbms.MbmsErrors; 42 import android.telephony.mbms.MbmsTempFileProvider; 43 import android.telephony.mbms.MbmsUtils; 44 import android.telephony.mbms.vendor.IMbmsDownloadService; 45 import android.util.Log; 46 47 import java.io.File; 48 import java.io.IOException; 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.concurrent.atomic.AtomicBoolean; 56 import java.util.concurrent.atomic.AtomicReference; 57 58 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; 59 60 /** 61 * This class provides functionality for file download over MBMS. 62 * @hide 63 */ 64 public 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