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.ConnectivityManager;
-import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Environment;
-import android.os.StatFs;
import android.os.SystemClock;
import android.provider.Downloads;
-import android.telephony.TelephonyManager;
-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.util.List;
+import java.io.File;
+import java.io.IOException;
import java.util.Random;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import java.util.Set;
/**
* Some helper functions for the download manager
*/
public class Helpers {
-
- public static Random rnd = new Random(SystemClock.uptimeMillis());
+ 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() {
}
}
/**
- * 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 contentLocation,
String mimeType,
int destination,
- int contentLength) throws FileNotFoundException {
-
- /*
- * Don't download files that we won't be able to handle
- */
- if (destination == Downloads.DESTINATION_EXTERNAL
- || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
- if (mimeType == null) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "external download with no mime type not allowed");
- }
- return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
- }
- 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 new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
- }
- }
+ long contentLength,
+ StorageManager storageManager) throws StopRequestException {
+ if (contentLength < 0) {
+ contentLength = 0;
}
- String filename = chooseFilename(
- url, hint, contentDisposition, contentLocation, destination);
-
- // Split filename between base and extension
- // Add an extension if filename does not have one
- String extension = null;
- int dotIndex = filename.indexOf('.');
- if (dotIndex < 0) {
- extension = chooseExtensionFromMimeType(mimeType, true);
+ String path;
+ File base = null;
+ if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
+ path = Uri.parse(hint).getPath();
} else {
- extension = chooseExtensionFromFilename(
- mimeType, destination, filename, dotIndex);
- filename = filename.substring(0, dotIndex);
+ base = storageManager.locateDestinationDirectory(mimeType, destination,
+ contentLength);
+ path = chooseFilename(url, hint, contentDisposition, contentLocation,
+ destination);
}
+ storageManager.verifySpace(destination, path, contentLength);
+ if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
+ path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
+ }
+ path = getFullPath(path, mimeType, destination, base);
+ return path;
+ }
- /*
- * Locate the directory where the file will be saved
- */
-
- File base = null;
- StatFs stat = null;
- // DRM messages should be temporarily stored internally and then passed to
- // the DRM content provider
- if (destination == Downloads.DESTINATION_CACHE_PARTITION
- || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE
- || destination == Downloads.DESTINATION_CACHE_PARTITION_NOROAMING
- || DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
- 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();
- for (;;) {
- int availableBlocks = stat.getAvailableBlocks();
- if (blockSize * ((long) availableBlocks - 4) >= contentLength) {
- break;
- }
- if (!discardPurgeableFiles(context,
- contentLength - blockSize * ((long) availableBlocks - 4))) {
- if (Config.LOGD) {
- Log.d(Constants.TAG,
- "download aborted - not enough free space in internal storage");
- }
- return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
- }
- stat.restat(base.getPath());
+ static String getFullPath(String filename, String mimeType, int destination, File base)
+ throws StopRequestException {
+ String extension = null;
+ 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 {
- if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- String root = Environment.getExternalStorageDirectory().getPath();
- base = new File(root + Constants.DEFAULT_DL_SUBDIR);
- if (!base.isDirectory() && !base.mkdir()) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - can't create base directory "
- + base.getPath());
- }
- return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
- }
- stat = new StatFs(base.getPath());
+ // Split filename between base and extension
+ // Add an extension if filename does not have one
+ if (missingExtension) {
+ extension = chooseExtensionFromMimeType(mimeType, true);
} else {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - no external storage");
- }
- return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
+ filename = filename.substring(0, dotIndex);
}
-
- /*
- * 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) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "download aborted - not enough free space");
- }
- return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
- }
-
}
boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
- filename = base.getPath() + File.separator + filename;
+ if (base != null) {
+ filename = base.getPath() + File.separator + filename;
+ }
- /*
- * Generate a unique filename, create the file, return it.
- */
if (Constants.LOGVV) {
Log.v(Constants.TAG, "target file: " + filename + extension);
}
- String fullFilename = chooseUniqueFilename(
- destination, filename, extension, recoveryDir);
- if (fullFilename != null) {
- return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
- } else {
- return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ synchronized (sUniqueLock) {
+ final String path = chooseUniqueFilenameLocked(
+ destination, filename, extension, recoveryDir);
+
+ // 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);
+ }
+ return path;
}
}
filename = Constants.DEFAULT_DL_FILENAME;
}
- filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_");
-
+ // The VFAT file system is assumed as target for downloads.
+ // Replace invalid characters according to the specifications of VFAT.
+ filename = replaceInvalidVfatCharacters(filename);
return filename;
}
}
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) {
+ 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.DESTINATION_CACHE_PARTITION &&
- destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE &&
- destination != Downloads.DESTINATION_CACHE_PARTITION_NOROAMING))) {
+ (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;
}
filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
if (Constants.LOGVV) {
Log.v(Constants.TAG, "file with sequence number " + sequence + " exists");
}
- sequence += rnd.nextInt(magnitude) + 1;
+ sequence += sRandom.nextInt(magnitude) + 1;
}
}
- return null;
+ 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.CONTENT_URI,
- null,
- "( " +
- Downloads.STATUS + " = " + Downloads.STATUS_SUCCESS + " AND " +
- Downloads.DESTINATION + " = " + Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE
- + " )",
- null,
- Downloads.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._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._ID));
- context.getContentResolver().delete(
- ContentUris.withAppendedId(Downloads.CONTENT_URI, id), null, null);
- cursor.moveToNext();
- }
- } finally {
- cursor.close();
- }
- if (Constants.LOGV) {
- if (totalFreed > 0) {
- Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
- targetBytes + " requested");
- }
+ 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;
}
- return totalFreed > 0;
- }
- /**
- * Returns whether the network is available
- */
- public static boolean isNetworkAvailable(Context context) {
- ConnectivityManager connectivity =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (connectivity == null) {
- Log.w(Constants.TAG, "couldn't get connectivity manager");
- } else {
- NetworkInfo[] info = connectivity.getAllNetworkInfo();
- if (info != null) {
- for (int i = 0; i < info.length; i++) {
- if (info[i].getState() == NetworkInfo.State.CONNECTED) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "network is available");
- }
- return true;
- }
- }
+ for (String test : whitelist) {
+ if (filename.startsWith(test)) {
+ return true;
}
}
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "network is not available");
- }
- return false;
- }
- /**
- * Returns whether the network is roaming
- */
- public static boolean isNetworkRoaming(Context context) {
- ConnectivityManager connectivity =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (connectivity == null) {
- Log.w(Constants.TAG, "couldn't get connectivity manager");
- } else {
- NetworkInfo info = connectivity.getActiveNetworkInfo();
- if (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE) {
- if (TelephonyManager.getDefault().isNetworkRoaming()) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "network is roaming");
- }
- return true;
- } else {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "network is not roaming");
- }
- }
- } else {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "not using mobile network");
- }
- }
- }
return false;
}
- /**
- * Checks whether the filename looks legitimate
- */
- public static boolean isFilenameValid(String filename) {
- File dir = new File(filename).getParentFile();
- return dir.equals(Environment.getDownloadCacheDirectory())
- || dir.equals(new File(Environment.getExternalStorageDirectory()
- + Constants.DEFAULT_DL_SUBDIR));
- }
-
/**
* Checks whether this looks like a legitimate selection parameter
*/
public static void validateSelection(String selection, Set<String> allowedColumns) {
try {
- if (selection == null) {
+ if (selection == null || selection.isEmpty()) {
return;
}
Lexer lexer = new Lexer(selection, allowedColumns);
} 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;
// quoted strings
if (chars[mOffset] == '\'') {
++mOffset;
- while(mOffset < chars.length) {
+ while (mOffset < chars.length) {
if (chars[mOffset] == '\'') {
if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') {
++mOffset;
}
// anything we don't recognize
- throw new IllegalArgumentException("illegal character");
+ throw new IllegalArgumentException("illegal character: " + chars[mOffset]);
}
private static final boolean isIdentifierStart(char c) {
(c >= '0' && c <= '9');
}
}
+
+ /**
+ * Replace invalid filename characters according to
+ * specifications of the VFAT.
+ * @note Package-private due to testing.
+ */
+ private static String replaceInvalidVfatCharacters(String filename) {
+ final char START_CTRLCODE = 0x00;
+ final char END_CTRLCODE = 0x1f;
+ final char QUOTEDBL = 0x22;
+ final char ASTERISK = 0x2A;
+ final char SLASH = 0x2F;
+ final char COLON = 0x3A;
+ final char LESS = 0x3C;
+ final char GREATER = 0x3E;
+ final char QUESTION = 0x3F;
+ final char BACKSLASH = 0x5C;
+ final char BAR = 0x7C;
+ final char DEL = 0x7F;
+ final char UNDERSCORE = 0x5F;
+
+ StringBuffer sb = new StringBuffer();
+ char ch;
+ boolean isRepetition = false;
+ for (int i = 0; i < filename.length(); i++) {
+ ch = filename.charAt(i);
+ if ((START_CTRLCODE <= ch &&
+ ch <= END_CTRLCODE) ||
+ ch == QUOTEDBL ||
+ ch == ASTERISK ||
+ ch == SLASH ||
+ ch == COLON ||
+ ch == LESS ||
+ ch == GREATER ||
+ ch == QUESTION ||
+ ch == BACKSLASH ||
+ ch == BAR ||
+ ch == DEL){
+ if (!isRepetition) {
+ sb.append(UNDERSCORE);
+ isRepetition = true;
+ }
+ } else {
+ sb.append(ch);
+ isRepetition = false;
+ }
+ }
+ return sb.toString();
+ }
}