1 /* 2 * Copyright (C) 2017 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.mbms; 18 19 import android.annotation.SystemApi; 20 import android.content.BroadcastReceiver; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.telephony.MbmsDownloadSession; 29 import android.telephony.mbms.vendor.VendorUtils; 30 import android.util.Log; 31 32 import java.io.File; 33 import java.io.FileFilter; 34 import java.io.FileInputStream; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.io.OutputStream; 39 import java.nio.file.FileSystems; 40 import java.nio.file.Files; 41 import java.nio.file.Path; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Objects; 45 import java.util.UUID; 46 47 /** 48 * The {@link BroadcastReceiver} responsible for handling intents sent from the middleware. Apps 49 * that wish to download using MBMS APIs should declare this class in their AndroidManifest.xml as 50 * follows: 51 <pre>{@code 52 <receiver 53 android:name="android.telephony.mbms.MbmsDownloadReceiver" 54 android:permission="android.permission.SEND_EMBMS_INTENTS" 55 android:enabled="true" 56 android:exported="true"> 57 </receiver>}</pre> 58 * @hide 59 */ 60 public class MbmsDownloadReceiver extends BroadcastReceiver { 61 /** @hide */ 62 public static final String DOWNLOAD_TOKEN_SUFFIX = ".download_token"; 63 /** @hide */ 64 public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority"; 65 66 /** 67 * Indicates that the requested operation completed without error. 68 * @hide 69 */ 70 //@SystemApi 71 public static final int RESULT_OK = 0; 72 73 /** 74 * Indicates that the intent sent had an invalid action. This will be the result if 75 * {@link Intent#getAction()} returns anything other than 76 * {@link VendorUtils#ACTION_DOWNLOAD_RESULT_INTERNAL}, 77 * {@link VendorUtils#ACTION_FILE_DESCRIPTOR_REQUEST}, or 78 * {@link VendorUtils#ACTION_CLEANUP}. 79 * This is a fatal result code and no result extras should be expected. 80 * @hide 81 */ 82 //@SystemApi 83 public static final int RESULT_INVALID_ACTION = 1; 84 85 /** 86 * Indicates that the intent was missing some required extras. 87 * This is a fatal result code and no result extras should be expected. 88 * @hide 89 */ 90 //@SystemApi 91 public static final int RESULT_MALFORMED_INTENT = 2; 92 93 /** 94 * Indicates that the supplied value for {@link VendorUtils#EXTRA_TEMP_FILE_ROOT} 95 * does not match what the app has stored. 96 * This is a fatal result code and no result extras should be expected. 97 * @hide 98 */ 99 //@SystemApi 100 public static final int RESULT_BAD_TEMP_FILE_ROOT = 3; 101 102 /** 103 * Indicates that the manager was unable to move the completed download to its final location. 104 * This is a fatal result code and no result extras should be expected. 105 * @hide 106 */ 107 //@SystemApi 108 public static final int RESULT_DOWNLOAD_FINALIZATION_ERROR = 4; 109 110 /** 111 * Indicates that the manager was unable to generate one or more of the requested file 112 * descriptors. 113 * This is a non-fatal result code -- some file descriptors may still be generated, but there 114 * is no guarantee that they will be the same number as requested. 115 * @hide 116 */ 117 //@SystemApi 118 public static final int RESULT_TEMP_FILE_GENERATION_ERROR = 5; 119 120 /** 121 * Indicates that the manager was unable to notify the app of the completed download. 122 * This is a fatal result code and no result extras should be expected. 123 * @hide 124 */ 125 @SystemApi 126 public static final int RESULT_APP_NOTIFICATION_ERROR = 6; 127 128 129 private static final String LOG_TAG = "MbmsDownloadReceiver"; 130 private static final String TEMP_FILE_SUFFIX = ".embms.temp"; 131 private static final String TEMP_FILE_STAGING_LOCATION = "staged_completed_files"; 132 133 private static final int MAX_TEMP_FILE_RETRIES = 5; 134 135 private String mFileProviderAuthorityCache = null; 136 private String mMiddlewarePackageNameCache = null; 137 138 /** @hide */ 139 @Override 140 public void onReceive(Context context, Intent intent) { 141 if (!verifyIntentContents(context, intent)) { 142 setResultCode(RESULT_MALFORMED_INTENT); 143 return; 144 } 145 if (!Objects.equals(intent.getStringExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT), 146 MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath())) { 147 setResultCode(RESULT_BAD_TEMP_FILE_ROOT); 148 return; 149 } 150 151 if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { 152 moveDownloadedFile(context, intent); 153 cleanupPostMove(context, intent); 154 } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { 155 generateTempFiles(context, intent); 156 } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) { 157 cleanupTempFiles(context, intent); 158 } else { 159 setResultCode(RESULT_INVALID_ACTION); 160 } 161 } 162 163 private boolean verifyIntentContents(Context context, Intent intent) { 164 if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { 165 if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT)) { 166 Log.w(LOG_TAG, "Download result did not include a result code. Ignoring."); 167 return false; 168 } 169 // We do not need to verify below extras if the result is not success. 170 if (MbmsDownloadSession.RESULT_SUCCESSFUL != 171 intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, 172 MbmsDownloadSession.RESULT_CANCELLED)) { 173 return true; 174 } 175 if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) { 176 Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring."); 177 return false; 178 } 179 if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { 180 Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring."); 181 return false; 182 } 183 if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO)) { 184 Log.w(LOG_TAG, "Download result did not include the associated file info. " + 185 "Ignoring."); 186 return false; 187 } 188 if (!intent.hasExtra(VendorUtils.EXTRA_FINAL_URI)) { 189 Log.w(LOG_TAG, "Download result did not include the path to the final " + 190 "temp file. Ignoring."); 191 return false; 192 } 193 DownloadRequest request = intent.getParcelableExtra( 194 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); 195 String expectedTokenFileName = request.getHash() + DOWNLOAD_TOKEN_SUFFIX; 196 File expectedTokenFile = new File( 197 MbmsUtils.getEmbmsTempFileDirForService(context, request.getFileServiceId()), 198 expectedTokenFileName); 199 if (!expectedTokenFile.exists()) { 200 Log.w(LOG_TAG, "Supplied download request does not match a token that we have. " + 201 "Expected " + expectedTokenFile); 202 return false; 203 } 204 } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { 205 if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) { 206 Log.w(LOG_TAG, "Temp file request did not include the associated service id." + 207 " Ignoring."); 208 return false; 209 } 210 if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { 211 Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring."); 212 return false; 213 } 214 } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) { 215 if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) { 216 Log.w(LOG_TAG, "Cleanup request did not include the associated service id." + 217 " Ignoring."); 218 return false; 219 } 220 if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { 221 Log.w(LOG_TAG, "Cleanup request did not include the temp file root. Ignoring."); 222 return false; 223 } 224 if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE)) { 225 Log.w(LOG_TAG, "Cleanup request did not include the list of temp files in use. " + 226 "Ignoring."); 227 return false; 228 } 229 } 230 return true; 231 } 232 233 private void moveDownloadedFile(Context context, Intent intent) { 234 DownloadRequest request = intent.getParcelableExtra( 235 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); 236 Intent intentForApp = request.getIntentForApp(); 237 if (intentForApp == null) { 238 Log.i(LOG_TAG, "Malformed app notification intent"); 239 setResultCode(RESULT_APP_NOTIFICATION_ERROR); 240 return; 241 } 242 243 int result = intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, 244 MbmsDownloadSession.RESULT_CANCELLED); 245 intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result); 246 247 if (result != MbmsDownloadSession.RESULT_SUCCESSFUL) { 248 Log.i(LOG_TAG, "Download request indicated a failed download. Aborting."); 249 context.sendBroadcast(intentForApp); 250 return; 251 } 252 253 Uri finalTempFile = intent.getParcelableExtra(VendorUtils.EXTRA_FINAL_URI); 254 if (!verifyTempFilePath(context, request.getFileServiceId(), finalTempFile)) { 255 Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile); 256 setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR); 257 return; 258 } 259 260 FileInfo completedFileInfo = 261 (FileInfo) intent.getParcelableExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO); 262 Path stagingDirectory = FileSystems.getDefault().getPath( 263 MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath(), 264 TEMP_FILE_STAGING_LOCATION); 265 266 Uri stagedFileLocation; 267 try { 268 stagedFileLocation = stageTempFile(finalTempFile, stagingDirectory); 269 } catch (IOException e) { 270 Log.w(LOG_TAG, "Failed to move temp file to final destination"); 271 setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR); 272 return; 273 } 274 intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI, 275 stagedFileLocation); 276 intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, completedFileInfo); 277 intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request); 278 279 context.sendBroadcast(intentForApp); 280 setResultCode(RESULT_OK); 281 } 282 283 private void cleanupPostMove(Context context, Intent intent) { 284 DownloadRequest request = intent.getParcelableExtra( 285 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); 286 if (request == null) { 287 Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring."); 288 return; 289 } 290 291 List<Uri> tempFiles = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_LIST); 292 if (tempFiles == null) { 293 return; 294 } 295 296 for (Uri tempFileUri : tempFiles) { 297 if (verifyTempFilePath(context, request.getFileServiceId(), tempFileUri)) { 298 File tempFile = new File(tempFileUri.getSchemeSpecificPart()); 299 tempFile.delete(); 300 } 301 } 302 } 303 304 private void generateTempFiles(Context context, Intent intent) { 305 String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID); 306 if (serviceId == null) { 307 Log.w(LOG_TAG, "Temp file request did not include the associated service id. " + 308 "Ignoring."); 309 setResultCode(RESULT_MALFORMED_INTENT); 310 return; 311 } 312 int fdCount = intent.getIntExtra(VendorUtils.EXTRA_FD_COUNT, 0); 313 List<Uri> pausedList = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_PAUSED_LIST); 314 315 if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) { 316 Log.i(LOG_TAG, "No temp files actually requested. Ending."); 317 setResultCode(RESULT_OK); 318 setResultExtras(Bundle.EMPTY); 319 return; 320 } 321 322 ArrayList<UriPathPair> freshTempFiles = 323 generateFreshTempFiles(context, serviceId, fdCount); 324 ArrayList<UriPathPair> pausedFiles = 325 generateUrisForPausedFiles(context, serviceId, pausedList); 326 327 Bundle result = new Bundle(); 328 result.putParcelableArrayList(VendorUtils.EXTRA_FREE_URI_LIST, freshTempFiles); 329 result.putParcelableArrayList(VendorUtils.EXTRA_PAUSED_URI_LIST, pausedFiles); 330 setResultCode(RESULT_OK); 331 setResultExtras(result); 332 } 333 334 private ArrayList<UriPathPair> generateFreshTempFiles(Context context, String serviceId, 335 int freshFdCount) { 336 File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId); 337 if (!tempFileDir.exists()) { 338 tempFileDir.mkdirs(); 339 } 340 341 // Name the files with the template "N-UUID", where N is the request ID and UUID is a 342 // random uuid. 343 ArrayList<UriPathPair> result = new ArrayList<>(freshFdCount); 344 for (int i = 0; i < freshFdCount; i++) { 345 File tempFile = generateSingleTempFile(tempFileDir); 346 if (tempFile == null) { 347 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); 348 Log.w(LOG_TAG, "Failed to generate a temp file. Moving on."); 349 continue; 350 } 351 Uri fileUri = Uri.fromFile(tempFile); 352 Uri contentUri = MbmsTempFileProvider.getUriForFile( 353 context, getFileProviderAuthorityCached(context), tempFile); 354 context.grantUriPermission(getMiddlewarePackageCached(context), contentUri, 355 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 356 result.add(new UriPathPair(fileUri, contentUri)); 357 } 358 359 return result; 360 } 361 362 private static File generateSingleTempFile(File tempFileDir) { 363 int numTries = 0; 364 while (numTries < MAX_TEMP_FILE_RETRIES) { 365 numTries++; 366 String fileName = UUID.randomUUID() + TEMP_FILE_SUFFIX; 367 File tempFile = new File(tempFileDir, fileName); 368 try { 369 if (tempFile.createNewFile()) { 370 return tempFile.getCanonicalFile(); 371 } 372 } catch (IOException e) { 373 continue; 374 } 375 } 376 return null; 377 } 378 379 private ArrayList<UriPathPair> generateUrisForPausedFiles(Context context, 380 String serviceId, List<Uri> pausedFiles) { 381 if (pausedFiles == null) { 382 return new ArrayList<>(0); 383 } 384 ArrayList<UriPathPair> result = new ArrayList<>(pausedFiles.size()); 385 386 for (Uri fileUri : pausedFiles) { 387 if (!verifyTempFilePath(context, serviceId, fileUri)) { 388 Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume"); 389 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); 390 continue; 391 } 392 File tempFile = new File(fileUri.getSchemeSpecificPart()); 393 if (!tempFile.exists()) { 394 Log.w(LOG_TAG, "Supplied file " + fileUri + " does not exist."); 395 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); 396 continue; 397 } 398 Uri contentUri = MbmsTempFileProvider.getUriForFile( 399 context, getFileProviderAuthorityCached(context), tempFile); 400 context.grantUriPermission(getMiddlewarePackageCached(context), contentUri, 401 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 402 403 result.add(new UriPathPair(fileUri, contentUri)); 404 } 405 return result; 406 } 407 408 private void cleanupTempFiles(Context context, Intent intent) { 409 String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID); 410 File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId); 411 final List<Uri> filesInUse = 412 intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE); 413 File[] filesToDelete = tempFileDir.listFiles(new FileFilter() { 414 @Override 415 public boolean accept(File file) { 416 File canonicalFile; 417 try { 418 canonicalFile = file.getCanonicalFile(); 419 } catch (IOException e) { 420 Log.w(LOG_TAG, "Got IOException canonicalizing " + file + ", not deleting."); 421 return false; 422 } 423 // Reject all files that don't match what we think a temp file should look like 424 // e.g. download tokens 425 if (!canonicalFile.getName().endsWith(TEMP_FILE_SUFFIX)) { 426 return false; 427 } 428 // If any of the files in use match the uri, return false to reject it from the 429 // list to delete. 430 Uri fileInUseUri = Uri.fromFile(canonicalFile); 431 return !filesInUse.contains(fileInUseUri); 432 } 433 }); 434 for (File fileToDelete : filesToDelete) { 435 fileToDelete.delete(); 436 } 437 } 438 439 /* 440 * Moves a tempfile located at fromPath to a new location in the staging directory. 441 */ 442 private static Uri stageTempFile(Uri fromPath, Path stagingDirectory) throws IOException { 443 if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) { 444 Log.w(LOG_TAG, "Moving source uri " + fromPath+ " does not have a file scheme"); 445 return null; 446 } 447 448 Path fromFile = FileSystems.getDefault().getPath(fromPath.getPath()); 449 if (!Files.isDirectory(stagingDirectory)) { 450 Files.createDirectory(stagingDirectory); 451 } 452 Path result = Files.move(fromFile, stagingDirectory.resolve(fromFile.getFileName())); 453 454 return Uri.fromFile(result.toFile()); 455 } 456 457 private static boolean verifyTempFilePath(Context context, String serviceId, 458 Uri filePath) { 459 if (!ContentResolver.SCHEME_FILE.equals(filePath.getScheme())) { 460 Log.w(LOG_TAG, "Uri " + filePath + " does not have a file scheme"); 461 return false; 462 } 463 464 String path = filePath.getSchemeSpecificPart(); 465 File tempFile = new File(path); 466 if (!tempFile.exists()) { 467 Log.w(LOG_TAG, "File at " + path + " does not exist."); 468 return false; 469 } 470 471 if (!MbmsUtils.isContainedIn( 472 MbmsUtils.getEmbmsTempFileDirForService(context, serviceId), tempFile)) { 473 return false; 474 } 475 476 return true; 477 } 478 479 private String getFileProviderAuthorityCached(Context context) { 480 if (mFileProviderAuthorityCache != null) { 481 return mFileProviderAuthorityCache; 482 } 483 484 mFileProviderAuthorityCache = getFileProviderAuthority(context); 485 return mFileProviderAuthorityCache; 486 } 487 488 private static String getFileProviderAuthority(Context context) { 489 ApplicationInfo appInfo; 490 try { 491 appInfo = context.getPackageManager() 492 .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); 493 } catch (PackageManager.NameNotFoundException e) { 494 throw new RuntimeException("Package manager couldn't find " + context.getPackageName()); 495 } 496 if (appInfo.metaData == null) { 497 throw new RuntimeException("App must declare the file provider authority as metadata " + 498 "in the manifest."); 499 } 500 String authority = appInfo.metaData.getString(MBMS_FILE_PROVIDER_META_DATA_KEY); 501 if (authority == null) { 502 throw new RuntimeException("App must declare the file provider authority as metadata " + 503 "in the manifest."); 504 } 505 return authority; 506 } 507 508 private String getMiddlewarePackageCached(Context context) { 509 if (mMiddlewarePackageNameCache == null) { 510 mMiddlewarePackageNameCache = MbmsUtils.getMiddlewareServiceInfo(context, 511 MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION).packageName; 512 } 513 return mMiddlewarePackageNameCache; 514 } 515 516 private static boolean manualMove(File src, File dst) { 517 InputStream in = null; 518 OutputStream out = null; 519 try { 520 if (!dst.exists()) { 521 dst.createNewFile(); 522 } 523 in = new FileInputStream(src); 524 out = new FileOutputStream(dst); 525 byte[] buffer = new byte[2048]; 526 int len; 527 do { 528 len = in.read(buffer); 529 out.write(buffer, 0, len); 530 } while (len > 0); 531 } catch (IOException e) { 532 Log.w(LOG_TAG, "Manual file move failed due to exception " + e); 533 if (dst.exists()) { 534 dst.delete(); 535 } 536 return false; 537 } finally { 538 try { 539 if (in != null) { 540 in.close(); 541 } 542 if (out != null) { 543 out.close(); 544 } 545 } catch (IOException e) { 546 Log.w(LOG_TAG, "Error closing streams: " + e); 547 } 548 } 549 return true; 550 } 551 } 552