2 * Copyright (C) 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package android.telephony.mbms;
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.ActivityInfo;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ResolveInfo;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.telephony.MbmsDownloadSession;
31 import android.telephony.mbms.vendor.VendorUtils;
32 import android.util.Log;
35 import java.io.FileFilter;
36 import java.io.IOException;
37 import java.nio.file.FileSystems;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.nio.file.StandardCopyOption;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Objects;
44 import java.util.UUID;
47 * The {@link BroadcastReceiver} responsible for handling intents sent from the middleware. Apps
48 * that wish to download using MBMS APIs should declare this class in their AndroidManifest.xml as
52 android:name="android.telephony.mbms.MbmsDownloadReceiver"
53 android:permission="android.permission.SEND_EMBMS_INTENTS"
54 android:enabled="true"
55 android:exported="true">
58 public class MbmsDownloadReceiver extends BroadcastReceiver {
60 public static final String DOWNLOAD_TOKEN_SUFFIX = ".download_token";
62 public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority";
64 private static final String EMBMS_INTENT_PERMISSION = "android.permission.SEND_EMBMS_INTENTS";
67 * Indicates that the requested operation completed without error.
71 public static final int RESULT_OK = 0;
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.
83 public static final int RESULT_INVALID_ACTION = 1;
86 * Indicates that the intent was missing some required extras.
87 * This is a fatal result code and no result extras should be expected.
91 public static final int RESULT_MALFORMED_INTENT = 2;
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.
100 public static final int RESULT_BAD_TEMP_FILE_ROOT = 3;
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.
108 public static final int RESULT_DOWNLOAD_FINALIZATION_ERROR = 4;
111 * Indicates that the manager was unable to generate one or more of the requested file
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.
118 public static final int RESULT_TEMP_FILE_GENERATION_ERROR = 5;
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.
126 public static final int RESULT_APP_NOTIFICATION_ERROR = 6;
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";
133 private static final int MAX_TEMP_FILE_RETRIES = 5;
135 private String mFileProviderAuthorityCache = null;
136 private String mMiddlewarePackageNameCache = null;
140 public void onReceive(Context context, Intent intent) {
141 verifyPermissionIntegrity(context);
143 if (!verifyIntentContents(context, intent)) {
144 setResultCode(RESULT_MALFORMED_INTENT);
147 if (!Objects.equals(intent.getStringExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT),
148 MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath())) {
149 setResultCode(RESULT_BAD_TEMP_FILE_ROOT);
153 if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) {
154 moveDownloadedFile(context, intent);
155 cleanupPostMove(context, intent);
156 } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) {
157 generateTempFiles(context, intent);
158 } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) {
159 cleanupTempFiles(context, intent);
161 setResultCode(RESULT_INVALID_ACTION);
165 private boolean verifyIntentContents(Context context, Intent intent) {
166 if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) {
167 if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT)) {
168 Log.w(LOG_TAG, "Download result did not include a result code. Ignoring.");
171 if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) {
172 Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring.");
175 // We do not need to verify below extras if the result is not success.
176 if (MbmsDownloadSession.RESULT_SUCCESSFUL !=
177 intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT,
178 MbmsDownloadSession.RESULT_CANCELLED)) {
181 if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
182 Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring.");
185 if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO)) {
186 Log.w(LOG_TAG, "Download result did not include the associated file info. " +
190 if (!intent.hasExtra(VendorUtils.EXTRA_FINAL_URI)) {
191 Log.w(LOG_TAG, "Download result did not include the path to the final " +
192 "temp file. Ignoring.");
195 DownloadRequest request = intent.getParcelableExtra(
196 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
197 String expectedTokenFileName = request.getHash() + DOWNLOAD_TOKEN_SUFFIX;
198 File expectedTokenFile = new File(
199 MbmsUtils.getEmbmsTempFileDirForService(context, request.getFileServiceId()),
200 expectedTokenFileName);
201 if (!expectedTokenFile.exists()) {
202 Log.w(LOG_TAG, "Supplied download request does not match a token that we have. " +
203 "Expected " + expectedTokenFile);
206 } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) {
207 if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) {
208 Log.w(LOG_TAG, "Temp file request did not include the associated service id." +
212 if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
213 Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring.");
216 } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) {
217 if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) {
218 Log.w(LOG_TAG, "Cleanup request did not include the associated service id." +
222 if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) {
223 Log.w(LOG_TAG, "Cleanup request did not include the temp file root. Ignoring.");
226 if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE)) {
227 Log.w(LOG_TAG, "Cleanup request did not include the list of temp files in use. " +
235 private void moveDownloadedFile(Context context, Intent intent) {
236 DownloadRequest request = intent.getParcelableExtra(
237 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
238 Intent intentForApp = request.getIntentForApp();
239 if (intentForApp == null) {
240 Log.i(LOG_TAG, "Malformed app notification intent");
241 setResultCode(RESULT_APP_NOTIFICATION_ERROR);
245 int result = intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT,
246 MbmsDownloadSession.RESULT_CANCELLED);
247 intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result);
248 intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request);
250 if (result != MbmsDownloadSession.RESULT_SUCCESSFUL) {
251 Log.i(LOG_TAG, "Download request indicated a failed download. Aborting.");
252 context.sendBroadcast(intentForApp);
253 setResultCode(RESULT_OK);
257 Uri finalTempFile = intent.getParcelableExtra(VendorUtils.EXTRA_FINAL_URI);
258 if (!verifyTempFilePath(context, request.getFileServiceId(), finalTempFile)) {
259 Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile);
260 setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR);
264 FileInfo completedFileInfo =
265 (FileInfo) intent.getParcelableExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO);
266 Path appSpecifiedDestination = FileSystems.getDefault().getPath(
267 request.getDestinationUri().getPath());
271 finalLocation = moveToFinalLocation(finalTempFile, appSpecifiedDestination);
272 } catch (IOException e) {
273 Log.w(LOG_TAG, "Failed to move temp file to final destination");
274 setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR);
277 intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI, finalLocation);
278 intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, completedFileInfo);
280 context.sendBroadcast(intentForApp);
281 setResultCode(RESULT_OK);
284 private void cleanupPostMove(Context context, Intent intent) {
285 DownloadRequest request = intent.getParcelableExtra(
286 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST);
287 if (request == null) {
288 Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring.");
292 List<Uri> tempFiles = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_LIST);
293 if (tempFiles == null) {
297 for (Uri tempFileUri : tempFiles) {
298 if (verifyTempFilePath(context, request.getFileServiceId(), tempFileUri)) {
299 File tempFile = new File(tempFileUri.getSchemeSpecificPart());
305 private void generateTempFiles(Context context, Intent intent) {
306 String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID);
307 if (serviceId == null) {
308 Log.w(LOG_TAG, "Temp file request did not include the associated service id. " +
310 setResultCode(RESULT_MALFORMED_INTENT);
313 int fdCount = intent.getIntExtra(VendorUtils.EXTRA_FD_COUNT, 0);
314 List<Uri> pausedList = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_PAUSED_LIST);
316 if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) {
317 Log.i(LOG_TAG, "No temp files actually requested. Ending.");
318 setResultCode(RESULT_OK);
319 setResultExtras(Bundle.EMPTY);
323 ArrayList<UriPathPair> freshTempFiles =
324 generateFreshTempFiles(context, serviceId, fdCount);
325 ArrayList<UriPathPair> pausedFiles =
326 generateUrisForPausedFiles(context, serviceId, pausedList);
328 Bundle result = new Bundle();
329 result.putParcelableArrayList(VendorUtils.EXTRA_FREE_URI_LIST, freshTempFiles);
330 result.putParcelableArrayList(VendorUtils.EXTRA_PAUSED_URI_LIST, pausedFiles);
331 setResultCode(RESULT_OK);
332 setResultExtras(result);
335 private ArrayList<UriPathPair> generateFreshTempFiles(Context context, String serviceId,
337 File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId);
338 if (!tempFileDir.exists()) {
339 tempFileDir.mkdirs();
342 // Name the files with the template "N-UUID", where N is the request ID and UUID is a
344 ArrayList<UriPathPair> result = new ArrayList<>(freshFdCount);
345 for (int i = 0; i < freshFdCount; i++) {
346 File tempFile = generateSingleTempFile(tempFileDir);
347 if (tempFile == null) {
348 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
349 Log.w(LOG_TAG, "Failed to generate a temp file. Moving on.");
352 Uri fileUri = Uri.fromFile(tempFile);
353 Uri contentUri = MbmsTempFileProvider.getUriForFile(
354 context, getFileProviderAuthorityCached(context), tempFile);
355 context.grantUriPermission(getMiddlewarePackageCached(context), contentUri,
356 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
357 result.add(new UriPathPair(fileUri, contentUri));
363 private static File generateSingleTempFile(File tempFileDir) {
365 while (numTries < MAX_TEMP_FILE_RETRIES) {
367 String fileName = UUID.randomUUID() + TEMP_FILE_SUFFIX;
368 File tempFile = new File(tempFileDir, fileName);
370 if (tempFile.createNewFile()) {
371 return tempFile.getCanonicalFile();
373 } catch (IOException e) {
380 private ArrayList<UriPathPair> generateUrisForPausedFiles(Context context,
381 String serviceId, List<Uri> pausedFiles) {
382 if (pausedFiles == null) {
383 return new ArrayList<>(0);
385 ArrayList<UriPathPair> result = new ArrayList<>(pausedFiles.size());
387 for (Uri fileUri : pausedFiles) {
388 if (!verifyTempFilePath(context, serviceId, fileUri)) {
389 Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume");
390 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
393 File tempFile = new File(fileUri.getSchemeSpecificPart());
394 if (!tempFile.exists()) {
395 Log.w(LOG_TAG, "Supplied file " + fileUri + " does not exist.");
396 setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR);
399 Uri contentUri = MbmsTempFileProvider.getUriForFile(
400 context, getFileProviderAuthorityCached(context), tempFile);
401 context.grantUriPermission(getMiddlewarePackageCached(context), contentUri,
402 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
404 result.add(new UriPathPair(fileUri, contentUri));
409 private void cleanupTempFiles(Context context, Intent intent) {
410 String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID);
411 File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId);
412 final List<Uri> filesInUse =
413 intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE);
414 File[] filesToDelete = tempFileDir.listFiles(new FileFilter() {
416 public boolean accept(File file) {
419 canonicalFile = file.getCanonicalFile();
420 } catch (IOException e) {
421 Log.w(LOG_TAG, "Got IOException canonicalizing " + file + ", not deleting.");
424 // Reject all files that don't match what we think a temp file should look like
425 // e.g. download tokens
426 if (!canonicalFile.getName().endsWith(TEMP_FILE_SUFFIX)) {
429 // If any of the files in use match the uri, return false to reject it from the
431 Uri fileInUseUri = Uri.fromFile(canonicalFile);
432 return !filesInUse.contains(fileInUseUri);
435 for (File fileToDelete : filesToDelete) {
436 fileToDelete.delete();
441 * Moves a tempfile located at fromPath to its final home where the app wants it
443 private static Uri moveToFinalLocation(Uri fromPath, Path appSpecifiedPath) throws IOException {
444 if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) {
445 Log.w(LOG_TAG, "Downloaded file location uri " + fromPath +
446 " does not have a file scheme");
450 Path fromFile = FileSystems.getDefault().getPath(fromPath.getPath());
451 if (!Files.isDirectory(appSpecifiedPath)) {
452 Files.createDirectory(appSpecifiedPath);
454 // TODO: do we want to support directory trees within the download directory?
455 Path result = Files.move(fromFile, appSpecifiedPath.resolve(fromFile.getFileName()),
456 StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
458 return Uri.fromFile(result.toFile());
461 private static boolean verifyTempFilePath(Context context, String serviceId,
463 if (!ContentResolver.SCHEME_FILE.equals(filePath.getScheme())) {
464 Log.w(LOG_TAG, "Uri " + filePath + " does not have a file scheme");
468 String path = filePath.getSchemeSpecificPart();
469 File tempFile = new File(path);
470 if (!tempFile.exists()) {
471 Log.w(LOG_TAG, "File at " + path + " does not exist.");
475 if (!MbmsUtils.isContainedIn(
476 MbmsUtils.getEmbmsTempFileDirForService(context, serviceId), tempFile)) {
483 private String getFileProviderAuthorityCached(Context context) {
484 if (mFileProviderAuthorityCache != null) {
485 return mFileProviderAuthorityCache;
488 mFileProviderAuthorityCache = getFileProviderAuthority(context);
489 return mFileProviderAuthorityCache;
492 private static String getFileProviderAuthority(Context context) {
493 ApplicationInfo appInfo;
495 appInfo = context.getPackageManager()
496 .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
497 } catch (PackageManager.NameNotFoundException e) {
498 throw new RuntimeException("Package manager couldn't find " + context.getPackageName());
500 if (appInfo.metaData == null) {
501 throw new RuntimeException("App must declare the file provider authority as metadata " +
504 String authority = appInfo.metaData.getString(MBMS_FILE_PROVIDER_META_DATA_KEY);
505 if (authority == null) {
506 throw new RuntimeException("App must declare the file provider authority as metadata " +
512 private String getMiddlewarePackageCached(Context context) {
513 if (mMiddlewarePackageNameCache == null) {
514 mMiddlewarePackageNameCache = MbmsUtils.getMiddlewareServiceInfo(context,
515 MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION).packageName;
517 return mMiddlewarePackageNameCache;
520 private void verifyPermissionIntegrity(Context context) {
521 PackageManager pm = context.getPackageManager();
522 Intent queryIntent = new Intent(context, MbmsDownloadReceiver.class);
523 List<ResolveInfo> infos = pm.queryBroadcastReceivers(queryIntent, 0);
524 if (infos.size() != 1) {
525 throw new IllegalStateException("Non-unique download receiver in your app");
527 ActivityInfo selfInfo = infos.get(0).activityInfo;
528 if (selfInfo == null) {
529 throw new IllegalStateException("Queried ResolveInfo does not contain a receiver");
531 if (MbmsUtils.getOverrideServiceName(context,
532 MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION) != null) {
533 // If an override was specified, just make sure that the permission isn't null.
534 if (selfInfo.permission == null) {
535 throw new IllegalStateException(
536 "MbmsDownloadReceiver must require some permission");
540 if (!Objects.equals(EMBMS_INTENT_PERMISSION, selfInfo.permission)) {
541 throw new IllegalStateException("MbmsDownloadReceiver must require the " +
542 "SEND_EMBMS_INTENTS permission.");