blob: 855cba28437a1f2b13cdc6b93cf6133df396e559 [file] [log] [blame]
/*
* 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.ContentResolver;
import android.content.ContentUris;
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.util.Random;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 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 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;
}
/**
* Exception thrown from methods called by generateSaveFile() for any fatal error.
*/
public static class GenerateSaveFileError extends Exception {
int mStatus;
String mMessage;
public GenerateSaveFileError(int status, String message) {
mStatus = status;
mMessage = message;
}
}
/**
* Creates a filename (where the file should be saved) from info about a download.
*/
public static String generateSaveFile(
Context context,
String url,
String hint,
String contentDisposition,
String contentLocation,
String mimeType,
int destination,
long contentLength,
boolean isPublicApi) throws GenerateSaveFileError {
checkCanHandleDownload(context, mimeType, destination, isPublicApi);
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
return getPathForFileUri(hint, contentLength);
} else {
return chooseFullPath(context, url, hint, contentDisposition, contentLocation, mimeType,
destination, contentLength);
}
}
private static String getPathForFileUri(String hint, long contentLength)
throws GenerateSaveFileError {
if (!isExternalMediaMounted()) {
throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
"external media not mounted");
}
String path = Uri.parse(hint).getPath();
if (new File(path).exists()) {
Log.d(Constants.TAG, "File already exists: " + path);
throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR,
"requested destination file already exists");
}
if (getAvailableBytes(getFilesystemRoot(path)) < contentLength) {
throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
"insufficient space on external storage");
}
return path;
}
/**
* @return the root of the filesystem containing the given path
*/
public static File getFilesystemRoot(String path) {
File cache = Environment.getDownloadCacheDirectory();
if (path.startsWith(cache.getPath())) {
return cache;
}
File external = Environment.getExternalStorageDirectory();
if (path.startsWith(external.getPath())) {
return external;
}
throw new IllegalArgumentException("Cannot determine filesystem root for " + 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
String extension = null;
int dotIndex = filename.indexOf('.');
if (dotIndex < 0) {
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 (Constants.LOGVV) {
Log.v(Constants.TAG, "target file: " + filename + extension);
}
return chooseUniqueFilename(destination, filename, extension, recoveryDir);
}
private static void checkCanHandleDownload(Context context, String mimeType, int destination,
boolean isPublicApi) throws GenerateSaveFileError {
if (isPublicApi) {
return;
}
if (destination == Downloads.Impl.DESTINATION_EXTERNAL
|| destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
if (mimeType == null) {
throw new GenerateSaveFileError(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
"external download with no mime type not allowed");
}
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 (Constants.LOGV) {
Log.v(Constants.TAG, "no handler found for type " + mimeType);
}
throw new GenerateSaveFileError(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
"no handler found for this download type");
}
}
}
}
private static File locateDestinationDirectory(Context context, String mimeType,
int destination, long contentLength)
throws GenerateSaveFileError {
// 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)) {
return getCacheDestination(context, contentLength);
}
return getExternalDestination(contentLength);
}
private static File getExternalDestination(long contentLength) throws GenerateSaveFileError {
if (!isExternalMediaMounted()) {
throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
"external media not mounted");
}
File root = Environment.getExternalStorageDirectory();
if (getAvailableBytes(root) < contentLength) {
// Insufficient space.
Log.d(Constants.TAG, "download aborted - not enough free space");
throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
"insufficient space on external media");
}
File base = new File(root.getPath() + 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.
throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR,
"unable to create external downloads directory " + base.getPath());
}
return base;
}
public static boolean isExternalMediaMounted() {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
// No SD card found.
Log.d(Constants.TAG, "no external storage");
return false;
}
return true;
}
private static File getCacheDestination(Context context, long contentLength)
throws GenerateSaveFileError {
File base;
base = Environment.getDownloadCacheDirectory();
long bytesAvailable = getAvailableBytes(base);
while (bytesAvailable < contentLength) {
// Insufficient space; try discarding purgeable files.
if (!discardPurgeableFiles(context, contentLength - bytesAvailable)) {
// No files to purge, give up.
throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
"not enough free space in internal download storage, unable to free any "
+ "more");
}
bytesAvailable = getAvailableBytes(base);
}
return base;
}
/**
* @return the number of bytes available on the filesystem rooted at the given File
*/
public static long getAvailableBytes(File root) {
StatFs stat = new StatFs(root.getPath());
// put a bit of margin (in case creating the file grows the system by a few blocks)
long availableBlocks = (long) stat.getAvailableBlocks() - 4;
return stat.getBlockSize() * availableBlocks;
}
private static String chooseFilename(String url, String hint, String contentDisposition,
String contentLocation, int destination) {
String filename = null;
// 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(Constants.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(Constants.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(Constants.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(Constants.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(Constants.TAG, "using default filename");
}
filename = Constants.DEFAULT_DL_FILENAME;
}
filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_");
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(Constants.TAG, "adding extension from type");
}
extension = "." + extension;
} else {
if (Constants.LOGVV) {
Log.v(Constants.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(Constants.TAG, "adding default html extension");
}
extension = Constants.DEFAULT_DL_HTML_EXTENSION;
} else if (useDefaults) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "adding default text extension");
}
extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
}
} else if (useDefaults) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "adding default binary extension");
}
extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
}
}
return extension;
}
private static String chooseExtensionFromFilename(String mimeType, int destination,
String filename, int dotIndex) {
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)) {
extension = chooseExtensionFromMimeType(mimeType, false);
if (extension != null) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "substituting extension from type");
}
} else {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
}
}
}
}
if (extension == null) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "keeping extension");
}
extension = filename.substring(dotIndex);
}
return extension;
}
private static String chooseUniqueFilename(int destination, String filename,
String extension, boolean recoveryDir) throws GenerateSaveFileError {
String fullFilename = filename + extension;
if (!new File(fullFilename).exists()
&& (!recoveryDir ||
(destination != Downloads.Impl.DESTINATION_CACHE_PARTITION &&
destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE &&
destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING))) {
return fullFilename;
}
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;
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(Constants.TAG, "file with sequence number " + sequence + " exists");
}
sequence += sRandom.nextInt(magnitude) + 1;
}
}
throw new GenerateSaveFileError(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.
*/
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;
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();
}
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(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());
}
/**
* Checks whether this looks like a legitimate selection parameter
*/
public static void validateSelection(String selection, Set<String> allowedColumns) {
try {
if (selection == null || selection.isEmpty()) {
return;
}
Lexer lexer = new Lexer(selection, allowedColumns);
parseExpression(lexer);
if (lexer.currentToken() != Lexer.TOKEN_END) {
throw new IllegalArgumentException("syntax error");
}
} catch (RuntimeException ex) {
if (Constants.LOGV) {
Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
} else if (Config.LOGD) {
Log.d(Constants.TAG, "invalid selection triggered " + ex);
}
throw ex;
}
}
// expression <- ( expression ) | statement [AND_OR ( expression ) | statement] *
// | statement [AND_OR expression]*
private static void parseExpression(Lexer lexer) {
for (;;) {
// ( expression )
if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) {
lexer.advance();
parseExpression(lexer);
if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) {
throw new IllegalArgumentException("syntax error, unmatched parenthese");
}
lexer.advance();
} else {
// statement
parseStatement(lexer);
}
if (lexer.currentToken() != Lexer.TOKEN_AND_OR) {
break;
}
lexer.advance();
}
}
// statement <- COLUMN COMPARE VALUE
// | COLUMN IS NULL
private static void parseStatement(Lexer lexer) {
// both possibilities start with COLUMN
if (lexer.currentToken() != Lexer.TOKEN_COLUMN) {
throw new IllegalArgumentException("syntax error, expected column name");
}
lexer.advance();
// statement <- COLUMN COMPARE VALUE
if (lexer.currentToken() == Lexer.TOKEN_COMPARE) {
lexer.advance();
if (lexer.currentToken() != Lexer.TOKEN_VALUE) {
throw new IllegalArgumentException("syntax error, expected quoted string");
}
lexer.advance();
return;
}
// statement <- COLUMN IS NULL
if (lexer.currentToken() == Lexer.TOKEN_IS) {
lexer.advance();
if (lexer.currentToken() != Lexer.TOKEN_NULL) {
throw new IllegalArgumentException("syntax error, expected NULL");
}
lexer.advance();
return;
}
// didn't get anything good after COLUMN
throw new IllegalArgumentException("syntax error after column name");
}
/**
* A simple lexer that recognizes the words of our restricted subset of SQL where clauses
*/
private static class Lexer {
public static final int TOKEN_START = 0;
public static final int TOKEN_OPEN_PAREN = 1;
public static final int TOKEN_CLOSE_PAREN = 2;
public static final int TOKEN_AND_OR = 3;
public static final int TOKEN_COLUMN = 4;
public static final int TOKEN_COMPARE = 5;
public static final int TOKEN_VALUE = 6;
public static final int TOKEN_IS = 7;
public static final int TOKEN_NULL = 8;
public static final int TOKEN_END = 9;
private final String mSelection;
private final Set<String> mAllowedColumns;
private int mOffset = 0;
private int mCurrentToken = TOKEN_START;
private final char[] mChars;
public Lexer(String selection, Set<String> allowedColumns) {
mSelection = selection;
mAllowedColumns = allowedColumns;
mChars = new char[mSelection.length()];
mSelection.getChars(0, mChars.length, mChars, 0);
advance();
}
public int currentToken() {
return mCurrentToken;
}
public void advance() {
char[] chars = mChars;
// consume whitespace
while (mOffset < chars.length && chars[mOffset] == ' ') {
++mOffset;
}
// end of input
if (mOffset == chars.length) {
mCurrentToken = TOKEN_END;
return;
}
// "("
if (chars[mOffset] == '(') {
++mOffset;
mCurrentToken = TOKEN_OPEN_PAREN;
return;
}
// ")"
if (chars[mOffset] == ')') {
++mOffset;
mCurrentToken = TOKEN_CLOSE_PAREN;
return;
}
// "?"
if (chars[mOffset] == '?') {
++mOffset;
mCurrentToken = TOKEN_VALUE;
return;
}
// "=" and "=="
if (chars[mOffset] == '=') {
++mOffset;
mCurrentToken = TOKEN_COMPARE;
if (mOffset < chars.length && chars[mOffset] == '=') {
++mOffset;
}
return;
}
// ">" and ">="
if (chars[mOffset] == '>') {
++mOffset;
mCurrentToken = TOKEN_COMPARE;
if (mOffset < chars.length && chars[mOffset] == '=') {
++mOffset;
}
return;
}
// "<", "<=" and "<>"
if (chars[mOffset] == '<') {
++mOffset;
mCurrentToken = TOKEN_COMPARE;
if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) {
++mOffset;
}
return;
}
// "!="
if (chars[mOffset] == '!') {
++mOffset;
mCurrentToken = TOKEN_COMPARE;
if (mOffset < chars.length && chars[mOffset] == '=') {
++mOffset;
return;
}
throw new IllegalArgumentException("Unexpected character after !");
}
// columns and keywords
// first look for anything that looks like an identifier or a keyword
// and then recognize the individual words.
// no attempt is made at discarding sequences of underscores with no alphanumeric
// characters, even though it's not clear that they'd be legal column names.
if (isIdentifierStart(chars[mOffset])) {
int startOffset = mOffset;
++mOffset;
while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) {
++mOffset;
}
String word = mSelection.substring(startOffset, mOffset);
if (mOffset - startOffset <= 4) {
if (word.equals("IS")) {
mCurrentToken = TOKEN_IS;
return;
}
if (word.equals("OR") || word.equals("AND")) {
mCurrentToken = TOKEN_AND_OR;
return;
}
if (word.equals("NULL")) {
mCurrentToken = TOKEN_NULL;
return;
}
}
if (mAllowedColumns.contains(word)) {
mCurrentToken = TOKEN_COLUMN;
return;
}
throw new IllegalArgumentException("unrecognized column or keyword");
}
// quoted strings
if (chars[mOffset] == '\'') {
++mOffset;
while (mOffset < chars.length) {
if (chars[mOffset] == '\'') {
if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') {
++mOffset;
} else {
break;
}
}
++mOffset;
}
if (mOffset == chars.length) {
throw new IllegalArgumentException("unterminated string");
}
++mOffset;
mCurrentToken = TOKEN_VALUE;
return;
}
// anything we don't recognize
throw new IllegalArgumentException("illegal character: " + chars[mOffset]);
}
private static final boolean isIdentifierStart(char c) {
return c == '_' ||
(c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z');
}
private static final boolean isIdentifierChar(char c) {
return c == '_' ||
(c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9');
}
}
/**
* Delete the given file from device
* and delete its row from the downloads database.
*/
/* package */ static void deleteFile(ContentResolver resolver, long id, String path, String mimeType) {
try {
File file = new File(path);
file.delete();
} catch (Exception e) {
Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
}
resolver.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, Downloads.Impl._ID + " = ? ",
new String[]{String.valueOf(id)});
}
}