package com.android.providers.downloads;
-import android.content.ContentUris;
+import static com.android.providers.downloads.Constants.TAG;
+
import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.database.Cursor;
-import android.drm.mobile1.DrmRawContent;
import android.net.Uri;
import android.os.Environment;
-import android.os.StatFs;
import android.os.SystemClock;
import android.provider.Downloads;
-import android.util.Config;
import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
+import java.io.IOException;
import java.util.Random;
import java.util.Set;
import java.util.regex.Matcher;
* Some helper functions for the download manager
*/
public class Helpers {
-
public static Random sRandom = new Random(SystemClock.uptimeMillis());
/** Regex used to parse content-disposition headers */
private static final Pattern CONTENT_DISPOSITION_PATTERN =
Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+ private static final Object sUniqueLock = new Object();
+
private Helpers() {
}
}
/**
- * Exception thrown from methods called by generateSaveFile() for any fatal error.
- */
- private static class GenerateSaveFileError extends Exception {
- int mStatus;
-
- public GenerateSaveFileError(int status) {
- mStatus = status;
- }
- }
-
- /**
- * Creates a filename (where the file should be saved) from a uri.
+ * Creates a filename (where the file should be saved) from info about a download.
*/
- public static DownloadFileInfo generateSaveFile(
+ static String generateSaveFile(
Context context,
String url,
String hint,
String mimeType,
int destination,
long contentLength,
- boolean isPublicApi) throws FileNotFoundException {
-
- if (!canHandleDownload(context, mimeType, destination, isPublicApi)) {
- return new DownloadFileInfo(null, null, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
+ StorageManager storageManager) throws StopRequestException {
+ if (contentLength < 0) {
+ contentLength = 0;
}
-
- String fullFilename;
- try {
- if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
- fullFilename = getPathForFileUri(hint);
- } else {
- fullFilename = chooseFullPath(context, url, hint, contentDisposition,
- contentLocation, mimeType, destination,
- contentLength);
- }
- } catch (GenerateSaveFileError exc) {
- return new DownloadFileInfo(null, null, exc.mStatus);
+ String path;
+ File base = null;
+ if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
+ path = Uri.parse(hint).getPath();
+ } else {
+ base = storageManager.locateDestinationDirectory(mimeType, destination,
+ contentLength);
+ path = chooseFilename(url, hint, contentDisposition, contentLocation,
+ destination);
}
-
- return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
- }
-
- private static String getPathForFileUri(String hint) throws GenerateSaveFileError {
- String path = Uri.parse(hint).getSchemeSpecificPart();
- if (new File(path).exists()) {
- Log.d(Constants.TAG, "File already exists: " + path);
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
+ storageManager.verifySpace(destination, path, contentLength);
+ if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
+ path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
}
-
+ path = getFullPath(path, mimeType, destination, base);
return path;
}
- private static String chooseFullPath(Context context, String url, String hint,
- String contentDisposition, String contentLocation,
- String mimeType, int destination, long contentLength)
- throws GenerateSaveFileError {
- File base = locateDestinationDirectory(context, mimeType, destination, contentLength);
- String filename = chooseFilename(url, hint, contentDisposition, contentLocation,
- destination);
-
- // Split filename between base and extension
- // Add an extension if filename does not have one
+ static String getFullPath(String filename, String mimeType, int destination, File base)
+ throws StopRequestException {
String extension = null;
- int dotIndex = filename.indexOf('.');
- if (dotIndex < 0) {
- extension = chooseExtensionFromMimeType(mimeType, true);
+ int dotIndex = filename.lastIndexOf('.');
+ boolean missingExtension = dotIndex < 0 || dotIndex < filename.lastIndexOf('/');
+ if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
+ // Destination is explicitly set - do not change the extension
+ if (missingExtension) {
+ extension = "";
+ } else {
+ extension = filename.substring(dotIndex);
+ filename = filename.substring(0, dotIndex);
+ }
} else {
- extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
- filename = filename.substring(0, dotIndex);
+ // Split filename between base and extension
+ // Add an extension if filename does not have one
+ if (missingExtension) {
+ extension = chooseExtensionFromMimeType(mimeType, true);
+ } else {
+ extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
+ filename = filename.substring(0, dotIndex);
+ }
}
boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
- filename = base.getPath() + File.separator + filename;
+ if (base != null) {
+ filename = base.getPath() + File.separator + filename;
+ }
if (Constants.LOGVV) {
Log.v(Constants.TAG, "target file: " + filename + extension);
}
- return chooseUniqueFilename(destination, filename, extension, recoveryDir);
- }
-
- private static boolean canHandleDownload(Context context, String mimeType, int destination,
- boolean isPublicApi) {
- if (isPublicApi) {
- return true;
- }
-
- if (destination == Downloads.Impl.DESTINATION_EXTERNAL
- || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
- if (mimeType == null) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "external download with no mime type not allowed");
- }
- return false;
- }
- if (!DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
- // Check to see if we are allowed to download this file. Only files
- // that can be handled by the platform can be downloaded.
- // special case DRM files, which we should always allow downloading.
- Intent intent = new Intent(Intent.ACTION_VIEW);
-
- // We can provide data as either content: or file: URIs,
- // so allow both. (I think it would be nice if we just did
- // everything as content: URIs)
- // Actually, right now the download manager's UId restrictions
- // prevent use from using content: so it's got to be file: or
- // nothing
-
- PackageManager pm = context.getPackageManager();
- intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
- ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
- //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list);
-
- if (ri == null) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "no handler found for type " + mimeType);
- }
- return false;
- }
- }
- }
- return true;
- }
-
- private static File locateDestinationDirectory(Context context, String mimeType,
- int destination, long contentLength)
- throws GenerateSaveFileError {
- File base = null;
- StatFs stat = null;
- // DRM messages should be temporarily stored internally and then passed to
- // the DRM content provider
- if (destination == Downloads.Impl.DESTINATION_CACHE_PARTITION
- || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
- || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
- || DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
- // Saving to internal storage.
- base = Environment.getDownloadCacheDirectory();
- stat = new StatFs(base.getPath());
-
- /*
- * Check whether there's enough space on the target filesystem to save the file.
- * Put a bit of margin (in case creating the file grows the system by a few blocks).
- */
- int blockSize = stat.getBlockSize();
- long bytesAvailable = blockSize * ((long) stat.getAvailableBlocks() - 4);
- while (bytesAvailable < contentLength) {
- // Insufficient space; try discarding purgeable files.
- if (!discardPurgeableFiles(context, contentLength - bytesAvailable)) {
- // No files to purge, give up.
- if (Config.LOGD) {
- Log.d(Constants.TAG,
- "download aborted - not enough free space in internal storage");
- }
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
- } else {
- // Recalculate available space and try again.
- stat.restat(base.getPath());
- bytesAvailable = blockSize * ((long) stat.getAvailableBlocks() - 4);
- }
- }
- } else if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- // Saving to external storage (SD card).
- String root = Environment.getExternalStorageDirectory().getPath();
- stat = new StatFs(root);
-
- /*
- * Check whether there's enough space on the target filesystem to save the file.
- * Put a bit of margin (in case creating the file grows the system by a few blocks).
- */
- if (stat.getBlockSize() * ((long) stat.getAvailableBlocks() - 4) < contentLength) {
- // Insufficient space.
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - not enough free space");
- }
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
- }
+ synchronized (sUniqueLock) {
+ final String path = chooseUniqueFilenameLocked(
+ destination, filename, extension, recoveryDir);
- base = new File(root + Constants.DEFAULT_DL_SUBDIR);
- if (!base.isDirectory() && !base.mkdir()) {
- // Can't create download directory, e.g. because a file called "download"
- // already exists at the root level, or the SD card filesystem is read-only.
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - can't create base directory "
- + base.getPath());
- }
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
- }
- } else {
- // No SD card found.
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - no external storage");
+ // Claim this filename inside lock to prevent other threads from
+ // clobbering us. We're not paranoid enough to use O_EXCL.
+ try {
+ new File(path).createNewFile();
+ } catch (IOException e) {
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
+ "Failed to create target file " + path, e);
}
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
+ return path;
}
-
- return base;
}
private static String chooseFilename(String url, String hint, String contentDisposition,
}
private static String chooseExtensionFromFilename(String mimeType, int destination,
- String filename, int dotIndex) {
+ String filename, int lastDotIndex) {
String extension = null;
if (mimeType != null) {
// Compare the last segment of the extension against the mime type.
// If there's a mismatch, discard the entire extension.
- int lastDotIndex = filename.lastIndexOf('.');
String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
filename.substring(lastDotIndex + 1));
if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "keeping extension");
}
- extension = filename.substring(dotIndex);
+ extension = filename.substring(lastDotIndex);
}
return extension;
}
- private static String chooseUniqueFilename(int destination, String filename,
- String extension, boolean recoveryDir) throws GenerateSaveFileError {
+ private static String chooseUniqueFilenameLocked(int destination, String filename,
+ String extension, boolean recoveryDir) throws StopRequestException {
String fullFilename = filename + extension;
if (!new File(fullFilename).exists()
&& (!recoveryDir ||
(destination != Downloads.Impl.DESTINATION_CACHE_PARTITION &&
+ destination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION &&
destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE &&
destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING))) {
return fullFilename;
sequence += sRandom.nextInt(magnitude) + 1;
}
}
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
+ "failed to generate an unused filename on internal download storage");
}
/**
- * Deletes purgeable files from the cache partition. This also deletes
- * the matching database entries. Files are deleted in LRU order until
- * the total byte size is greater than targetBytes.
+ * Checks whether the filename looks legitimate
*/
- public static final boolean discardPurgeableFiles(Context context, long targetBytes) {
- Cursor cursor = context.getContentResolver().query(
- Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- null,
- "( " +
- Downloads.Impl.COLUMN_STATUS + " = '" + Downloads.Impl.STATUS_SUCCESS + "' AND " +
- Downloads.Impl.COLUMN_DESTINATION +
- " = '" + Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE + "' )",
- null,
- Downloads.Impl.COLUMN_LAST_MODIFICATION);
- if (cursor == null) {
- return false;
- }
- long totalFreed = 0;
+ static boolean isFilenameValid(String filename, File downloadsDataDir) {
+ final String[] whitelist;
try {
- cursor.moveToFirst();
- while (!cursor.isAfterLast() && totalFreed < targetBytes) {
- File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.Impl._DATA)));
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "purging " + file.getAbsolutePath() + " for " +
- file.length() + " bytes");
- }
- totalFreed += file.length();
- file.delete();
- long id = cursor.getLong(cursor.getColumnIndex(Downloads.Impl._ID));
- context.getContentResolver().delete(
- ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
- null, null);
- cursor.moveToNext();
- }
- } finally {
- cursor.close();
+ filename = new File(filename).getCanonicalPath();
+ whitelist = new String[] {
+ downloadsDataDir.getCanonicalPath(),
+ Environment.getDownloadCacheDirectory().getCanonicalPath(),
+ Environment.getExternalStorageDirectory().getCanonicalPath(),
+ };
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to resolve canonical path: " + e);
+ return false;
}
- if (Constants.LOGV) {
- if (totalFreed > 0) {
- Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
- targetBytes + " requested");
+
+ for (String test : whitelist) {
+ if (filename.startsWith(test)) {
+ return true;
}
}
- return totalFreed > 0;
- }
-
- /**
- * Returns whether the network is available
- */
- public static boolean isNetworkAvailable(SystemFacade system) {
- return system.getActiveNetworkType() != null;
- }
- /**
- * Checks whether the filename looks legitimate
- */
- public static boolean isFilenameValid(String filename) {
- filename = filename.replaceFirst("/+", "/"); // normalize leading slashes
- return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
- || filename.startsWith(Environment.getExternalStorageDirectory().toString());
+ return false;
}
/**
} catch (RuntimeException ex) {
if (Constants.LOGV) {
Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
- } else if (Config.LOGD) {
+ } else if (false) {
Log.d(Constants.TAG, "invalid selection triggered " + ex);
}
throw ex;