1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.telephony; 18 19 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.SdkConstant; 25 import android.annotation.SystemApi; 26 import android.annotation.TestApi; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.ServiceConnection; 31 import android.content.SharedPreferences; 32 import android.net.Uri; 33 import android.os.Handler; 34 import android.os.IBinder; 35 import android.os.RemoteException; 36 import android.telephony.mbms.DownloadProgressListener; 37 import android.telephony.mbms.DownloadRequest; 38 import android.telephony.mbms.DownloadStatusListener; 39 import android.telephony.mbms.FileInfo; 40 import android.telephony.mbms.InternalDownloadProgressListener; 41 import android.telephony.mbms.InternalDownloadSessionCallback; 42 import android.telephony.mbms.InternalDownloadStatusListener; 43 import android.telephony.mbms.MbmsDownloadReceiver; 44 import android.telephony.mbms.MbmsDownloadSessionCallback; 45 import android.telephony.mbms.MbmsErrors; 46 import android.telephony.mbms.MbmsTempFileProvider; 47 import android.telephony.mbms.MbmsUtils; 48 import android.telephony.mbms.vendor.IMbmsDownloadService; 49 import android.util.Log; 50 51 import java.io.File; 52 import java.io.IOException; 53 import java.lang.annotation.Retention; 54 import java.lang.annotation.RetentionPolicy; 55 import java.util.Collections; 56 import java.util.HashMap; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.concurrent.Executor; 60 import java.util.concurrent.atomic.AtomicBoolean; 61 import java.util.concurrent.atomic.AtomicReference; 62 63 /** 64 * This class provides functionality for file download over MBMS. 65 */ 66 public class MbmsDownloadSession implements AutoCloseable { 67 private static final String LOG_TAG = MbmsDownloadSession.class.getSimpleName(); 68 69 /** 70 * Service action which must be handled by the middleware implementing the MBMS file download 71 * interface. 72 * @hide 73 */ 74 @SystemApi 75 @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) 76 public static final String MBMS_DOWNLOAD_SERVICE_ACTION = 77 "android.telephony.action.EmbmsDownload"; 78 79 /** 80 * Metadata key that specifies the component name of the service to bind to for file-download. 81 * @hide 82 */ 83 @TestApi 84 public static final String MBMS_DOWNLOAD_SERVICE_OVERRIDE_METADATA = 85 "mbms-download-service-override"; 86 87 /** 88 * Integer extra that Android will attach to the intent supplied via 89 * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} 90 * Indicates the result code of the download. One of 91 * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, {@link #RESULT_CANCELLED}, 92 * {@link #RESULT_IO_ERROR}, {@link #RESULT_DOWNLOAD_FAILURE}, {@link #RESULT_OUT_OF_STORAGE}, 93 * {@link #RESULT_SERVICE_ID_NOT_DEFINED}, or {@link #RESULT_FILE_ROOT_UNREACHABLE}. 94 * 95 * This extra may also be used by the middleware when it is sending intents to the app. 96 */ 97 public static final String EXTRA_MBMS_DOWNLOAD_RESULT = 98 "android.telephony.extra.MBMS_DOWNLOAD_RESULT"; 99 100 /** 101 * {@link FileInfo} extra that Android will attach to the intent supplied via 102 * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} 103 * Indicates the file for which the download result is for. Never null. 104 * 105 * This extra may also be used by the middleware when it is sending intents to the app. 106 */ 107 public static final String EXTRA_MBMS_FILE_INFO = "android.telephony.extra.MBMS_FILE_INFO"; 108 109 /** 110 * {@link Uri} extra that Android will attach to the intent supplied via 111 * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)} 112 * Indicates the location of the successfully downloaded file within the directory that the 113 * app provided via the builder. 114 * 115 * Will always be set to a non-null value if 116 * {@link #EXTRA_MBMS_DOWNLOAD_RESULT} is set to {@link #RESULT_SUCCESSFUL}. 117 */ 118 public static final String EXTRA_MBMS_COMPLETED_FILE_URI = 119 "android.telephony.extra.MBMS_COMPLETED_FILE_URI"; 120 121 /** 122 * Extra containing the {@link DownloadRequest} for which the download result or file 123 * descriptor request is for. Must not be null. 124 */ 125 public static final String EXTRA_MBMS_DOWNLOAD_REQUEST = 126 "android.telephony.extra.MBMS_DOWNLOAD_REQUEST"; 127 128 /** 129 * The default directory name for all MBMS temp files. If you call 130 * {@link #download(DownloadRequest)} without first calling 131 * {@link #setTempFileRootDirectory(File)}, this directory will be created for you under the 132 * path returned by {@link Context#getFilesDir()}. 133 */ 134 public static final String DEFAULT_TOP_LEVEL_TEMP_DIRECTORY = "androidMbmsTempFileRoot"; 135 136 137 /** @hide */ 138 @Retention(RetentionPolicy.SOURCE) 139 @IntDef(value = {RESULT_SUCCESSFUL, RESULT_CANCELLED, RESULT_EXPIRED, RESULT_IO_ERROR, 140 RESULT_SERVICE_ID_NOT_DEFINED, RESULT_DOWNLOAD_FAILURE, RESULT_OUT_OF_STORAGE, 141 RESULT_FILE_ROOT_UNREACHABLE}, prefix = { "RESULT_" }) 142 public @interface DownloadResultCode{} 143 144 /** 145 * Indicates that the download was successful. 146 */ 147 public static final int RESULT_SUCCESSFUL = 1; 148 149 /** 150 * Indicates that the download was cancelled via {@link #cancelDownload(DownloadRequest)}. 151 */ 152 public static final int RESULT_CANCELLED = 2; 153 154 /** 155 * Indicates that the download will not be completed due to the expiration of its download 156 * window on the carrier's network. 157 */ 158 public static final int RESULT_EXPIRED = 3; 159 160 /** 161 * Indicates that the download will not be completed due to an I/O error incurred while 162 * writing to temp files. 163 * 164 * This is likely a transient error and another {@link DownloadRequest} should be sent to try 165 * the download again. 166 */ 167 public static final int RESULT_IO_ERROR = 4; 168 169 /** 170 * Indicates that the Service ID specified in the {@link DownloadRequest} is incorrect due to 171 * the Id being incorrect, stale, expired, or similar. 172 */ 173 public static final int RESULT_SERVICE_ID_NOT_DEFINED = 5; 174 175 /** 176 * Indicates that there was an error while processing downloaded files, such as a file repair or 177 * file decoding error and is not due to a file I/O error. 178 * 179 * This is likely a transient error and another {@link DownloadRequest} should be sent to try 180 * the download again. 181 */ 182 public static final int RESULT_DOWNLOAD_FAILURE = 6; 183 184 /** 185 * Indicates that the file system is full and the {@link DownloadRequest} can not complete. 186 * Either space must be made on the current file system or the temp file root location must be 187 * changed to a location that is not full to download the temp files. 188 */ 189 public static final int RESULT_OUT_OF_STORAGE = 7; 190 191 /** 192 * Indicates that the file root that was set is currently unreachable. This can happen if the 193 * temp files are set to be stored on external storage and the SD card was removed, for example. 194 * The temp file root should be changed before sending another DownloadRequest. 195 */ 196 public static final int RESULT_FILE_ROOT_UNREACHABLE = 8; 197 198 /** @hide */ 199 @Retention(RetentionPolicy.SOURCE) 200 @IntDef({STATUS_UNKNOWN, STATUS_ACTIVELY_DOWNLOADING, STATUS_PENDING_DOWNLOAD, 201 STATUS_PENDING_REPAIR, STATUS_PENDING_DOWNLOAD_WINDOW}) 202 public @interface DownloadStatus {} 203 204 /** 205 * Indicates that the middleware has no information on the file. 206 */ 207 public static final int STATUS_UNKNOWN = 0; 208 209 /** 210 * Indicates that the file is actively being downloaded. 211 */ 212 public static final int STATUS_ACTIVELY_DOWNLOADING = 1; 213 214 /** 215 * Indicates that the file is awaiting the next download or repair operations. When a more 216 * precise status is known, the status will change to either {@link #STATUS_PENDING_REPAIR} or 217 * {@link #STATUS_PENDING_DOWNLOAD_WINDOW}. 218 */ 219 public static final int STATUS_PENDING_DOWNLOAD = 2; 220 221 /** 222 * Indicates that the file is awaiting file repair after the download has ended. 223 */ 224 public static final int STATUS_PENDING_REPAIR = 3; 225 226 /** 227 * Indicates that the file is waiting to download because its download window has not yet 228 * started and is scheduled for a future time. 229 */ 230 public static final int STATUS_PENDING_DOWNLOAD_WINDOW = 4; 231 232 private static final String DESTINATION_SANITY_CHECK_FILE_NAME = "destinationSanityCheckFile"; 233 234 private static AtomicBoolean sIsInitialized = new AtomicBoolean(false); 235 236 private final Context mContext; 237 private int mSubscriptionId = INVALID_SUBSCRIPTION_ID; 238 private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { 239 @Override 240 public void binderDied() { 241 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, "Received death notification"); 242 } 243 }; 244 245 private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null); 246 private final InternalDownloadSessionCallback mInternalCallback; 247 private final Map<DownloadStatusListener, InternalDownloadStatusListener> 248 mInternalDownloadStatusListeners = new HashMap<>(); 249 private final Map<DownloadProgressListener, InternalDownloadProgressListener> 250 mInternalDownloadProgressListeners = new HashMap<>(); 251 252 private MbmsDownloadSession(Context context, Executor executor, int subscriptionId, 253 MbmsDownloadSessionCallback callback) { 254 mContext = context; 255 mSubscriptionId = subscriptionId; 256 mInternalCallback = new InternalDownloadSessionCallback(callback, executor); 257 } 258 259 /** 260 * Create a new {@link MbmsDownloadSession} using the system default data subscription ID. 261 * See {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)} 262 */ 263 public static MbmsDownloadSession create(@NonNull Context context, 264 @NonNull Executor executor, @NonNull MbmsDownloadSessionCallback callback) { 265 return create(context, executor, SubscriptionManager.getDefaultSubscriptionId(), callback); 266 } 267 268 /** 269 * Create a new MbmsDownloadManager using the given subscription ID. 270 * 271 * Note that this call will bind a remote service and that may take a bit. The instance of 272 * {@link MbmsDownloadSession} that is returned will not be ready for use until 273 * {@link MbmsDownloadSessionCallback#onMiddlewareReady()} is called on the provided callback. 274 * If you attempt to use the instance before it is ready, an {@link IllegalStateException} 275 * will be thrown or an error will be delivered through 276 * {@link MbmsDownloadSessionCallback#onError(int, String)}. 277 * 278 * This also may throw an {@link IllegalArgumentException}. 279 * 280 * You may only have one instance of {@link MbmsDownloadSession} per UID. If you call this 281 * method while there is an active instance of {@link MbmsDownloadSession} in your process 282 * (in other words, one that has not had {@link #close()} called on it), this method will 283 * throw an {@link IllegalStateException}. If you call this method in a different process 284 * running under the same UID, an error will be indicated via 285 * {@link MbmsDownloadSessionCallback#onError(int, String)}. 286 * 287 * Note that initialization may fail asynchronously. If you wish to try again after you 288 * receive such an asynchronous error, you must call {@link #close()} on the instance of 289 * {@link MbmsDownloadSession} that you received before calling this method again. 290 * 291 * @param context The instance of {@link Context} to use 292 * @param executor The executor on which you wish to execute callbacks. 293 * @param subscriptionId The data subscription ID to use 294 * @param callback A callback to get asynchronous error messages and file service updates. 295 * @return A new instance of {@link MbmsDownloadSession}, or null if an error occurred during 296 * setup. 297 */ 298 public static @Nullable MbmsDownloadSession create(@NonNull Context context, 299 @NonNull Executor executor, int subscriptionId, 300 final @NonNull MbmsDownloadSessionCallback callback) { 301 if (!sIsInitialized.compareAndSet(false, true)) { 302 throw new IllegalStateException("Cannot have two active instances"); 303 } 304 MbmsDownloadSession session = 305 new MbmsDownloadSession(context, executor, subscriptionId, callback); 306 final int result = session.bindAndInitialize(); 307 if (result != MbmsErrors.SUCCESS) { 308 sIsInitialized.set(false); 309 executor.execute(new Runnable() { 310 @Override 311 public void run() { 312 callback.onError(result, null); 313 } 314 }); 315 return null; 316 } 317 return session; 318 } 319 320 private int bindAndInitialize() { 321 return MbmsUtils.startBinding(mContext, MBMS_DOWNLOAD_SERVICE_ACTION, 322 new ServiceConnection() { 323 @Override 324 public void onServiceConnected(ComponentName name, IBinder service) { 325 IMbmsDownloadService downloadService = 326 IMbmsDownloadService.Stub.asInterface(service); 327 int result; 328 try { 329 result = downloadService.initialize(mSubscriptionId, mInternalCallback); 330 } catch (RemoteException e) { 331 Log.e(LOG_TAG, "Service died before initialization"); 332 sIsInitialized.set(false); 333 return; 334 } catch (RuntimeException e) { 335 Log.e(LOG_TAG, "Runtime exception during initialization"); 336 sendErrorToApp( 337 MbmsErrors.InitializationErrors.ERROR_UNABLE_TO_INITIALIZE, 338 e.toString()); 339 sIsInitialized.set(false); 340 return; 341 } 342 if (result == MbmsErrors.UNKNOWN) { 343 // Unbind and throw an obvious error 344 close(); 345 throw new IllegalStateException("Middleware must not return an" 346 + " unknown error code"); 347 } 348 if (result != MbmsErrors.SUCCESS) { 349 sendErrorToApp(result, "Error returned during initialization"); 350 sIsInitialized.set(false); 351 return; 352 } 353 try { 354 downloadService.asBinder().linkToDeath(mDeathRecipient, 0); 355 } catch (RemoteException e) { 356 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, 357 "Middleware lost during initialization"); 358 sIsInitialized.set(false); 359 return; 360 } 361 mService.set(downloadService); 362 } 363 364 @Override 365 public void onServiceDisconnected(ComponentName name) { 366 Log.w(LOG_TAG, "bindAndInitialize: Remote service disconnected"); 367 sIsInitialized.set(false); 368 mService.set(null); 369 } 370 }); 371 } 372 373 /** 374 * An inspection API to retrieve the list of available 375 * {@link android.telephony.mbms.FileServiceInfo}s currently being advertised. 376 * The results are returned asynchronously via a call to 377 * {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)} 378 * 379 * Asynchronous error codes via the {@link MbmsDownloadSessionCallback#onError(int, String)} 380 * callback may include any of the errors that are not specific to the streaming use-case. 381 * 382 * May throw an {@link IllegalStateException} or {@link IllegalArgumentException}. 383 * 384 * @param classList A list of service classes which the app wishes to receive 385 * {@link MbmsDownloadSessionCallback#onFileServicesUpdated(List)} callbacks 386 * about. Subsequent calls to this method will replace this list of service 387 * classes (i.e. the middleware will no longer send updates for services 388 * matching classes only in the old list). 389 * Values in this list should be negotiated with the wireless carrier prior 390 * to using this API. 391 */ 392 public void requestUpdateFileServices(@NonNull List<String> classList) { 393 IMbmsDownloadService downloadService = mService.get(); 394 if (downloadService == null) { 395 throw new IllegalStateException("Middleware not yet bound"); 396 } 397 try { 398 int returnCode = downloadService.requestUpdateFileServices(mSubscriptionId, classList); 399 if (returnCode == MbmsErrors.UNKNOWN) { 400 // Unbind and throw an obvious error 401 close(); 402 throw new IllegalStateException("Middleware must not return an unknown error code"); 403 } 404 if (returnCode != MbmsErrors.SUCCESS) { 405 sendErrorToApp(returnCode, null); 406 } 407 } catch (RemoteException e) { 408 Log.w(LOG_TAG, "Remote process died"); 409 mService.set(null); 410 sIsInitialized.set(false); 411 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 412 } 413 } 414 415 /** 416 * Sets the temp file root for downloads. 417 * All temp files created for the middleware to write to will be contained in the specified 418 * directory. Applications that wish to specify a location only need to call this method once 419 * as long their data is persisted in storage -- the argument will be stored both in a 420 * local instance of {@link android.content.SharedPreferences} and by the middleware. 421 * 422 * If this method is not called at least once before calling 423 * {@link #download(DownloadRequest)}, the framework 424 * will default to a directory formed by the concatenation of the app's files directory and 425 * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}. 426 * 427 * Before calling this method, the app must cancel all of its pending 428 * {@link DownloadRequest}s via {@link #cancelDownload(DownloadRequest)}. If this is not done, 429 * you will receive an asynchronous error with code 430 * {@link MbmsErrors.DownloadErrors#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT} unless the 431 * provided directory is the same as what has been previously configured. 432 * 433 * The {@link File} supplied as a root temp file directory must already exist. If not, an 434 * {@link IllegalArgumentException} will be thrown. In addition, as an additional sanity 435 * check, an {@link IllegalArgumentException} will be thrown if you attempt to set the temp 436 * file root directory to one of your data roots (the value of {@link Context#getDataDir()}, 437 * {@link Context#getFilesDir()}, or {@link Context#getCacheDir()}). 438 * @param tempFileRootDirectory A directory to place temp files in. 439 */ 440 public void setTempFileRootDirectory(@NonNull File tempFileRootDirectory) { 441 IMbmsDownloadService downloadService = mService.get(); 442 if (downloadService == null) { 443 throw new IllegalStateException("Middleware not yet bound"); 444 } 445 try { 446 validateTempFileRootSanity(tempFileRootDirectory); 447 } catch (IOException e) { 448 throw new IllegalStateException("Got IOException checking directory sanity"); 449 } 450 String filePath; 451 try { 452 filePath = tempFileRootDirectory.getCanonicalPath(); 453 } catch (IOException e) { 454 throw new IllegalArgumentException("Unable to canonicalize the provided path: " + e); 455 } 456 457 try { 458 int result = downloadService.setTempFileRootDirectory(mSubscriptionId, filePath); 459 if (result == MbmsErrors.UNKNOWN) { 460 // Unbind and throw an obvious error 461 close(); 462 throw new IllegalStateException("Middleware must not return an unknown error code"); 463 } 464 if (result != MbmsErrors.SUCCESS) { 465 sendErrorToApp(result, null); 466 return; 467 } 468 } catch (RemoteException e) { 469 mService.set(null); 470 sIsInitialized.set(false); 471 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 472 return; 473 } 474 475 SharedPreferences prefs = mContext.getSharedPreferences( 476 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); 477 prefs.edit().putString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, filePath).apply(); 478 } 479 480 private void validateTempFileRootSanity(File tempFileRootDirectory) throws IOException { 481 if (!tempFileRootDirectory.exists()) { 482 throw new IllegalArgumentException("Provided directory does not exist"); 483 } 484 if (!tempFileRootDirectory.isDirectory()) { 485 throw new IllegalArgumentException("Provided File is not a directory"); 486 } 487 String canonicalTempFilePath = tempFileRootDirectory.getCanonicalPath(); 488 if (mContext.getDataDir().getCanonicalPath().equals(canonicalTempFilePath)) { 489 throw new IllegalArgumentException("Temp file root cannot be your data dir"); 490 } 491 if (mContext.getCacheDir().getCanonicalPath().equals(canonicalTempFilePath)) { 492 throw new IllegalArgumentException("Temp file root cannot be your cache dir"); 493 } 494 if (mContext.getFilesDir().getCanonicalPath().equals(canonicalTempFilePath)) { 495 throw new IllegalArgumentException("Temp file root cannot be your files dir"); 496 } 497 } 498 /** 499 * Retrieves the currently configured temp file root directory. Returns the file that was 500 * configured via {@link #setTempFileRootDirectory(File)} or the default directory 501 * {@link #download(DownloadRequest)} was called without ever 502 * setting the temp file root. If neither method has been called since the last time the app's 503 * shared preferences were reset, returns {@code null}. 504 * 505 * @return A {@link File} pointing to the configured temp file directory, or null if not yet 506 * configured. 507 */ 508 public @Nullable File getTempFileRootDirectory() { 509 SharedPreferences prefs = mContext.getSharedPreferences( 510 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); 511 String path = prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null); 512 if (path != null) { 513 return new File(path); 514 } 515 return null; 516 } 517 518 /** 519 * Requests the download of a file or set of files that the carrier has indicated to be 520 * available. 521 * 522 * May throw an {@link IllegalArgumentException} 523 * 524 * If {@link #setTempFileRootDirectory(File)} has not called after the app has been installed, 525 * this method will create a directory at the default location defined at 526 * {@link MbmsDownloadSession#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp 527 * file root directory. 528 * 529 * If the {@link DownloadRequest} has a destination that is not on the same filesystem as the 530 * temp file directory provided via {@link #getTempFileRootDirectory()}, an 531 * {@link IllegalArgumentException} will be thrown. 532 * 533 * Asynchronous errors through the callback may include any error not specific to the 534 * streaming use-case. 535 * 536 * If no error is delivered via the callback after calling this method, that means that the 537 * middleware has successfully started the download or scheduled the download, if the download 538 * is at a future time. 539 * @param request The request that specifies what should be downloaded. 540 */ 541 public void download(@NonNull DownloadRequest request) { 542 IMbmsDownloadService downloadService = mService.get(); 543 if (downloadService == null) { 544 throw new IllegalStateException("Middleware not yet bound"); 545 } 546 547 // Check to see whether the app's set a temp root dir yet, and set it if not. 548 SharedPreferences prefs = mContext.getSharedPreferences( 549 MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0); 550 if (prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null) == null) { 551 File tempRootDirectory = new File(mContext.getFilesDir(), 552 DEFAULT_TOP_LEVEL_TEMP_DIRECTORY); 553 tempRootDirectory.mkdirs(); 554 setTempFileRootDirectory(tempRootDirectory); 555 } 556 557 checkDownloadRequestDestination(request); 558 559 try { 560 int result = downloadService.download(request); 561 if (result == MbmsErrors.SUCCESS) { 562 writeDownloadRequestToken(request); 563 } else { 564 if (result == MbmsErrors.UNKNOWN) { 565 // Unbind and throw an obvious error 566 close(); 567 throw new IllegalStateException("Middleware must not return an unknown" 568 + " error code"); 569 } 570 sendErrorToApp(result, null); 571 } 572 } catch (RemoteException e) { 573 mService.set(null); 574 sIsInitialized.set(false); 575 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 576 } 577 } 578 579 /** 580 * Returns a list of pending {@link DownloadRequest}s that originated from this application. 581 * A pending request is one that was issued via 582 * {@link #download(DownloadRequest)} but not cancelled through 583 * {@link #cancelDownload(DownloadRequest)}. 584 * @return A list, possibly empty, of {@link DownloadRequest}s 585 */ 586 public @NonNull List<DownloadRequest> listPendingDownloads() { 587 IMbmsDownloadService downloadService = mService.get(); 588 if (downloadService == null) { 589 throw new IllegalStateException("Middleware not yet bound"); 590 } 591 592 try { 593 return downloadService.listPendingDownloads(mSubscriptionId); 594 } catch (RemoteException e) { 595 mService.set(null); 596 sIsInitialized.set(false); 597 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 598 return Collections.emptyList(); 599 } 600 } 601 602 /** 603 * Registers a download status listener for a {@link DownloadRequest} previously requested via 604 * {@link #download(DownloadRequest)}. This callback will only be called as long as both this 605 * app and the middleware are both running -- if either one stops, no further calls on the 606 * provided {@link DownloadStatusListener} will be enqueued. 607 * 608 * If the middleware is not aware of the specified download request, 609 * this method will throw an {@link IllegalArgumentException}. 610 * 611 * If the operation encountered an error, the error code will be delivered via 612 * {@link MbmsDownloadSessionCallback#onError}. 613 * 614 * Repeated calls to this method for the same {@link DownloadRequest} will replace the 615 * previously registered listener. 616 * 617 * @param request The {@link DownloadRequest} that you want updates on. 618 * @param executor The {@link Executor} on which calls to {@code listener } should be executed. 619 * @param listener The listener that should be called when the middleware has information to 620 * share on the status download. 621 */ 622 public void addStatusListener(@NonNull DownloadRequest request, 623 @NonNull Executor executor, @NonNull DownloadStatusListener listener) { 624 IMbmsDownloadService downloadService = mService.get(); 625 if (downloadService == null) { 626 throw new IllegalStateException("Middleware not yet bound"); 627 } 628 629 InternalDownloadStatusListener internalListener = 630 new InternalDownloadStatusListener(listener, executor); 631 632 try { 633 int result = downloadService.addStatusListener(request, internalListener); 634 if (result == MbmsErrors.UNKNOWN) { 635 // Unbind and throw an obvious error 636 close(); 637 throw new IllegalStateException("Middleware must not return an unknown error code"); 638 } 639 if (result != MbmsErrors.SUCCESS) { 640 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { 641 throw new IllegalArgumentException("Unknown download request."); 642 } 643 sendErrorToApp(result, null); 644 return; 645 } 646 } catch (RemoteException e) { 647 mService.set(null); 648 sIsInitialized.set(false); 649 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 650 return; 651 } 652 mInternalDownloadStatusListeners.put(listener, internalListener); 653 } 654 655 /** 656 * Un-register a listener previously registered via 657 * {@link #addStatusListener(DownloadRequest, Executor, DownloadStatusListener)}. After 658 * this method is called, no further calls will be enqueued on the {@link Executor} 659 * provided upon registration, even if this method throws an exception. 660 * 661 * If the middleware is not aware of the specified download request, 662 * this method will throw an {@link IllegalArgumentException}. 663 * 664 * If the operation encountered an error, the error code will be delivered via 665 * {@link MbmsDownloadSessionCallback#onError}. 666 * 667 * @param request The {@link DownloadRequest} provided during registration 668 * @param listener The listener provided during registration. 669 */ 670 public void removeStatusListener(@NonNull DownloadRequest request, 671 @NonNull DownloadStatusListener listener) { 672 try { 673 IMbmsDownloadService downloadService = mService.get(); 674 if (downloadService == null) { 675 throw new IllegalStateException("Middleware not yet bound"); 676 } 677 678 InternalDownloadStatusListener internalListener = 679 mInternalDownloadStatusListeners.get(listener); 680 if (internalListener == null) { 681 throw new IllegalArgumentException("Provided listener was never registered"); 682 } 683 684 try { 685 int result = downloadService.removeStatusListener(request, internalListener); 686 if (result == MbmsErrors.UNKNOWN) { 687 // Unbind and throw an obvious error 688 close(); 689 throw new IllegalStateException("Middleware must not return an" 690 + " unknown error code"); 691 } 692 if (result != MbmsErrors.SUCCESS) { 693 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { 694 throw new IllegalArgumentException("Unknown download request."); 695 } 696 sendErrorToApp(result, null); 697 return; 698 } 699 } catch (RemoteException e) { 700 mService.set(null); 701 sIsInitialized.set(false); 702 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 703 return; 704 } 705 } finally { 706 InternalDownloadStatusListener internalCallback = 707 mInternalDownloadStatusListeners.remove(listener); 708 if (internalCallback != null) { 709 internalCallback.stop(); 710 } 711 } 712 } 713 714 /** 715 * Registers a progress listener for a {@link DownloadRequest} previously requested via 716 * {@link #download(DownloadRequest)}. This listener will only be called as long as both this 717 * app and the middleware are both running -- if either one stops, no further calls on the 718 * provided {@link DownloadProgressListener} will be enqueued. 719 * 720 * If the middleware is not aware of the specified download request, 721 * this method will throw an {@link IllegalArgumentException}. 722 * 723 * If the operation encountered an error, the error code will be delivered via 724 * {@link MbmsDownloadSessionCallback#onError}. 725 * 726 * Repeated calls to this method for the same {@link DownloadRequest} will replace the 727 * previously registered listener. 728 * 729 * @param request The {@link DownloadRequest} that you want updates on. 730 * @param executor The {@link Executor} on which calls to {@code listener} should be executed. 731 * @param listener The listener that should be called when the middleware has information to 732 * share on the progress of the download. 733 */ 734 public void addProgressListener(@NonNull DownloadRequest request, 735 @NonNull Executor executor, @NonNull DownloadProgressListener listener) { 736 IMbmsDownloadService downloadService = mService.get(); 737 if (downloadService == null) { 738 throw new IllegalStateException("Middleware not yet bound"); 739 } 740 741 InternalDownloadProgressListener internalListener = 742 new InternalDownloadProgressListener(listener, executor); 743 744 try { 745 int result = downloadService.addProgressListener(request, internalListener); 746 if (result == MbmsErrors.UNKNOWN) { 747 // Unbind and throw an obvious error 748 close(); 749 throw new IllegalStateException("Middleware must not return an unknown error code"); 750 } 751 if (result != MbmsErrors.SUCCESS) { 752 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { 753 throw new IllegalArgumentException("Unknown download request."); 754 } 755 sendErrorToApp(result, null); 756 return; 757 } 758 } catch (RemoteException e) { 759 mService.set(null); 760 sIsInitialized.set(false); 761 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 762 return; 763 } 764 mInternalDownloadProgressListeners.put(listener, internalListener); 765 } 766 767 /** 768 * Un-register a listener previously registered via 769 * {@link #addProgressListener(DownloadRequest, Executor, DownloadProgressListener)}. After 770 * this method is called, no further callbacks will be enqueued on the {@link Handler} 771 * provided upon registration, even if this method throws an exception. 772 * 773 * If the middleware is not aware of the specified download request, 774 * this method will throw an {@link IllegalArgumentException}. 775 * 776 * If the operation encountered an error, the error code will be delivered via 777 * {@link MbmsDownloadSessionCallback#onError}. 778 * 779 * @param request The {@link DownloadRequest} provided during registration 780 * @param listener The listener provided during registration. 781 */ 782 public void removeProgressListener(@NonNull DownloadRequest request, 783 @NonNull DownloadProgressListener listener) { 784 try { 785 IMbmsDownloadService downloadService = mService.get(); 786 if (downloadService == null) { 787 throw new IllegalStateException("Middleware not yet bound"); 788 } 789 790 InternalDownloadProgressListener internalListener = 791 mInternalDownloadProgressListeners.get(listener); 792 if (internalListener == null) { 793 throw new IllegalArgumentException("Provided listener was never registered"); 794 } 795 796 try { 797 int result = downloadService.removeProgressListener(request, internalListener); 798 if (result == MbmsErrors.UNKNOWN) { 799 // Unbind and throw an obvious error 800 close(); 801 throw new IllegalStateException("Middleware must not" 802 + " return an unknown error code"); 803 } 804 if (result != MbmsErrors.SUCCESS) { 805 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { 806 throw new IllegalArgumentException("Unknown download request."); 807 } 808 sendErrorToApp(result, null); 809 return; 810 } 811 } catch (RemoteException e) { 812 mService.set(null); 813 sIsInitialized.set(false); 814 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 815 return; 816 } 817 } finally { 818 InternalDownloadProgressListener internalCallback = 819 mInternalDownloadProgressListeners.remove(listener); 820 if (internalCallback != null) { 821 internalCallback.stop(); 822 } 823 } 824 } 825 826 /** 827 * Attempts to cancel the specified {@link DownloadRequest}. 828 * 829 * If the operation encountered an error, the error code will be delivered via 830 * {@link MbmsDownloadSessionCallback#onError}. 831 * 832 * @param downloadRequest The download request that you wish to cancel. 833 */ 834 public void cancelDownload(@NonNull DownloadRequest downloadRequest) { 835 IMbmsDownloadService downloadService = mService.get(); 836 if (downloadService == null) { 837 throw new IllegalStateException("Middleware not yet bound"); 838 } 839 840 try { 841 int result = downloadService.cancelDownload(downloadRequest); 842 if (result == MbmsErrors.UNKNOWN) { 843 // Unbind and throw an obvious error 844 close(); 845 throw new IllegalStateException("Middleware must not return an unknown error code"); 846 } 847 if (result != MbmsErrors.SUCCESS) { 848 sendErrorToApp(result, null); 849 } else { 850 deleteDownloadRequestToken(downloadRequest); 851 } 852 } catch (RemoteException e) { 853 mService.set(null); 854 sIsInitialized.set(false); 855 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 856 } 857 } 858 859 /** 860 * Requests information about the state of a file pending download. 861 * 862 * The state will be delivered as a callback via 863 * {@link DownloadStatusListener#onStatusUpdated(DownloadRequest, FileInfo, int)}. If no such 864 * callback has been registered via 865 * {@link #addProgressListener(DownloadRequest, Executor, DownloadProgressListener)}, this 866 * method will be a no-op. 867 * 868 * If the middleware has no record of the 869 * file indicated by {@code fileInfo} being associated with {@code downloadRequest}, 870 * an {@link IllegalArgumentException} will be thrown. 871 * 872 * @param downloadRequest The download request to query. 873 * @param fileInfo The particular file within the request to get information on. 874 */ 875 public void requestDownloadState(DownloadRequest downloadRequest, FileInfo fileInfo) { 876 IMbmsDownloadService downloadService = mService.get(); 877 if (downloadService == null) { 878 throw new IllegalStateException("Middleware not yet bound"); 879 } 880 881 try { 882 int result = downloadService.requestDownloadState(downloadRequest, fileInfo); 883 if (result == MbmsErrors.UNKNOWN) { 884 // Unbind and throw an obvious error 885 close(); 886 throw new IllegalStateException("Middleware must not return an unknown error code"); 887 } 888 if (result != MbmsErrors.SUCCESS) { 889 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { 890 throw new IllegalArgumentException("Unknown download request."); 891 } 892 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_FILE_INFO) { 893 throw new IllegalArgumentException("Unknown file."); 894 } 895 sendErrorToApp(result, null); 896 } 897 } catch (RemoteException e) { 898 mService.set(null); 899 sIsInitialized.set(false); 900 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 901 } 902 } 903 904 /** 905 * Resets the middleware's knowledge of previously-downloaded files in this download request. 906 * 907 * Normally, the middleware keeps track of the hashes of downloaded files and won't re-download 908 * files whose server-reported hash matches one of the already-downloaded files. This means 909 * that if the file is accidentally deleted by the user or by the app, the middleware will 910 * not try to download it again. 911 * This method will reset the middleware's cache of hashes for the provided 912 * {@link DownloadRequest}, so that previously downloaded content will be downloaded again 913 * when available. 914 * This will not interrupt in-progress downloads. 915 * 916 * This is distinct from cancelling and re-issuing the download request -- if you cancel and 917 * re-issue, the middleware will not clear its cache of download state information. 918 * 919 * If the middleware is not aware of the specified download request, an 920 * {@link IllegalArgumentException} will be thrown. 921 * 922 * @param downloadRequest The request to re-download files for. 923 */ 924 public void resetDownloadKnowledge(DownloadRequest downloadRequest) { 925 IMbmsDownloadService downloadService = mService.get(); 926 if (downloadService == null) { 927 throw new IllegalStateException("Middleware not yet bound"); 928 } 929 930 try { 931 int result = downloadService.resetDownloadKnowledge(downloadRequest); 932 if (result == MbmsErrors.UNKNOWN) { 933 // Unbind and throw an obvious error 934 close(); 935 throw new IllegalStateException("Middleware must not return an unknown error code"); 936 } 937 if (result != MbmsErrors.SUCCESS) { 938 if (result == MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST) { 939 throw new IllegalArgumentException("Unknown download request."); 940 } 941 sendErrorToApp(result, null); 942 } 943 } catch (RemoteException e) { 944 mService.set(null); 945 sIsInitialized.set(false); 946 sendErrorToApp(MbmsErrors.ERROR_MIDDLEWARE_LOST, null); 947 } 948 } 949 950 /** 951 * Terminates this instance. 952 * 953 * After this method returns, 954 * no further callbacks originating from the middleware will be enqueued on the provided 955 * instance of {@link MbmsDownloadSessionCallback}, but callbacks that have already been 956 * enqueued will still be delivered. 957 * 958 * It is safe to call {@link #create(Context, Executor, int, MbmsDownloadSessionCallback)} to 959 * obtain another instance of {@link MbmsDownloadSession} immediately after this method 960 * returns. 961 * 962 * May throw an {@link IllegalStateException} 963 */ 964 @Override 965 public void close() { 966 try { 967 IMbmsDownloadService downloadService = mService.get(); 968 if (downloadService == null) { 969 Log.i(LOG_TAG, "Service already dead"); 970 return; 971 } 972 downloadService.dispose(mSubscriptionId); 973 } catch (RemoteException e) { 974 // Ignore 975 Log.i(LOG_TAG, "Remote exception while disposing of service"); 976 } finally { 977 mService.set(null); 978 sIsInitialized.set(false); 979 mInternalCallback.stop(); 980 } 981 } 982 983 private void writeDownloadRequestToken(DownloadRequest request) { 984 File token = getDownloadRequestTokenPath(request); 985 if (!token.getParentFile().exists()) { 986 token.getParentFile().mkdirs(); 987 } 988 if (token.exists()) { 989 Log.w(LOG_TAG, "Download token " + token.getName() + " already exists"); 990 return; 991 } 992 try { 993 if (!token.createNewFile()) { 994 throw new RuntimeException("Failed to create download token for request " 995 + request + ". Token location is " + token.getPath()); 996 } 997 } catch (IOException e) { 998 throw new RuntimeException("Failed to create download token for request " + request 999 + " due to IOException " + e + ". Attempted to write to " + token.getPath()); 1000 } 1001 } 1002 1003 private void deleteDownloadRequestToken(DownloadRequest request) { 1004 File token = getDownloadRequestTokenPath(request); 1005 if (!token.isFile()) { 1006 Log.w(LOG_TAG, "Attempting to delete non-existent download token at " + token); 1007 return; 1008 } 1009 if (!token.delete()) { 1010 Log.w(LOG_TAG, "Couldn't delete download token at " + token); 1011 } 1012 } 1013 1014 private void checkDownloadRequestDestination(DownloadRequest request) { 1015 File downloadRequestDestination = new File(request.getDestinationUri().getPath()); 1016 if (!downloadRequestDestination.isDirectory()) { 1017 throw new IllegalArgumentException("The destination path must be a directory"); 1018 } 1019 // Check if the request destination is okay to use by attempting to rename an empty 1020 // file to there. 1021 File testFile = new File(MbmsTempFileProvider.getEmbmsTempFileDir(mContext), 1022 DESTINATION_SANITY_CHECK_FILE_NAME); 1023 File testFileDestination = new File(downloadRequestDestination, 1024 DESTINATION_SANITY_CHECK_FILE_NAME); 1025 1026 try { 1027 if (!testFile.exists()) { 1028 testFile.createNewFile(); 1029 } 1030 if (!testFile.renameTo(testFileDestination)) { 1031 throw new IllegalArgumentException("Destination provided in the download request " + 1032 "is invalid -- files in the temp file directory cannot be directly moved " + 1033 "there."); 1034 } 1035 } catch (IOException e) { 1036 throw new IllegalStateException("Got IOException while testing out the destination: " 1037 + e); 1038 } finally { 1039 testFile.delete(); 1040 testFileDestination.delete(); 1041 } 1042 } 1043 1044 private File getDownloadRequestTokenPath(DownloadRequest request) { 1045 File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext, 1046 request.getFileServiceId()); 1047 String downloadTokenFileName = request.getHash() 1048 + MbmsDownloadReceiver.DOWNLOAD_TOKEN_SUFFIX; 1049 return new File(tempFileLocation, downloadTokenFileName); 1050 } 1051 1052 private void sendErrorToApp(int errorCode, String message) { 1053 mInternalCallback.onError(errorCode, message); 1054 } 1055 } 1056