/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package android.telephony.mbms; import android.annotation.SystemApi; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.telephony.MbmsDownloadSession; import android.telephony.mbms.vendor.VendorUtils; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.UUID; /** * The {@link BroadcastReceiver} responsible for handling intents sent from the middleware. Apps * that wish to download using MBMS APIs should declare this class in their AndroidManifest.xml as * follows:
{@code

}
*/ public class MbmsDownloadReceiver extends BroadcastReceiver { /** @hide */ public static final String DOWNLOAD_TOKEN_SUFFIX = ".download_token"; /** @hide */ public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority"; private static final String EMBMS_INTENT_PERMISSION = "android.permission.SEND_EMBMS_INTENTS"; /** * Indicates that the requested operation completed without error. * @hide */ @SystemApi public static final int RESULT_OK = 0; /** * Indicates that the intent sent had an invalid action. This will be the result if * {@link Intent#getAction()} returns anything other than * {@link VendorUtils#ACTION_DOWNLOAD_RESULT_INTERNAL}, * {@link VendorUtils#ACTION_FILE_DESCRIPTOR_REQUEST}, or * {@link VendorUtils#ACTION_CLEANUP}. * This is a fatal result code and no result extras should be expected. * @hide */ @SystemApi public static final int RESULT_INVALID_ACTION = 1; /** * Indicates that the intent was missing some required extras. * This is a fatal result code and no result extras should be expected. * @hide */ @SystemApi public static final int RESULT_MALFORMED_INTENT = 2; /** * Indicates that the supplied value for {@link VendorUtils#EXTRA_TEMP_FILE_ROOT} * does not match what the app has stored. * This is a fatal result code and no result extras should be expected. * @hide */ @SystemApi public static final int RESULT_BAD_TEMP_FILE_ROOT = 3; /** * Indicates that the manager was unable to move the completed download to its final location. * This is a fatal result code and no result extras should be expected. * @hide */ @SystemApi public static final int RESULT_DOWNLOAD_FINALIZATION_ERROR = 4; /** * Indicates that the manager was unable to generate one or more of the requested file * descriptors. * This is a non-fatal result code -- some file descriptors may still be generated, but there * is no guarantee that they will be the same number as requested. * @hide */ @SystemApi public static final int RESULT_TEMP_FILE_GENERATION_ERROR = 5; /** * Indicates that the manager was unable to notify the app of the completed download. * This is a fatal result code and no result extras should be expected. * @hide */ @SystemApi public static final int RESULT_APP_NOTIFICATION_ERROR = 6; private static final String LOG_TAG = "MbmsDownloadReceiver"; private static final String TEMP_FILE_SUFFIX = ".embms.temp"; private static final String TEMP_FILE_STAGING_LOCATION = "staged_completed_files"; private static final int MAX_TEMP_FILE_RETRIES = 5; private String mFileProviderAuthorityCache = null; private String mMiddlewarePackageNameCache = null; /** @hide */ @Override public void onReceive(Context context, Intent intent) { verifyPermissionIntegrity(context); if (!verifyIntentContents(context, intent)) { setResultCode(RESULT_MALFORMED_INTENT); return; } if (!Objects.equals(intent.getStringExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT), MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath())) { setResultCode(RESULT_BAD_TEMP_FILE_ROOT); return; } if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { moveDownloadedFile(context, intent); cleanupPostMove(context, intent); } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { generateTempFiles(context, intent); } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) { cleanupTempFiles(context, intent); } else { setResultCode(RESULT_INVALID_ACTION); } } private boolean verifyIntentContents(Context context, Intent intent) { if (VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT)) { Log.w(LOG_TAG, "Download result did not include a result code. Ignoring."); return false; } if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST)) { Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring."); return false; } // We do not need to verify below extras if the result is not success. if (MbmsDownloadSession.RESULT_SUCCESSFUL != intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, MbmsDownloadSession.RESULT_CANCELLED)) { return true; } if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring."); return false; } if (!intent.hasExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO)) { Log.w(LOG_TAG, "Download result did not include the associated file info. " + "Ignoring."); return false; } if (!intent.hasExtra(VendorUtils.EXTRA_FINAL_URI)) { Log.w(LOG_TAG, "Download result did not include the path to the final " + "temp file. Ignoring."); return false; } DownloadRequest request = intent.getParcelableExtra( MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); String expectedTokenFileName = request.getHash() + DOWNLOAD_TOKEN_SUFFIX; File expectedTokenFile = new File( MbmsUtils.getEmbmsTempFileDirForService(context, request.getFileServiceId()), expectedTokenFileName); if (!expectedTokenFile.exists()) { Log.w(LOG_TAG, "Supplied download request does not match a token that we have. " + "Expected " + expectedTokenFile); return false; } } else if (VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) { Log.w(LOG_TAG, "Temp file request did not include the associated service id." + " Ignoring."); return false; } if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring."); return false; } } else if (VendorUtils.ACTION_CLEANUP.equals(intent.getAction())) { if (!intent.hasExtra(VendorUtils.EXTRA_SERVICE_ID)) { Log.w(LOG_TAG, "Cleanup request did not include the associated service id." + " Ignoring."); return false; } if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT)) { Log.w(LOG_TAG, "Cleanup request did not include the temp file root. Ignoring."); return false; } if (!intent.hasExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE)) { Log.w(LOG_TAG, "Cleanup request did not include the list of temp files in use. " + "Ignoring."); return false; } } return true; } private void moveDownloadedFile(Context context, Intent intent) { DownloadRequest request = intent.getParcelableExtra( MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); Intent intentForApp = request.getIntentForApp(); if (intentForApp == null) { Log.i(LOG_TAG, "Malformed app notification intent"); setResultCode(RESULT_APP_NOTIFICATION_ERROR); return; } int result = intent.getIntExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, MbmsDownloadSession.RESULT_CANCELLED); intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result); intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request); if (result != MbmsDownloadSession.RESULT_SUCCESSFUL) { Log.i(LOG_TAG, "Download request indicated a failed download. Aborting."); context.sendBroadcast(intentForApp); setResultCode(RESULT_OK); return; } Uri finalTempFile = intent.getParcelableExtra(VendorUtils.EXTRA_FINAL_URI); if (!verifyTempFilePath(context, request.getFileServiceId(), finalTempFile)) { Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile); setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR); return; } FileInfo completedFileInfo = (FileInfo) intent.getParcelableExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO); Path appSpecifiedDestination = FileSystems.getDefault().getPath( request.getDestinationUri().getPath()); Uri finalLocation; try { String relativeLocation = getFileRelativePath(request.getSourceUri().getPath(), completedFileInfo.getUri().getPath()); finalLocation = moveToFinalLocation(finalTempFile, appSpecifiedDestination, relativeLocation); } catch (IOException e) { Log.w(LOG_TAG, "Failed to move temp file to final destination"); setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR); return; } intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_COMPLETED_FILE_URI, finalLocation); intentForApp.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, completedFileInfo); context.sendBroadcast(intentForApp); setResultCode(RESULT_OK); } private void cleanupPostMove(Context context, Intent intent) { DownloadRequest request = intent.getParcelableExtra( MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST); if (request == null) { Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring."); return; } List tempFiles = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_LIST); if (tempFiles == null) { return; } for (Uri tempFileUri : tempFiles) { if (verifyTempFilePath(context, request.getFileServiceId(), tempFileUri)) { File tempFile = new File(tempFileUri.getSchemeSpecificPart()); if (!tempFile.delete()) { Log.w(LOG_TAG, "Failed to delete temp file at " + tempFile.getPath()); } } } } private void generateTempFiles(Context context, Intent intent) { String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID); if (serviceId == null) { Log.w(LOG_TAG, "Temp file request did not include the associated service id. " + "Ignoring."); setResultCode(RESULT_MALFORMED_INTENT); return; } int fdCount = intent.getIntExtra(VendorUtils.EXTRA_FD_COUNT, 0); List pausedList = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_PAUSED_LIST); if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) { Log.i(LOG_TAG, "No temp files actually requested. Ending."); setResultCode(RESULT_OK); setResultExtras(Bundle.EMPTY); return; } ArrayList freshTempFiles = generateFreshTempFiles(context, serviceId, fdCount); ArrayList pausedFiles = generateUrisForPausedFiles(context, serviceId, pausedList); Bundle result = new Bundle(); result.putParcelableArrayList(VendorUtils.EXTRA_FREE_URI_LIST, freshTempFiles); result.putParcelableArrayList(VendorUtils.EXTRA_PAUSED_URI_LIST, pausedFiles); setResultCode(RESULT_OK); setResultExtras(result); } private ArrayList generateFreshTempFiles(Context context, String serviceId, int freshFdCount) { File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId); if (!tempFileDir.exists()) { tempFileDir.mkdirs(); } // Name the files with the template "N-UUID", where N is the request ID and UUID is a // random uuid. ArrayList result = new ArrayList<>(freshFdCount); for (int i = 0; i < freshFdCount; i++) { File tempFile = generateSingleTempFile(tempFileDir); if (tempFile == null) { setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); Log.w(LOG_TAG, "Failed to generate a temp file. Moving on."); continue; } Uri fileUri = Uri.fromFile(tempFile); Uri contentUri = MbmsTempFileProvider.getUriForFile( context, getFileProviderAuthorityCached(context), tempFile); context.grantUriPermission(getMiddlewarePackageCached(context), contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); result.add(new UriPathPair(fileUri, contentUri)); } return result; } private static File generateSingleTempFile(File tempFileDir) { int numTries = 0; while (numTries < MAX_TEMP_FILE_RETRIES) { numTries++; String fileName = UUID.randomUUID() + TEMP_FILE_SUFFIX; File tempFile = new File(tempFileDir, fileName); try { if (tempFile.createNewFile()) { return tempFile.getCanonicalFile(); } } catch (IOException e) { continue; } } return null; } private ArrayList generateUrisForPausedFiles(Context context, String serviceId, List pausedFiles) { if (pausedFiles == null) { return new ArrayList<>(0); } ArrayList result = new ArrayList<>(pausedFiles.size()); for (Uri fileUri : pausedFiles) { if (!verifyTempFilePath(context, serviceId, fileUri)) { Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume"); setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); continue; } File tempFile = new File(fileUri.getSchemeSpecificPart()); if (!tempFile.exists()) { Log.w(LOG_TAG, "Supplied file " + fileUri + " does not exist."); setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); continue; } Uri contentUri = MbmsTempFileProvider.getUriForFile( context, getFileProviderAuthorityCached(context), tempFile); context.grantUriPermission(getMiddlewarePackageCached(context), contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); result.add(new UriPathPair(fileUri, contentUri)); } return result; } private void cleanupTempFiles(Context context, Intent intent) { String serviceId = intent.getStringExtra(VendorUtils.EXTRA_SERVICE_ID); File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceId); final List filesInUse = intent.getParcelableArrayListExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE); File[] filesToDelete = tempFileDir.listFiles(new FileFilter() { @Override public boolean accept(File file) { File canonicalFile; try { canonicalFile = file.getCanonicalFile(); } catch (IOException e) { Log.w(LOG_TAG, "Got IOException canonicalizing " + file + ", not deleting."); return false; } // Reject all files that don't match what we think a temp file should look like // e.g. download tokens if (!canonicalFile.getName().endsWith(TEMP_FILE_SUFFIX)) { return false; } // If any of the files in use match the uri, return false to reject it from the // list to delete. Uri fileInUseUri = Uri.fromFile(canonicalFile); return !filesInUse.contains(fileInUseUri); } }); for (File fileToDelete : filesToDelete) { fileToDelete.delete(); } } /* * Moves a tempfile located at fromPath to its final home where the app wants it */ private static Uri moveToFinalLocation(Uri fromPath, Path appSpecifiedPath, String relativeLocation) throws IOException { if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) { Log.w(LOG_TAG, "Downloaded file location uri " + fromPath + " does not have a file scheme"); return null; } Path fromFile = FileSystems.getDefault().getPath(fromPath.getPath()); Path toFile = appSpecifiedPath.resolve(relativeLocation); if (!Files.isDirectory(toFile.getParent())) { Files.createDirectories(toFile.getParent()); } Path result = Files.move(fromFile, toFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); return Uri.fromFile(result.toFile()); } /** * @hide */ @VisibleForTesting public static String getFileRelativePath(String sourceUriPath, String fileInfoPath) { if (sourceUriPath.endsWith("*")) { // This is a wildcard path. Strip the last path component and use that as the root of // the relative path. int lastSlash = sourceUriPath.lastIndexOf('/'); sourceUriPath = sourceUriPath.substring(0, lastSlash); } if (!fileInfoPath.startsWith(sourceUriPath)) { Log.e(LOG_TAG, "File location specified in FileInfo does not match the source URI." + " source: " + sourceUriPath + " fileinfo path: " + fileInfoPath); return null; } if (fileInfoPath.length() == sourceUriPath.length()) { // This is the single-file download case. Return the name of the file so that the // receiver puts the file directly into the dest directory. return sourceUriPath.substring(sourceUriPath.lastIndexOf('/') + 1); } String prefixOmittedPath = fileInfoPath.substring(sourceUriPath.length()); if (prefixOmittedPath.startsWith("/")) { prefixOmittedPath = prefixOmittedPath.substring(1); } return prefixOmittedPath; } private static boolean verifyTempFilePath(Context context, String serviceId, Uri filePath) { if (!ContentResolver.SCHEME_FILE.equals(filePath.getScheme())) { Log.w(LOG_TAG, "Uri " + filePath + " does not have a file scheme"); return false; } String path = filePath.getSchemeSpecificPart(); File tempFile = new File(path); if (!tempFile.exists()) { Log.w(LOG_TAG, "File at " + path + " does not exist."); return false; } if (!MbmsUtils.isContainedIn( MbmsUtils.getEmbmsTempFileDirForService(context, serviceId), tempFile)) { Log.w(LOG_TAG, "File at " + path + " is not contained in the temp file root," + " which is " + MbmsUtils.getEmbmsTempFileDirForService(context, serviceId)); return false; } return true; } private String getFileProviderAuthorityCached(Context context) { if (mFileProviderAuthorityCache != null) { return mFileProviderAuthorityCache; } mFileProviderAuthorityCache = getFileProviderAuthority(context); return mFileProviderAuthorityCache; } private static String getFileProviderAuthority(Context context) { ApplicationInfo appInfo; try { appInfo = context.getPackageManager() .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException("Package manager couldn't find " + context.getPackageName()); } if (appInfo.metaData == null) { throw new RuntimeException("App must declare the file provider authority as metadata " + "in the manifest."); } String authority = appInfo.metaData.getString(MBMS_FILE_PROVIDER_META_DATA_KEY); if (authority == null) { throw new RuntimeException("App must declare the file provider authority as metadata " + "in the manifest."); } return authority; } private String getMiddlewarePackageCached(Context context) { if (mMiddlewarePackageNameCache == null) { mMiddlewarePackageNameCache = MbmsUtils.getMiddlewareServiceInfo(context, MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION).packageName; } return mMiddlewarePackageNameCache; } private void verifyPermissionIntegrity(Context context) { PackageManager pm = context.getPackageManager(); Intent queryIntent = new Intent(context, MbmsDownloadReceiver.class); List infos = pm.queryBroadcastReceivers(queryIntent, 0); if (infos.size() != 1) { throw new IllegalStateException("Non-unique download receiver in your app"); } ActivityInfo selfInfo = infos.get(0).activityInfo; if (selfInfo == null) { throw new IllegalStateException("Queried ResolveInfo does not contain a receiver"); } if (MbmsUtils.getOverrideServiceName(context, MbmsDownloadSession.MBMS_DOWNLOAD_SERVICE_ACTION) != null) { // If an override was specified, just make sure that the permission isn't null. if (selfInfo.permission == null) { throw new IllegalStateException( "MbmsDownloadReceiver must require some permission"); } return; } if (!Objects.equals(EMBMS_INTENT_PERMISSION, selfInfo.permission)) { throw new IllegalStateException("MbmsDownloadReceiver must require the " + "SEND_EMBMS_INTENTS permission."); } } }