diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 57f55b3cb4f7e4136cde8d1ea12c1e70ec903362 (patch) | |
tree | 627c44eef7eb87514d987b5bce439eb9cb79be99 /src/com/android/providers/downloads/Helpers.java | |
download | android_packages_providers_DownloadProvider-57f55b3cb4f7e4136cde8d1ea12c1e70ec903362.tar.gz android_packages_providers_DownloadProvider-57f55b3cb4f7e4136cde8d1ea12c1e70ec903362.tar.bz2 android_packages_providers_DownloadProvider-57f55b3cb4f7e4136cde8d1ea12c1e70ec903362.zip |
Initial Contribution
Diffstat (limited to 'src/com/android/providers/downloads/Helpers.java')
-rw-r--r-- | src/com/android/providers/downloads/Helpers.java | 510 |
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; + } + +} |