summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/Helpers.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/downloads/Helpers.java')
-rw-r--r--src/com/android/providers/downloads/Helpers.java510
1 files changed, 510 insertions, 0 deletions
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
new file mode 100644
index 00000000..f966a7f5
--- /dev/null
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2008 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 com.android.providers.downloads;
+
+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.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.util.List;
+import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Some helper functions for the download manager
+ */
+public class Helpers {
+ /** Tag used for debugging/logging */
+ private static final String TAG = Constants.TAG;
+
+ /** Regex used to parse content-disposition headers */
+ private static final Pattern CONTENT_DISPOSITION_PATTERN =
+ Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+
+ private Helpers() {
+ }
+
+ /*
+ * Parse the Content-Disposition HTTP Header. The format of the header
+ * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
+ * This header provides a filename for content that is going to be
+ * downloaded to the file system. We only support the attachment type.
+ */
+ private static String parseContentDisposition(String contentDisposition) {
+ try {
+ Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
+ if (m.find()) {
+ return m.group(1);
+ }
+ } catch (IllegalStateException ex) {
+ // This function is defined as returning null when it can't parse the header
+ }
+ return null;
+ }
+
+ /**
+ * Creates a filename (where the file should be saved) from a uri.
+ */
+ public static DownloadFileInfo generateSaveFile(
+ Context context,
+ String url,
+ String hint,
+ String contentDisposition,
+ String contentLocation,
+ String mimeType,
+ int destination,
+ boolean otaUpdate,
+ boolean noSystem,
+ 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(TAG, "external download with no mime type not allowed");
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
+ }
+ if (noSystem && mimeType.equalsIgnoreCase(Constants.MIMETYPE_APK)) {
+ if (Config.LOGD) {
+ Log.d(TAG, "system files not allowed by initiating application");
+ }
+ 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);
+ List<ResolveInfo> list = pm.queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ //Log.i(TAG, "*** FILENAME QUERY " + intent + ": " + list);
+
+ if (list.size() == 0) {
+ if (Config.LOGD) {
+ Log.d(Constants.TAG, "no handler found for type " + mimeType);
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
+ }
+ }
+ }
+ String filename = chooseFilename(
+ url, hint, contentDisposition, contentLocation, destination, otaUpdate);
+
+ // 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);
+ } else {
+ extension = chooseExtensionFromFilename(
+ mimeType, destination, otaUpdate, filename, dotIndex);
+ filename = filename.substring(0, dotIndex);
+ }
+
+ /*
+ * 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
+ || 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(TAG, "download aborted - not enough free space in internal storage");
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ }
+ stat.restat(base.getPath());
+ }
+
+ } else {
+ if (destination == Downloads.DESTINATION_DATA_CACHE) {
+ base = context.getCacheDir();
+ if (!base.isDirectory() && !base.mkdir()) {
+ if (Config.LOGD) {
+ Log.d(TAG, "download aborted - can't create base directory "
+ + base.getPath());
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ }
+ stat = new StatFs(base.getPath());
+ } 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(TAG, "download aborted - can't create base directory "
+ + base.getPath());
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ }
+ stat = new StatFs(base.getPath());
+ } else {
+ if (Config.LOGD) {
+ Log.d(TAG, "download aborted - no external storage");
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ }
+ }
+
+ /*
+ * 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(TAG, "download aborted - not enough free space");
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ }
+
+ }
+
+ boolean otaFilename = Constants.OTA_UPDATE_FILENAME.equalsIgnoreCase(filename + extension);
+ boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
+
+ filename = base.getPath() + File.separator + filename;
+
+ /*
+ * Generate a unique filename, create the file, return it.
+ */
+ if (Constants.LOGVV) {
+ Log.v(TAG, "target file: " + filename + extension);
+ }
+
+ String fullFilename = chooseUniqueFilename(
+ destination, otaUpdate, filename, extension, otaFilename, recoveryDir);
+ if (fullFilename != null) {
+ return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
+ } else {
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ }
+ }
+
+ private static String chooseFilename(String url, String hint, String contentDisposition,
+ String contentLocation, int destination, boolean otaUpdate) {
+ String filename = null;
+
+ // Before we even start, special-case the OTA updates
+ if (destination == Downloads.DESTINATION_CACHE_PARTITION && otaUpdate) {
+ filename = Constants.OTA_UPDATE_FILENAME;
+ }
+
+ // First, try to use the hint from the application, if there's one
+ if (filename == null && hint != null && !hint.endsWith("/")) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "getting filename from hint");
+ }
+ int index = hint.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = hint.substring(index);
+ } else {
+ filename = hint;
+ }
+ }
+
+ // If we couldn't do anything with the hint, move toward the content disposition
+ if (filename == null && contentDisposition != null) {
+ filename = parseContentDisposition(contentDisposition);
+ if (filename != null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "getting filename from content-disposition");
+ }
+ int index = filename.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = filename.substring(index);
+ }
+ }
+ }
+
+ // If we still have nothing at this point, try the content location
+ if (filename == null && contentLocation != null) {
+ String decodedContentLocation = Uri.decode(contentLocation);
+ if (decodedContentLocation != null
+ && !decodedContentLocation.endsWith("/")
+ && decodedContentLocation.indexOf('?') < 0) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "getting filename from content-location");
+ }
+ int index = decodedContentLocation.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = decodedContentLocation.substring(index);
+ } else {
+ filename = decodedContentLocation;
+ }
+ }
+ }
+
+ // If all the other http-related approaches failed, use the plain uri
+ if (filename == null) {
+ String decodedUrl = Uri.decode(url);
+ if (decodedUrl != null
+ && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) {
+ int index = decodedUrl.lastIndexOf('/') + 1;
+ if (index > 0) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "getting filename from uri");
+ }
+ filename = decodedUrl.substring(index);
+ }
+ }
+ }
+
+ // Finally, if couldn't get filename from URI, get a generic filename
+ if (filename == null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "using default filename");
+ }
+ filename = Constants.DEFAULT_DL_FILENAME;
+ }
+ return filename;
+ }
+
+ private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) {
+ String extension = null;
+ if (mimeType != null) {
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ if (extension != null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "adding extension from type");
+ }
+ extension = "." + extension;
+ } else {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "couldn't find extension for " + mimeType);
+ }
+ }
+ }
+ if (extension == null) {
+ if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
+ if (mimeType.equalsIgnoreCase("text/html")) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "adding default html extension");
+ }
+ extension = Constants.DEFAULT_DL_HTML_EXTENSION;
+ } else if (useDefaults) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "adding default text extension");
+ }
+ extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
+ }
+ } else if (useDefaults) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "adding default binary extension");
+ }
+ extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
+ }
+ }
+ return extension;
+ }
+
+ private static String chooseExtensionFromFilename(String mimeType, int destination,
+ boolean otaUpdate, String filename, int dotIndex) {
+ String extension = null;
+ if (mimeType != null
+ && !(destination == Downloads.DESTINATION_CACHE_PARTITION && otaUpdate)) {
+ // 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)) {
+ extension = chooseExtensionFromMimeType(mimeType, false);
+ if (extension != null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "substituting extension from type");
+ }
+ } else {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "couldn't find extension for " + mimeType);
+ }
+ }
+ }
+ }
+ if (extension == null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "keeping extension");
+ }
+ extension = filename.substring(dotIndex);
+ }
+ return extension;
+ }
+
+ private static String chooseUniqueFilename(int destination, boolean otaUpdate, String filename,
+ String extension, boolean otaFilename, boolean recoveryDir) {
+ String fullFilename = filename + extension;
+ if (!new File(fullFilename).exists()
+ && (!recoveryDir ||
+ (destination != Downloads.DESTINATION_CACHE_PARTITION &&
+ destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE))
+ && (!otaFilename ||
+ (otaUpdate && destination == Downloads.DESTINATION_CACHE_PARTITION))) {
+ return fullFilename;
+ } else if (!(otaUpdate && destination == Downloads.DESTINATION_CACHE_PARTITION)) {
+ filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
+ /*
+ * This number is used to generate partially randomized filenames to avoid
+ * collisions.
+ * It starts at 1.
+ * The next 9 iterations increment it by 1 at a time (up to 10).
+ * The next 9 iterations increment it by 1 to 10 (random) at a time.
+ * The next 9 iterations increment it by 1 to 100 (random) at a time.
+ * ... Up to the point where it increases by 100000000 at a time.
+ * (the maximum value that can be reached is 1000000000)
+ * As soon as a number is reached that generates a filename that doesn't exist,
+ * that filename is used.
+ * If the filename coming in is [base].[ext], the generated filenames are
+ * [base]-[sequence].[ext].
+ */
+ int sequence = 1;
+ Random random = new Random();
+ for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
+ for (int iteration = 0; iteration < 9; ++iteration) {
+ fullFilename = filename + sequence + extension;
+ if (!new File(fullFilename).exists()) {
+ return fullFilename;
+ }
+ if (Constants.LOGVV) {
+ Log.v(TAG, "file with sequence number " + sequence + " exists");
+ }
+ sequence += random.nextInt(magnitude) + 1;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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.
+ */
+ 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;
+ try {
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast() && totalFreed < targetBytes) {
+ File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.FILENAME)));
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "purging " + file.getAbsolutePath() + " for " +
+ file.length() + " bytes");
+ }
+ totalFreed += file.length();
+ file.delete();
+ cursor.deleteRow(); // This moves the cursor to the next entry,
+ // no need to call next()
+ }
+ } finally {
+ cursor.close();
+ }
+ if (Constants.LOGV) {
+ if (totalFreed > 0) {
+ Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
+ targetBytes + " requested");
+ }
+ }
+ 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.getActiveNetworkInfo();
+ if (info != null) {
+ if (info.getState() == NetworkInfo.State.CONNECTED) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "network is available");
+ }
+ return true;
+ }
+ }
+ }
+ if (Constants.LOGVV) {
+ Log.v(TAG, "network is not available");
+ }
+ return false;
+ }
+
+}