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