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