summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/android/providers/downloads/Constants.java45
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java61
-rw-r--r--src/com/android/providers/downloads/DownloadNotification.java20
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java363
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java30
-rw-r--r--src/com/android/providers/downloads/DownloadService.java198
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java327
-rw-r--r--src/com/android/providers/downloads/Helpers.java492
8 files changed, 1074 insertions, 462 deletions
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index f3dd08c7..cffda04a 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -28,11 +28,26 @@ public class Constants {
/** Tag used for debugging/logging */
public static final String TAG = "DownloadManager";
- /** The permission that allows to access data about all downloads */
- public static final String UI_PERMISSION = "android.permission.ACCESS_DOWNLOAD_DATA";
+ /** The column that used to be used for the HTTP method of the request */
+ public static final String RETRY_AFTER___REDIRECT_COUNT = "method";
- /** The permission that allows to download a system image */
- public static final String OTA_UPDATE_PERMISSION = "android.permission.DOWNLOAD_OTA_UPDATE";
+ /** The column that used to be used for the magic OTA update filename */
+ public static final String OTA_UPDATE = "otaupdate";
+
+ /** The column that used to be used to reject system filetypes */
+ public static final String NO_SYSTEM_FILES = "no_system";
+
+ /** The column that is used for the downloads's ETag */
+ public static final String ETAG = "etag";
+
+ /** The column that is used for the initiating app's UID */
+ public static final String UID = "uid";
+
+ /** The column that is used to remember whether the media scanner was invoked */
+ public static final String MEDIA_SCANNED = "scanned";
+
+ /** The column that is used to count retries */
+ public static final String FAILED_CONNECTIONS = "numfailed";
/** The intent that gets sent when the service must wake up for a retry */
public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
@@ -73,9 +88,6 @@ public class Constants {
/** A magic filename that is allowed to exist within the system cache */
public static final String RECOVERY_DIRECTORY = "recovery";
- /** The magic filename for OTA updates */
- public static final String OTA_UPDATE_FILENAME = "update.install";
-
/** The default user agent used for downloads */
public static final String DEFAULT_USER_AGENT = "AndroidDownloadManager";
@@ -105,6 +117,23 @@ public class Constants {
public static final int MAX_RETRIES = 5;
/**
+ * The minimum amount of time that the download manager accepts for
+ * a Retry-After response header with a parameter in delta-seconds.
+ */
+ public static final int MIN_RETRY_AFTER = 30; // 30s
+
+ /**
+ * The maximum amount of time that the download manager accepts for
+ * a Retry-After response header with a parameter in delta-seconds.
+ */
+ public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h
+
+ /**
+ * The maximum number of redirects.
+ */
+ public static final int MAX_REDIRECTS = 5; // can't be more than 7.
+
+ /**
* The time between a failure and the first retry after an IOException.
* Each subsequent retry grows exponentially, doubling each time.
* The time is in seconds.
@@ -112,7 +141,7 @@ public class Constants {
public static final int RETRY_FIRST_DELAY = 30;
/** Enable verbose logging - use with "setprop log.tag.DownloadManager VERBOSE" */
- private static final boolean LOCAL_LOGV = false;
+ private static final boolean LOCAL_LOGV = true;
public static final boolean LOGV = Config.LOGV
|| (Config.LOGD && LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE));
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index b8cead65..e051f41a 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -27,19 +27,17 @@ import android.provider.Downloads;
public class DownloadInfo {
public int id;
public String uri;
- public int method;
- public String entity;
public boolean noIntegrity;
public String hint;
public String filename;
- public boolean otaUpdate;
public String mimetype;
public int destination;
- public boolean noSystem;
public int visibility;
public int control;
public int status;
public int numFailed;
+ public int retryAfter;
+ public int redirectCount;
public long lastMod;
public String pckg;
public String clazz;
@@ -54,28 +52,26 @@ public class DownloadInfo {
public volatile boolean hasActiveThread;
- public DownloadInfo(int id, String uri, int method, String entity, boolean noIntegrity,
- String hint, String filename, boolean otaUpdate,
- String mimetype, int destination, boolean noSystem, int visibility,
- int control, int status, int numFailed, long lastMod,
+ public DownloadInfo(int id, String uri, boolean noIntegrity,
+ String hint, String filename,
+ String mimetype, int destination, int visibility, int control,
+ int status, int numFailed, int retryAfter, int redirectCount, long lastMod,
String pckg, String clazz, String extras, String cookies,
String userAgent, String referer, int totalBytes, int currentBytes, String etag,
boolean mediaScanned) {
this.id = id;
this.uri = uri;
- this.method = method;
- this.entity = entity;
this.noIntegrity = noIntegrity;
this.hint = hint;
this.filename = filename;
- this.otaUpdate = otaUpdate;
this.mimetype = mimetype;
this.destination = destination;
- this.noSystem = noSystem;
this.visibility = visibility;
this.control = control;
this.status = status;
this.numFailed = numFailed;
+ this.retryAfter = retryAfter;
+ this.redirectCount = redirectCount;
this.lastMod = lastMod;
this.pckg = pckg;
this.clazz = clazz;
@@ -109,14 +105,23 @@ public class DownloadInfo {
* be called when numFailed > 0.
*/
public long restartTime() {
- return lastMod + Constants.RETRY_FIRST_DELAY * 1000 * (1 << (numFailed - 1));
+ if (retryAfter > 0) {
+ return lastMod + retryAfter;
+ }
+ return lastMod +
+ Constants.RETRY_FIRST_DELAY *
+ (1000 + Helpers.rnd.nextInt(1001)) * (1 << (numFailed - 1));
}
/**
- * Returns whether this download should be started at the time when
- * it's first inserted in the database.
+ * Returns whether this download (which the download manager hasn't seen yet)
+ * should be started.
*/
public boolean isReadyToStart(long now) {
+ if (control == Downloads.CONTROL_PAUSED) {
+ // the download is paused, so it's not going to start
+ return false;
+ }
if (status == 0) {
// status hasn't been initialized yet, this is a new download
return true;
@@ -144,10 +149,18 @@ public class DownloadInfo {
}
/**
- * Returns whether this download should be restarted at the time when
- * it was already known by the download manager
+ * Returns whether this download (which the download manager has already seen
+ * and therefore potentially started) should be restarted.
+ *
+ * In a nutshell, this returns true if the download isn't already running
+ * but should be, and it can know whether the download is already running
+ * by checking the status.
*/
public boolean isReadyToRestart(long now) {
+ if (control == Downloads.CONTROL_PAUSED) {
+ // the download is paused, so it's not going to restart
+ return false;
+ }
if (status == 0) {
// download hadn't been initialized yet
return true;
@@ -182,4 +195,18 @@ public class DownloadInfo {
}
return false;
}
+
+ /**
+ * Returns whether this download is allowed to use the network.
+ */
+ public boolean canUseNetwork(boolean available, boolean roaming) {
+ if (!available) {
+ return false;
+ }
+ if (destination == Downloads.DESTINATION_CACHE_PARTITION_NOROAMING) {
+ return !roaming;
+ } else {
+ return true;
+ }
+ }
}
diff --git a/src/com/android/providers/downloads/DownloadNotification.java b/src/com/android/providers/downloads/DownloadNotification.java
index 38cd84f2..ed17ab7a 100644
--- a/src/com/android/providers/downloads/DownloadNotification.java
+++ b/src/com/android/providers/downloads/DownloadNotification.java
@@ -43,14 +43,14 @@ class DownloadNotification {
static final String LOGTAG = "DownloadNotification";
static final String WHERE_RUNNING =
- "(" + Downloads.STATUS + " >= 100) AND (" +
- Downloads.STATUS + " <= 199) AND (" +
- Downloads.VISIBILITY + " IS NULL OR " +
- Downloads.VISIBILITY + " == " + Downloads.VISIBILITY_VISIBLE + " OR " +
- Downloads.VISIBILITY + " == " + Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED + ")";
- static final String WHERE_COMPLETED =
- Downloads.STATUS + " >= 200 AND " +
- Downloads.VISIBILITY + " == " + Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
+ "(" + Downloads.STATUS + " >= '100') AND (" +
+ Downloads.STATUS + " <= '199') AND (" +
+ Downloads.VISIBILITY + " IS NULL OR " +
+ Downloads.VISIBILITY + " == '" + Downloads.VISIBILITY_VISIBLE + "' OR " +
+ Downloads.VISIBILITY + " == '" + Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED + "')";
+ static final String WHERE_COMPLETED =
+ Downloads.STATUS + " >= '200' AND " +
+ Downloads.VISIBILITY + " == '" + Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED + "'";
/**
@@ -114,7 +114,7 @@ class DownloadNotification {
Downloads.NOTIFICATION_PACKAGE,
Downloads.NOTIFICATION_CLASS,
Downloads.CURRENT_BYTES, Downloads.TOTAL_BYTES,
- Downloads.STATUS, Downloads.FILENAME
+ Downloads.STATUS, Downloads._DATA
},
WHERE_RUNNING, null, Downloads._ID);
@@ -216,7 +216,7 @@ class DownloadNotification {
Downloads.NOTIFICATION_PACKAGE,
Downloads.NOTIFICATION_CLASS,
Downloads.CURRENT_BYTES, Downloads.TOTAL_BYTES,
- Downloads.STATUS, Downloads.FILENAME,
+ Downloads.STATUS, Downloads._DATA,
Downloads.LAST_MODIFICATION, Downloads.DESTINATION
},
WHERE_COMPLETED, null, Downloads._ID);
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index c85c94a9..d86fdf97 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -22,7 +22,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.content.pm.PackageManager;
+import android.database.CrossProcessCursor;
import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.CursorWrapper;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
@@ -31,25 +34,29 @@ import android.net.Uri;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
-import android.provider.BaseColumns;
import android.provider.Downloads;
import android.util.Config;
import android.util.Log;
+import java.io.File;
import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashSet;
+
/**
* Allows application to interact with the download manager.
*/
public final class DownloadProvider extends ContentProvider {
- /** Tag used in logging */
- private static final String TAG = Constants.TAG;
-
/** Database filename */
private static final String DB_NAME = "downloads.db";
- /** Current database vesion */
- private static final int DB_VERSION = 31;
+ /** Current database version */
+ private static final int DB_VERSION = 100;
+ /** Database version from which upgrading is a nop */
+ private static final int DB_VERSION_NOP_UPGRADE_FROM = 31;
+ /** Database version to which upgrading is a nop */
+ private static final int DB_VERSION_NOP_UPGRADE_TO = 100;
/** Name of table in the database */
private static final String DB_TABLE = "downloads";
@@ -69,6 +76,31 @@ public final class DownloadProvider extends ContentProvider {
sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
}
+ private static final String[] sAppReadableColumnsArray = new String[] {
+ Downloads._ID,
+ Downloads.APP_DATA,
+ Downloads._DATA,
+ Downloads.MIMETYPE,
+ Downloads.VISIBILITY,
+ Downloads.CONTROL,
+ Downloads.STATUS,
+ Downloads.LAST_MODIFICATION,
+ Downloads.NOTIFICATION_PACKAGE,
+ Downloads.NOTIFICATION_CLASS,
+ Downloads.TOTAL_BYTES,
+ Downloads.CURRENT_BYTES,
+ Downloads.TITLE,
+ Downloads.DESCRIPTION
+ };
+
+ private static HashSet<String> sAppReadableColumnsSet;
+ static {
+ sAppReadableColumnsSet = new HashSet<String>();
+ for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
+ sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
+ }
+ }
+
/** The database that lies underneath this content provider */
private SQLiteOpenHelper mOpenHelper = null;
@@ -113,8 +145,16 @@ public final class DownloadProvider extends ContentProvider {
// to gracefully handle upgrades we should be careful about
// what to do on downgrades.
@Override
- public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
- Log.i(TAG, "Upgrading downloads database from version " + oldV + " to " + newV
+ public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
+ if (oldV == DB_VERSION_NOP_UPGRADE_FROM) {
+ if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op upgrade.
+ return;
+ }
+ // NOP_FROM and NOP_TO are identical, just in different codelines. Upgrading
+ // from NOP_FROM is the same as upgrading from NOP_TO.
+ oldV = DB_VERSION_NOP_UPGRADE_TO;
+ }
+ Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV + " to " + newV
+ ", which will destroy all old data");
dropTable(db);
createTable(db);
@@ -159,21 +199,21 @@ public final class DownloadProvider extends ContentProvider {
private void createTable(SQLiteDatabase db) {
try {
db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
- BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Downloads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Downloads.URI + " TEXT, " +
- Downloads.METHOD + " INTEGER, " +
- Downloads.ENTITY + " TEXT, " +
+ Constants.RETRY_AFTER___REDIRECT_COUNT + " INTEGER, " +
+ Downloads.APP_DATA + " TEXT, " +
Downloads.NO_INTEGRITY + " BOOLEAN, " +
Downloads.FILENAME_HINT + " TEXT, " +
- Downloads.OTA_UPDATE + " BOOLEAN, " +
- Downloads.FILENAME + " TEXT, " +
+ Constants.OTA_UPDATE + " BOOLEAN, " +
+ Downloads._DATA + " TEXT, " +
Downloads.MIMETYPE + " TEXT, " +
Downloads.DESTINATION + " INTEGER, " +
- Downloads.NO_SYSTEM_FILES + " BOOLEAN, " +
+ Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
Downloads.VISIBILITY + " INTEGER, " +
Downloads.CONTROL + " INTEGER, " +
Downloads.STATUS + " INTEGER, " +
- Downloads.FAILED_CONNECTIONS + " INTEGER, " +
+ Constants.FAILED_CONNECTIONS + " INTEGER, " +
Downloads.LAST_MODIFICATION + " BIGINT, " +
Downloads.NOTIFICATION_PACKAGE + " TEXT, " +
Downloads.NOTIFICATION_CLASS + " TEXT, " +
@@ -183,12 +223,12 @@ public final class DownloadProvider extends ContentProvider {
Downloads.REFERER + " TEXT, " +
Downloads.TOTAL_BYTES + " INTEGER, " +
Downloads.CURRENT_BYTES + " INTEGER, " +
- Downloads.ETAG + " TEXT, " +
- Downloads.UID + " INTEGER, " +
+ Constants.ETAG + " TEXT, " +
+ Constants.UID + " INTEGER, " +
Downloads.OTHER_UID + " INTEGER, " +
Downloads.TITLE + " TEXT, " +
Downloads.DESCRIPTION + " TEXT, " +
- Downloads.MEDIA_SCANNED + " BOOLEAN);");
+ Constants.MEDIA_SCANNED + " BOOLEAN);");
} catch (SQLException ex) {
Log.e(Constants.TAG, "couldn't create table in downloads database");
throw ex;
@@ -221,41 +261,73 @@ public final class DownloadProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
}
- boolean hasUID = values.containsKey(Downloads.UID);
- if (hasUID && Binder.getCallingUid() != 0) {
- values.remove(Downloads.UID);
- hasUID = false;
- }
- if (!hasUID) {
- values.put(Downloads.UID, Binder.getCallingUid());
+ ContentValues filteredValues = new ContentValues();
+
+ copyString(Downloads.URI, values, filteredValues);
+ copyString(Downloads.APP_DATA, values, filteredValues);
+ copyBoolean(Downloads.NO_INTEGRITY, values, filteredValues);
+ copyString(Downloads.FILENAME_HINT, values, filteredValues);
+ copyString(Downloads.MIMETYPE, values, filteredValues);
+ Integer i = values.getAsInteger(Downloads.DESTINATION);
+ if (i != null) {
+ if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
+ != PackageManager.PERMISSION_GRANTED
+ && i != Downloads.DESTINATION_EXTERNAL
+ && i != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
+ throw new SecurityException("unauthorized destination code");
+ }
+ filteredValues.put(Downloads.DESTINATION, i);
+ if (i != Downloads.DESTINATION_EXTERNAL &&
+ values.getAsInteger(Downloads.VISIBILITY) == null) {
+ filteredValues.put(Downloads.VISIBILITY, Downloads.VISIBILITY_HIDDEN);
+ }
}
- if (Constants.LOGVV) {
- Log.v(TAG, "initiating download with UID " + Binder.getCallingUid());
- if (values.containsKey(Downloads.OTHER_UID)) {
- Log.v(TAG, "other UID " + values.getAsInteger(Downloads.OTHER_UID));
+ copyInteger(Downloads.VISIBILITY, values, filteredValues);
+ copyInteger(Downloads.CONTROL, values, filteredValues);
+ filteredValues.put(Downloads.STATUS, Downloads.STATUS_PENDING);
+ filteredValues.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
+ String pckg = values.getAsString(Downloads.NOTIFICATION_PACKAGE);
+ String clazz = values.getAsString(Downloads.NOTIFICATION_CLASS);
+ if (pckg != null && clazz != null) {
+ int uid = Binder.getCallingUid();
+ try {
+ if (uid == 0 ||
+ getContext().getPackageManager().getApplicationInfo(pckg, 0).uid == uid) {
+ filteredValues.put(Downloads.NOTIFICATION_PACKAGE, pckg);
+ filteredValues.put(Downloads.NOTIFICATION_CLASS, clazz);
+ }
+ } catch (PackageManager.NameNotFoundException ex) {
+ /* ignored for now */
}
}
-
- if (values.containsKey(Downloads.LAST_MODIFICATION)) {
- values.remove(Downloads.LAST_MODIFICATION);
+ copyString(Downloads.NOTIFICATION_EXTRAS, values, filteredValues);
+ copyString(Downloads.COOKIE_DATA, values, filteredValues);
+ copyString(Downloads.USER_AGENT, values, filteredValues);
+ copyString(Downloads.REFERER, values, filteredValues);
+ if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
+ == PackageManager.PERMISSION_GRANTED) {
+ copyInteger(Downloads.OTHER_UID, values, filteredValues);
}
- values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
-
- if (values.containsKey(Downloads.STATUS)) {
- values.remove(Downloads.STATUS);
+ filteredValues.put(Constants.UID, Binder.getCallingUid());
+ if (Binder.getCallingUid() == 0) {
+ copyInteger(Constants.UID, values, filteredValues);
}
- values.put(Downloads.STATUS, Downloads.STATUS_PENDING);
+ copyString(Downloads.TITLE, values, filteredValues);
+ copyString(Downloads.DESCRIPTION, values, filteredValues);
- if (values.containsKey(Downloads.OTA_UPDATE)
- && getContext().checkCallingPermission(Constants.OTA_UPDATE_PERMISSION)
- != PackageManager.PERMISSION_GRANTED) {
- values.remove(Downloads.OTA_UPDATE);
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "initiating download with UID "
+ + filteredValues.getAsInteger(Constants.UID));
+ if (filteredValues.containsKey(Downloads.OTHER_UID)) {
+ Log.v(Constants.TAG, "other UID " +
+ filteredValues.getAsInteger(Downloads.OTHER_UID));
+ }
}
Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
- long rowID = db.insert(DB_TABLE, null, values);
+ long rowID = db.insert(DB_TABLE, null, filteredValues);
Uri ret = null;
@@ -265,7 +337,7 @@ public final class DownloadProvider extends ContentProvider {
context.getContentResolver().notifyChange(uri, null);
} else {
if (Config.LOGD) {
- Log.d(TAG, "couldn't insert into downloads database");
+ Log.d(Constants.TAG, "couldn't insert into downloads database");
}
}
@@ -276,9 +348,12 @@ public final class DownloadProvider extends ContentProvider {
* Starts a database query
*/
@Override
- public Cursor query(final Uri uri, final String[] projection,
+ public Cursor query(final Uri uri, String[] projection,
final String selection, final String[] selectionArgs,
final String sort) {
+
+ Helpers.validateSelection(selection, sAppReadableColumnsSet);
+
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
@@ -292,29 +367,37 @@ public final class DownloadProvider extends ContentProvider {
}
case DOWNLOADS_ID: {
qb.setTables(DB_TABLE);
- qb.appendWhere(BaseColumns._ID + "=");
+ qb.appendWhere(Downloads._ID + "=");
qb.appendWhere(uri.getPathSegments().get(1));
emptyWhere = false;
break;
}
default: {
if (Constants.LOGV) {
- Log.v(TAG, "querying unknown URI: " + uri);
+ Log.v(Constants.TAG, "querying unknown URI: " + uri);
}
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
- if (Binder.getCallingPid() != Process.myPid()
- && Binder.getCallingUid() != 0
- && getContext().checkCallingPermission(Constants.UI_PERMISSION)
- != PackageManager.PERMISSION_GRANTED) {
+ if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
if (!emptyWhere) {
qb.appendWhere(" AND ");
}
- qb.appendWhere("( " + Downloads.UID + "=" + Binder.getCallingUid() + " OR "
+ qb.appendWhere("( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
+ Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )");
emptyWhere = false;
+
+ if (projection == null) {
+ projection = sAppReadableColumnsArray;
+ } else {
+ for (int i = 0; i < projection.length; ++i) {
+ if (!sAppReadableColumnsSet.contains(projection[i])) {
+ throw new IllegalArgumentException(
+ "column " + projection[i] + " is not allowed in queries");
+ }
+ }
+ }
}
if (Constants.LOGVV) {
@@ -356,13 +439,17 @@ public final class DownloadProvider extends ContentProvider {
sb.append("sort is ");
sb.append(sort);
sb.append(".");
- Log.v(TAG, sb.toString());
+ Log.v(Constants.TAG, sb.toString());
}
Cursor ret = qb.query(db, projection, selection, selectionArgs,
null, null, sort);
if (ret != null) {
+ ret = new ReadOnlyCursorWrapper(ret);
+ }
+
+ if (ret != null) {
ret.setNotificationUri(getContext().getContentResolver(), uri);
if (Constants.LOGVV) {
Log.v(Constants.TAG,
@@ -370,7 +457,7 @@ public final class DownloadProvider extends ContentProvider {
}
} else {
if (Constants.LOGV) {
- Log.v(TAG, "query failed in downloads database");
+ Log.v(Constants.TAG, "query failed in downloads database");
}
}
@@ -383,12 +470,30 @@ public final class DownloadProvider extends ContentProvider {
@Override
public int update(final Uri uri, final ContentValues values,
final String where, final String[] whereArgs) {
+
+ Helpers.validateSelection(where, sAppReadableColumnsSet);
+
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
long rowId = 0;
- if (values.containsKey(Downloads.UID)) {
- values.remove(Downloads.UID);
+ boolean startService = false;
+
+ ContentValues filteredValues;
+ if (Binder.getCallingPid() != Process.myPid()) {
+ filteredValues = new ContentValues();
+ copyString(Downloads.APP_DATA, values, filteredValues);
+ copyInteger(Downloads.VISIBILITY, values, filteredValues);
+ Integer i = values.getAsInteger(Downloads.CONTROL);
+ if (i != null) {
+ filteredValues.put(Downloads.CONTROL, i);
+ startService = true;
+ }
+ copyInteger(Downloads.CONTROL, values, filteredValues);
+ copyString(Downloads.TITLE, values, filteredValues);
+ copyString(Downloads.DESCRIPTION, values, filteredValues);
+ } else {
+ filteredValues = values;
}
int match = sURIMatcher.match(uri);
switch (match) {
@@ -397,9 +502,9 @@ public final class DownloadProvider extends ContentProvider {
String myWhere;
if (where != null) {
if (match == DOWNLOADS) {
- myWhere = where;
+ myWhere = "( " + where + " )";
} else {
- myWhere = where + " AND ";
+ myWhere = "( " + where + " ) AND ";
}
} else {
myWhere = "";
@@ -407,26 +512,31 @@ public final class DownloadProvider extends ContentProvider {
if (match == DOWNLOADS_ID) {
String segment = uri.getPathSegments().get(1);
rowId = Long.parseLong(segment);
- myWhere += Downloads._ID + " = " + rowId;
+ myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
}
- if (Binder.getCallingPid() != Process.myPid()
- && Binder.getCallingUid() != 0
- && getContext().checkCallingPermission(Constants.UI_PERMISSION)
- != PackageManager.PERMISSION_GRANTED) {
- myWhere += " AND ( " + Downloads.UID + "=" + Binder.getCallingUid() + " OR "
+ if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
+ myWhere += " AND ( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
+ Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )";
}
- count = db.update(DB_TABLE, values, myWhere, whereArgs);
+ if (filteredValues.size() > 0) {
+ count = db.update(DB_TABLE, filteredValues, myWhere, whereArgs);
+ } else {
+ count = 0;
+ }
break;
}
default: {
if (Config.LOGD) {
- Log.d(TAG, "updating unknown/invalid URI: " + uri);
+ Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
}
throw new UnsupportedOperationException("Cannot update URI: " + uri);
}
}
getContext().getContentResolver().notifyChange(uri, null);
+ if (startService) {
+ Context context = getContext();
+ context.startService(new Intent(context, DownloadService.class));
+ }
return count;
}
@@ -436,6 +546,9 @@ public final class DownloadProvider extends ContentProvider {
@Override
public int delete(final Uri uri, final String where,
final String[] whereArgs) {
+
+ Helpers.validateSelection(where, sAppReadableColumnsSet);
+
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
int match = sURIMatcher.match(uri);
@@ -445,9 +558,9 @@ public final class DownloadProvider extends ContentProvider {
String myWhere;
if (where != null) {
if (match == DOWNLOADS) {
- myWhere = where;
+ myWhere = "( " + where + " )";
} else {
- myWhere = where + " AND ";
+ myWhere = "( " + where + " ) AND ";
}
} else {
myWhere = "";
@@ -455,13 +568,10 @@ public final class DownloadProvider extends ContentProvider {
if (match == DOWNLOADS_ID) {
String segment = uri.getPathSegments().get(1);
long rowId = Long.parseLong(segment);
- myWhere += Downloads._ID + " = " + rowId;
+ myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
}
- if (Binder.getCallingPid() != Process.myPid()
- && Binder.getCallingUid() != 0
- && getContext().checkCallingPermission(Constants.UI_PERMISSION)
- != PackageManager.PERMISSION_GRANTED) {
- myWhere += " AND ( " + Downloads.UID + "=" + Binder.getCallingUid() + " OR "
+ if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
+ myWhere += " AND ( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
+ Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )";
}
count = db.delete(DB_TABLE, myWhere, whereArgs);
@@ -469,7 +579,7 @@ public final class DownloadProvider extends ContentProvider {
}
default: {
if (Config.LOGD) {
- Log.d(TAG, "deleting unknown/invalid URI: " + uri);
+ Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
}
throw new UnsupportedOperationException("Cannot delete URI: " + uri);
}
@@ -485,42 +595,75 @@ public final class DownloadProvider extends ContentProvider {
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
if (Constants.LOGVV) {
- Log.v(TAG, "openFile uri: " + uri + ", mode: " + mode
+ Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
+ ", uid: " + Binder.getCallingUid());
Cursor cursor = query(Downloads.CONTENT_URI, new String[] { "_id" }, null, null, "_id");
if (cursor == null) {
- Log.v(TAG, "null cursor in openFile");
+ Log.v(Constants.TAG, "null cursor in openFile");
} else {
if (!cursor.moveToFirst()) {
- Log.v(TAG, "empty cursor in openFile");
+ Log.v(Constants.TAG, "empty cursor in openFile");
} else {
do {
- Log.v(TAG, "row " + cursor.getInt(0) + " available");
+ Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
} while(cursor.moveToNext());
}
cursor.close();
}
cursor = query(uri, new String[] { "_data" }, null, null, null);
if (cursor == null) {
- Log.v(TAG, "null cursor in openFile");
+ Log.v(Constants.TAG, "null cursor in openFile");
} else {
if (!cursor.moveToFirst()) {
- Log.v(TAG, "empty cursor in openFile");
+ Log.v(Constants.TAG, "empty cursor in openFile");
} else {
String filename = cursor.getString(0);
- Log.v(TAG, "filename in openFile: " + filename);
+ Log.v(Constants.TAG, "filename in openFile: " + filename);
if (new java.io.File(filename).isFile()) {
- Log.v(TAG, "file exists in openFile");
+ Log.v(Constants.TAG, "file exists in openFile");
}
}
cursor.close();
}
}
- ParcelFileDescriptor ret = openFileHelper(uri, mode);
+
+ // This logic is mostly copied form openFileHelper. If openFileHelper eventually
+ // gets split into small bits (to extract the filename and the modebits),
+ // this code could use the separate bits and be deeply simplified.
+ Cursor c = query(uri, new String[]{"_data"}, null, null, null);
+ int count = (c != null) ? c.getCount() : 0;
+ if (count != 1) {
+ // If there is not exactly one result, throw an appropriate exception.
+ if (c != null) {
+ c.close();
+ }
+ if (count == 0) {
+ throw new FileNotFoundException("No entry for " + uri);
+ }
+ throw new FileNotFoundException("Multiple items at " + uri);
+ }
+
+ c.moveToFirst();
+ String path = c.getString(0);
+ c.close();
+ if (path == null) {
+ throw new FileNotFoundException("No filename found.");
+ }
+ if (!Helpers.isFilenameValid(path)) {
+ throw new FileNotFoundException("Invalid filename.");
+ }
+
+ if (!"r".equals(mode)) {
+ throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
+ }
+ ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
+ ParcelFileDescriptor.MODE_READ_ONLY);
+
if (ret == null) {
- if (Config.LOGD) {
- Log.d(TAG, "couldn't open file");
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "couldn't open file");
}
+ throw new FileNotFoundException("couldn't open file");
} else {
ContentValues values = new ContentValues();
values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
@@ -529,4 +672,54 @@ public final class DownloadProvider extends ContentProvider {
return ret;
}
+ private static final void copyInteger(String key, ContentValues from, ContentValues to) {
+ Integer i = from.getAsInteger(key);
+ if (i != null) {
+ to.put(key, i);
+ }
+ }
+
+ private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
+ Boolean b = from.getAsBoolean(key);
+ if (b != null) {
+ to.put(key, b);
+ }
+ }
+
+ private static final void copyString(String key, ContentValues from, ContentValues to) {
+ String s = from.getAsString(key);
+ if (s != null) {
+ to.put(key, s);
+ }
+ }
+
+ private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor {
+ public ReadOnlyCursorWrapper(Cursor cursor) {
+ super(cursor);
+ mCursor = (CrossProcessCursor) cursor;
+ }
+
+ public boolean deleteRow() {
+ throw new SecurityException("Download manager cursors are read-only");
+ }
+
+ public boolean commitUpdates() {
+ throw new SecurityException("Download manager cursors are read-only");
+ }
+
+ public void fillWindow(int pos, CursorWindow window) {
+ mCursor.fillWindow(pos, window);
+ }
+
+ public CursorWindow getWindow() {
+ return mCursor.getWindow();
+ }
+
+ public boolean onMove(int oldPosition, int newPosition) {
+ return mCursor.onMove(oldPosition, newPosition);
+ }
+
+ private CrossProcessCursor mCursor;
+ }
+
}
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
index e5bc4e1f..03a37186 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -20,6 +20,7 @@ import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -40,18 +41,15 @@ import java.util.List;
*/
public class DownloadReceiver extends BroadcastReceiver {
- /** Tag used for debugging/logging */
- public static final String TAG = Constants.TAG;
-
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
if (Constants.LOGVV) {
- Log.v(TAG, "Receiver onBoot");
+ Log.v(Constants.TAG, "Receiver onBoot");
}
context.startService(new Intent(context, DownloadService.class));
} else if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
if (Constants.LOGVV) {
- Log.v(TAG, "Receiver onConnectivity");
+ Log.v(Constants.TAG, "Receiver onConnectivity");
}
NetworkInfo info = (NetworkInfo)
intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
@@ -60,7 +58,7 @@ public class DownloadReceiver extends BroadcastReceiver {
}
} else if (intent.getAction().equals(Constants.ACTION_RETRY)) {
if (Constants.LOGVV) {
- Log.v(TAG, "Receiver retry");
+ Log.v(Constants.TAG, "Receiver retry");
}
context.startService(new Intent(context, DownloadService.class));
} else if (intent.getAction().equals(Constants.ACTION_OPEN)
@@ -75,7 +73,6 @@ public class DownloadReceiver extends BroadcastReceiver {
Cursor cursor = context.getContentResolver().query(
intent.getData(), null, null, null, null);
if (cursor != null) {
- boolean mustCommit = false;
if (cursor.moveToFirst()) {
int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
int status = cursor.getInt(statusColumn);
@@ -83,12 +80,13 @@ public class DownloadReceiver extends BroadcastReceiver {
int visibility = cursor.getInt(visibilityColumn);
if (Downloads.isStatusCompleted(status)
&& visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
- cursor.updateInt(visibilityColumn, Downloads.VISIBILITY_VISIBLE);
- mustCommit = true;
+ ContentValues values = new ContentValues();
+ values.put(Downloads.VISIBILITY, Downloads.VISIBILITY_VISIBLE);
+ context.getContentResolver().update(intent.getData(), values, null, null);
}
if (intent.getAction().equals(Constants.ACTION_OPEN)) {
- int filenameColumn = cursor.getColumnIndexOrThrow(Downloads.FILENAME);
+ int filenameColumn = cursor.getColumnIndexOrThrow(Downloads._DATA);
int mimetypeColumn = cursor.getColumnIndexOrThrow(Downloads.MIMETYPE);
String filename = cursor.getString(filenameColumn);
String mimetype = cursor.getString(mimetypeColumn);
@@ -128,11 +126,6 @@ public class DownloadReceiver extends BroadcastReceiver {
}
}
}
- if (mustCommit) {
- if (!cursor.commitUpdates()) {
- Log.e(Constants.TAG, "commitUpdate failed in onReceive/OPEN-LIST");
- }
- }
cursor.close();
}
NotificationManager notMgr = (NotificationManager) context
@@ -154,10 +147,9 @@ public class DownloadReceiver extends BroadcastReceiver {
int visibility = cursor.getInt(visibilityColumn);
if (Downloads.isStatusCompleted(status)
&& visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
- cursor.updateInt(visibilityColumn, Downloads.VISIBILITY_VISIBLE);
- if (!cursor.commitUpdates()) {
- Log.e(Constants.TAG, "commitUpdate failed in onReceive/HIDE");
- }
+ ContentValues values = new ContentValues();
+ values.put(Downloads.VISIBILITY, Downloads.VISIBILITY_VISIBLE);
+ context.getContentResolver().update(intent.getData(), values, null, null);
}
}
cursor.close();
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index 0d3650c0..d4b5f1e6 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -40,7 +40,6 @@ import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
-import android.provider.BaseColumns;
import android.provider.Downloads;
import android.util.Config;
import android.util.Log;
@@ -59,9 +58,6 @@ public class DownloadService extends Service {
/* ------------ Constants ------------ */
- /** Tag used for debugging/logging */
- private static final String TAG = Constants.TAG;
-
/* ------------ Members ------------ */
/** Observer to get notified when the content observer's data changes */
@@ -130,7 +126,7 @@ public class DownloadService extends Service {
*/
public void onChange(final boolean selfChange) {
if (Constants.LOGVV) {
- Log.v(TAG, "Service ContentObserver received notification");
+ Log.v(Constants.TAG, "Service ContentObserver received notification");
}
updateFromProvider();
}
@@ -144,7 +140,7 @@ public class DownloadService extends Service {
public class MediaScannerConnection implements ServiceConnection {
public void onServiceConnected(ComponentName className, IBinder service) {
if (Constants.LOGVV) {
- Log.v(TAG, "Connected to Media Scanner");
+ Log.v(Constants.TAG, "Connected to Media Scanner");
}
mMediaScannerConnecting = false;
synchronized (DownloadService.this) {
@@ -160,7 +156,7 @@ public class DownloadService extends Service {
if (mMediaScannerService != null) {
mMediaScannerService = null;
if (Constants.LOGVV) {
- Log.v(TAG, "Disconnecting from Media Scanner");
+ Log.v(Constants.TAG, "Disconnecting from Media Scanner");
}
try {
unbindService(this);
@@ -201,7 +197,7 @@ public class DownloadService extends Service {
public void onCreate() {
super.onCreate();
if (Constants.LOGVV) {
- Log.v(TAG, "Service onCreate");
+ Log.v(Constants.TAG, "Service onCreate");
}
mDownloads = Lists.newArrayList();
@@ -229,7 +225,7 @@ public class DownloadService extends Service {
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
if (Constants.LOGVV) {
- Log.v(TAG, "Service onStart");
+ Log.v(Constants.TAG, "Service onStart");
}
updateFromProvider();
@@ -241,7 +237,7 @@ public class DownloadService extends Service {
public void onDestroy() {
getContentResolver().unregisterContentObserver(mObserver);
if (Constants.LOGVV) {
- Log.v(TAG, "Service onDestroy");
+ Log.v(Constants.TAG, "Service onDestroy");
}
super.onDestroy();
}
@@ -308,10 +304,11 @@ public class DownloadService extends Service {
pendingUpdate = false;
}
boolean networkAvailable = Helpers.isNetworkAvailable(DownloadService.this);
+ boolean networkRoaming = Helpers.isNetworkRoaming(DownloadService.this);
long now = System.currentTimeMillis();
Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
- null, null, null, BaseColumns._ID);
+ null, null, null, Downloads._ID);
if (cursor == null) {
return;
@@ -327,7 +324,7 @@ public class DownloadService extends Service {
boolean isAfterLast = cursor.isAfterLast();
- int idColumn = cursor.getColumnIndexOrThrow(BaseColumns._ID);
+ int idColumn = cursor.getColumnIndexOrThrow(Downloads._ID);
/*
* Walk the cursor and the local array to keep them in sync. The key
@@ -352,7 +349,8 @@ public class DownloadService extends Service {
// stuff in the local array, which can only be junk
if (Constants.LOGVV) {
int arrayId = ((DownloadInfo) mDownloads.get(arrayPos)).id;
- Log.v(TAG, "Array update: trimming " + arrayId + " @ " + arrayPos);
+ Log.v(Constants.TAG, "Array update: trimming " +
+ arrayId + " @ " + arrayPos);
}
if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
scanFile(null, arrayPos);
@@ -362,9 +360,10 @@ public class DownloadService extends Service {
int id = cursor.getInt(idColumn);
if (arrayPos == mDownloads.size()) {
- insertDownload(cursor, arrayPos, networkAvailable, now);
+ insertDownload(cursor, arrayPos, networkAvailable, networkRoaming, now);
if (Constants.LOGVV) {
- Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
+ Log.v(Constants.TAG, "Array update: inserting " +
+ id + " @ " + arrayPos);
}
if (shouldScanFile(arrayPos)
&& (!mediaScannerConnected() || !scanFile(cursor, arrayPos))) {
@@ -389,7 +388,7 @@ public class DownloadService extends Service {
if (arrayId < id) {
// The array entry isn't in the cursor
if (Constants.LOGVV) {
- Log.v(TAG, "Array update: removing " + arrayId
+ Log.v(Constants.TAG, "Array update: removing " + arrayId
+ " @ " + arrayPos);
}
if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
@@ -398,7 +397,9 @@ public class DownloadService extends Service {
deleteDownload(arrayPos); // this advances in the array
} else if (arrayId == id) {
// This cursor row already exists in the stored array
- updateDownload(cursor, arrayPos, networkAvailable, now);
+ updateDownload(
+ cursor, arrayPos,
+ networkAvailable, networkRoaming, now);
if (shouldScanFile(arrayPos)
&& (!mediaScannerConnected()
|| !scanFile(cursor, arrayPos))) {
@@ -420,9 +421,12 @@ public class DownloadService extends Service {
} else {
// This cursor entry didn't exist in the stored array
if (Constants.LOGVV) {
- Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
+ Log.v(Constants.TAG, "Array update: appending " +
+ id + " @ " + arrayPos);
}
- insertDownload(cursor, arrayPos, networkAvailable, now);
+ insertDownload(
+ cursor, arrayPos,
+ networkAvailable, networkRoaming, now);
if (shouldScanFile(arrayPos)
&& (!mediaScannerConnected()
|| !scanFile(cursor, arrayPos))) {
@@ -460,9 +464,6 @@ public class DownloadService extends Service {
mMediaScannerConnection.disconnectMediaScanner();
}
- if (!cursor.commitUpdates()) {
- Log.e(Constants.TAG, "commitUpdates failed in updateFromProvider");
- }
cursor.close();
}
}
@@ -490,7 +491,7 @@ public class DownloadService extends Service {
}
Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
- new String[] { Downloads.FILENAME }, null, null, null);
+ new String[] { Downloads._DATA }, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
@@ -515,16 +516,24 @@ public class DownloadService extends Service {
private void trimDatabase() {
Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
new String[] { Downloads._ID },
- Downloads.STATUS + " >= 200", null,
+ Downloads.STATUS + " >= '200'", null,
Downloads.LAST_MODIFICATION);
if (cursor == null) {
// This isn't good - if we can't do basic queries in our database, nothing's gonna work
- Log.e(TAG, "null cursor in trimDatabase");
+ Log.e(Constants.TAG, "null cursor in trimDatabase");
return;
}
if (cursor.moveToFirst()) {
- while (cursor.getCount() > Constants.MAX_DOWNLOADS) {
- cursor.deleteRow();
+ int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
+ int columnId = cursor.getColumnIndexOrThrow(Downloads._ID);
+ while (numDelete > 0) {
+ getContentResolver().delete(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI, cursor.getLong(columnId)),
+ null, null);
+ if (!cursor.moveToNext()) {
+ break;
+ }
+ numDelete--;
}
}
cursor.close();
@@ -534,25 +543,27 @@ public class DownloadService extends Service {
* Keeps a local copy of the info about a download, and initiates the
* download if appropriate.
*/
- private void insertDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
+ private void insertDownload(
+ Cursor cursor, int arrayPos,
+ boolean networkAvailable, boolean networkRoaming, long now) {
int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
- int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
+ int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
+ int retryRedirect =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER___REDIRECT_COUNT));
DownloadInfo info = new DownloadInfo(
cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(Downloads.URI)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.METHOD)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.ENTITY)),
cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1,
cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME_HINT)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.OTA_UPDATE)) == 1,
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads._DATA)),
cursor.getString(cursor.getColumnIndexOrThrow(Downloads.MIMETYPE)),
cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_SYSTEM_FILES)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY)),
cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL)),
cursor.getInt(statusColumn),
cursor.getInt(failedColumn),
+ retryRedirect & 0xfffffff,
+ retryRedirect >> 28,
cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION)),
cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_PACKAGE)),
cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_CLASS)),
@@ -562,36 +573,34 @@ public class DownloadService extends Service {
cursor.getString(cursor.getColumnIndexOrThrow(Downloads.REFERER)),
cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.TOTAL_BYTES)),
cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CURRENT_BYTES)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.ETAG)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1);
+ cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1);
if (Constants.LOGVV) {
- Log.v(TAG, "Service adding new entry");
- Log.v(TAG, "ID : " + info.id);
- Log.v(TAG, "URI : " + ((info.uri != null) ? "yes" : "no"));
- Log.v(TAG, "METHOD : " + info.method);
- Log.v(TAG, "ENTITY : " + ((info.entity != null) ? "yes" : "no"));
- Log.v(TAG, "NO_INTEG: " + info.noIntegrity);
- Log.v(TAG, "HINT : " + info.hint);
- Log.v(TAG, "FILENAME: " + info.filename);
- Log.v(TAG, "SYSIMAGE: " + info.otaUpdate);
- Log.v(TAG, "MIMETYPE: " + info.mimetype);
- Log.v(TAG, "DESTINAT: " + info.destination);
- Log.v(TAG, "NO_SYSTE: " + info.noSystem);
- Log.v(TAG, "VISIBILI: " + info.visibility);
- Log.v(TAG, "CONTROL : " + info.control);
- Log.v(TAG, "STATUS : " + info.status);
- Log.v(TAG, "FAILED_C: " + info.numFailed);
- Log.v(TAG, "LAST_MOD: " + info.lastMod);
- Log.v(TAG, "PACKAGE : " + info.pckg);
- Log.v(TAG, "CLASS : " + info.clazz);
- Log.v(TAG, "COOKIES : " + ((info.cookies != null) ? "yes" : "no"));
- Log.v(TAG, "AGENT : " + info.userAgent);
- Log.v(TAG, "REFERER : " + ((info.referer != null) ? "yes" : "no"));
- Log.v(TAG, "TOTAL : " + info.totalBytes);
- Log.v(TAG, "CURRENT : " + info.currentBytes);
- Log.v(TAG, "ETAG : " + info.etag);
- Log.v(TAG, "SCANNED : " + info.mediaScanned);
+ Log.v(Constants.TAG, "Service adding new entry");
+ Log.v(Constants.TAG, "ID : " + info.id);
+ Log.v(Constants.TAG, "URI : " + ((info.uri != null) ? "yes" : "no"));
+ Log.v(Constants.TAG, "NO_INTEG: " + info.noIntegrity);
+ Log.v(Constants.TAG, "HINT : " + info.hint);
+ Log.v(Constants.TAG, "FILENAME: " + info.filename);
+ Log.v(Constants.TAG, "MIMETYPE: " + info.mimetype);
+ Log.v(Constants.TAG, "DESTINAT: " + info.destination);
+ Log.v(Constants.TAG, "VISIBILI: " + info.visibility);
+ Log.v(Constants.TAG, "CONTROL : " + info.control);
+ Log.v(Constants.TAG, "STATUS : " + info.status);
+ Log.v(Constants.TAG, "FAILED_C: " + info.numFailed);
+ Log.v(Constants.TAG, "RETRY_AF: " + info.retryAfter);
+ Log.v(Constants.TAG, "REDIRECT: " + info.redirectCount);
+ Log.v(Constants.TAG, "LAST_MOD: " + info.lastMod);
+ Log.v(Constants.TAG, "PACKAGE : " + info.pckg);
+ Log.v(Constants.TAG, "CLASS : " + info.clazz);
+ Log.v(Constants.TAG, "COOKIES : " + ((info.cookies != null) ? "yes" : "no"));
+ Log.v(Constants.TAG, "AGENT : " + info.userAgent);
+ Log.v(Constants.TAG, "REFERER : " + ((info.referer != null) ? "yes" : "no"));
+ Log.v(Constants.TAG, "TOTAL : " + info.totalBytes);
+ Log.v(Constants.TAG, "CURRENT : " + info.currentBytes);
+ Log.v(Constants.TAG, "ETAG : " + info.etag);
+ Log.v(Constants.TAG, "SCANNED : " + info.mediaScanned);
}
mDownloads.add(arrayPos, info);
@@ -616,29 +625,28 @@ public class DownloadService extends Service {
mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mimetype);
List<ResolveInfo> list = getPackageManager().queryIntentActivities(mimetypeIntent,
PackageManager.MATCH_DEFAULT_ONLY);
- //Log.i(TAG, "*** QUERY " + mimetypeIntent + ": " + list);
+ //Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
- if (list.size() == 0
- || (info.noSystem && info.mimetype.equalsIgnoreCase(Constants.MIMETYPE_APK))) {
+ if (list.size() == 0) {
if (Config.LOGD) {
Log.d(Constants.TAG, "no application to handle MIME type " + info.mimetype);
}
info.status = Downloads.STATUS_NOT_ACCEPTABLE;
- cursor.updateInt(statusColumn, Downloads.STATUS_NOT_ACCEPTABLE);
- Uri uri = Uri.parse(Downloads.CONTENT_URI + "/" + info.id);
- Intent intent = new Intent(Downloads.DOWNLOAD_COMPLETED_ACTION);
- intent.setData(uri);
- sendBroadcast(intent, "android.permission.ACCESS_DOWNLOAD_DATA");
+ Uri uri = ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id);
+ ContentValues values = new ContentValues();
+ values.put(Downloads.STATUS, Downloads.STATUS_NOT_ACCEPTABLE);
+ getContentResolver().update(uri, values, null, null);
info.sendIntentIfRequested(uri, this);
return;
}
}
- if (networkAvailable) {
+ if (info.canUseNetwork(networkAvailable, networkRoaming)) {
if (info.isReadyToStart(now)) {
if (Constants.LOGV) {
- Log.v(TAG, "Service spawning thread to handle new download " + info.id);
+ Log.v(Constants.TAG, "Service spawning thread to handle new download " +
+ info.id);
}
if (info.hasActiveThread) {
throw new IllegalStateException("Multiple threads on same download on insert");
@@ -660,7 +668,10 @@ public class DownloadService extends Service {
|| info.status == Downloads.STATUS_PENDING
|| info.status == Downloads.STATUS_RUNNING) {
info.status = Downloads.STATUS_RUNNING_PAUSED;
- cursor.updateInt(statusColumn, Downloads.STATUS_RUNNING_PAUSED);
+ Uri uri = ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id);
+ ContentValues values = new ContentValues();
+ values.put(Downloads.STATUS, Downloads.STATUS_RUNNING_PAUSED);
+ getContentResolver().update(uri, values, null, null);
}
}
}
@@ -668,23 +679,20 @@ public class DownloadService extends Service {
/**
* Updates the local copy of the info about a download.
*/
- private void updateDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
+ private void updateDownload(
+ Cursor cursor, int arrayPos,
+ boolean networkAvailable, boolean networkRoaming, long now) {
DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
- int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
+ int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
info.id = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID));
info.uri = stringFromCursor(info.uri, cursor, Downloads.URI);
- info.method = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.METHOD));
- info.entity = stringFromCursor(info.entity, cursor, Downloads.ENTITY);
info.noIntegrity =
cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1;
info.hint = stringFromCursor(info.hint, cursor, Downloads.FILENAME_HINT);
- info.filename = stringFromCursor(info.filename, cursor, Downloads.FILENAME);
- info.otaUpdate = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.OTA_UPDATE)) == 1;
+ info.filename = stringFromCursor(info.filename, cursor, Downloads._DATA);
info.mimetype = stringFromCursor(info.mimetype, cursor, Downloads.MIMETYPE);
info.destination = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION));
- info.noSystem =
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_SYSTEM_FILES)) == 1;
int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY));
if (info.visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& newVisibility != Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
@@ -692,13 +700,19 @@ public class DownloadService extends Service {
mNotifier.mNotificationMgr.cancel(info.id);
}
info.visibility = newVisibility;
- info.control = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL));
+ synchronized(info) {
+ info.control = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL));
+ }
int newStatus = cursor.getInt(statusColumn);
if (!Downloads.isStatusCompleted(info.status) && Downloads.isStatusCompleted(newStatus)) {
mNotifier.mNotificationMgr.cancel(info.id);
}
info.status = newStatus;
info.numFailed = cursor.getInt(failedColumn);
+ int retryRedirect =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER___REDIRECT_COUNT));
+ info.retryAfter = retryRedirect & 0xfffffff;
+ info.redirectCount = retryRedirect >> 28;
info.lastMod = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION));
info.pckg = stringFromCursor(info.pckg, cursor, Downloads.NOTIFICATION_PACKAGE);
info.clazz = stringFromCursor(info.clazz, cursor, Downloads.NOTIFICATION_CLASS);
@@ -707,14 +721,15 @@ public class DownloadService extends Service {
info.referer = stringFromCursor(info.referer, cursor, Downloads.REFERER);
info.totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.TOTAL_BYTES));
info.currentBytes = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CURRENT_BYTES));
- info.etag = stringFromCursor(info.etag, cursor, Downloads.ETAG);
+ info.etag = stringFromCursor(info.etag, cursor, Constants.ETAG);
info.mediaScanned =
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1;
+ cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
- if (networkAvailable) {
+ if (info.canUseNetwork(networkAvailable, networkRoaming)) {
if (info.isReadyToRestart(now)) {
if (Constants.LOGV) {
- Log.v(TAG, "Service spawning thread to handle updated download " + info.id);
+ Log.v(Constants.TAG, "Service spawning thread to handle updated download " +
+ info.id);
}
if (info.hasActiveThread) {
throw new IllegalStateException("Multiple threads on same download on update");
@@ -839,16 +854,21 @@ public class DownloadService extends Service {
if (mMediaScannerService != null) {
try {
if (Constants.LOGV) {
- Log.v(TAG, "Scanning file " + info.filename);
+ Log.v(Constants.TAG, "Scanning file " + info.filename);
}
mMediaScannerService.scanFile(info.filename, info.mimetype);
if (cursor != null) {
- cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED), 1);
+ ContentValues values = new ContentValues();
+ values.put(Constants.MEDIA_SCANNED, 1);
+ getContentResolver().update(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI,
+ cursor.getLong(cursor.getColumnIndexOrThrow(Downloads._ID))),
+ values, null, null);
}
return true;
} catch (RemoteException e) {
if (Config.LOGD) {
- Log.d(TAG, "Failed to scan file " + info.filename);
+ Log.d(Constants.TAG, "Failed to scan file " + info.filename);
}
}
}
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 66417b3e..923e36d1 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -25,6 +25,7 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
+import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -46,15 +47,13 @@ import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
+import java.net.URI;
/**
* Runs an actual download
*/
public class DownloadThread extends Thread {
- /** Tag used for debugging/logging */
- private static final String TAG = Constants.TAG;
-
private Context mContext;
private DownloadInfo mInfo;
@@ -84,6 +83,9 @@ public class DownloadThread extends Thread {
int finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
boolean countRetry = false;
+ int retryAfter = 0;
+ int redirectCount = mInfo.redirectCount;
+ String newUri = null;
boolean gotData = false;
String filename = null;
String mimeType = mInfo.mimetype;
@@ -106,30 +108,38 @@ public class DownloadThread extends Thread {
int bytesSoFar = 0;
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
- wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
wakeLock.acquire();
- if (mInfo.filename != null) {
+ filename = mInfo.filename;
+ if (filename != null) {
+ if (!Helpers.isFilenameValid(filename)) {
+ finalStatus = Downloads.STATUS_FILE_ERROR;
+ notifyDownloadCompleted(
+ finalStatus, false, 0, 0, false, filename, null, mInfo.mimetype);
+ return;
+ }
// We're resuming a download that got interrupted
- File f = new File(mInfo.filename);
+ File f = new File(filename);
if (f.exists()) {
long fileLength = f.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from scratch
f.delete();
+ filename = null;
} else if (mInfo.etag == null && !mInfo.noIntegrity) {
// Tough luck, that's not a resumable download
if (Config.LOGD) {
- Log.d(TAG, "can't resume interrupted non-resumable download");
+ Log.d(Constants.TAG,
+ "can't resume interrupted non-resumable download");
}
f.delete();
finalStatus = Downloads.STATUS_PRECONDITION_FAILED;
notifyDownloadCompleted(
- finalStatus, false, false, mInfo.filename, mInfo.mimetype);
+ finalStatus, false, 0, 0, false, filename, null, mInfo.mimetype);
return;
} else {
// All right, we'll be able to resume this download
- filename = mInfo.filename;
stream = new FileOutputStream(filename, true);
bytesSoFar = (int) fileLength;
if (mInfo.totalBytes != -1) {
@@ -171,59 +181,38 @@ public class DownloadThread extends Thread {
http_request_loop:
while (true) {
// Prepares the request and fires it.
- HttpUriRequest requestU;
- AbortableHttpRequest requestA;
- if (mInfo.method == Downloads.METHOD_POST) {
- HttpPost request = new HttpPost(mInfo.uri);
- if (mInfo.entity != null) {
- try {
- request.setEntity(new StringEntity(mInfo.entity));
- } catch (UnsupportedEncodingException ex) {
- if (Config.LOGD) {
- Log.d(TAG, "unsupported encoding for POST entity : " + ex);
- }
- finalStatus = Downloads.STATUS_BAD_REQUEST;
- break http_request_loop;
- }
- }
- requestU = request;
- requestA = request;
- } else {
- HttpGet request = new HttpGet(mInfo.uri);
- requestU = request;
- requestA = request;
- }
+ HttpGet request = new HttpGet(mInfo.uri);
if (Constants.LOGV) {
- Log.v(TAG, "initiating download for " + mInfo.uri);
+ Log.v(Constants.TAG, "initiating download for " + mInfo.uri);
}
if (mInfo.cookies != null) {
- requestU.addHeader("Cookie", mInfo.cookies);
+ request.addHeader("Cookie", mInfo.cookies);
}
if (mInfo.referer != null) {
- requestU.addHeader("Referer", mInfo.referer);
+ request.addHeader("Referer", mInfo.referer);
}
if (continuingDownload) {
if (headerETag != null) {
- requestU.addHeader("If-Match", headerETag);
+ request.addHeader("If-Match", headerETag);
}
- requestU.addHeader("Range", "bytes=" + bytesSoFar + "-");
+ request.addHeader("Range", "bytes=" + bytesSoFar + "-");
}
HttpResponse response;
try {
- response = client.execute(requestU);
+ response = client.execute(request);
} catch (IllegalArgumentException ex) {
if (Constants.LOGV) {
- Log.d(TAG, "Arg exception trying to execute request for " + mInfo.uri +
- " : " + ex);
+ Log.d(Constants.TAG, "Arg exception trying to execute request for " +
+ mInfo.uri + " : " + ex);
} else if (Config.LOGD) {
- Log.d(TAG, "Arg exception trying to execute request for " + mInfo.id +
- " : " + ex);
+ Log.d(Constants.TAG, "Arg exception trying to execute request for " +
+ mInfo.id + " : " + ex);
}
finalStatus = Downloads.STATUS_BAD_REQUEST;
- requestA.abort();
+ request.abort();
break http_request_loop;
} catch (IOException ex) {
if (!Helpers.isNetworkAvailable(mContext)) {
@@ -233,25 +222,87 @@ http_request_loop:
countRetry = true;
} else {
if (Constants.LOGV) {
- Log.d(TAG, "IOException trying to execute request for " + mInfo.uri +
- " : " + ex);
+ Log.d(Constants.TAG, "IOException trying to execute request for " +
+ mInfo.uri + " : " + ex);
} else if (Config.LOGD) {
- Log.d(TAG, "IOException trying to execute request for " + mInfo.id +
- " : " + ex);
+ Log.d(Constants.TAG, "IOException trying to execute request for " +
+ mInfo.id + " : " + ex);
}
finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
}
- requestA.abort();
+ request.abort();
break http_request_loop;
}
int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode == 503 && mInfo.numFailed < Constants.MAX_RETRIES) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "got HTTP response code 503");
+ }
+ finalStatus = Downloads.STATUS_RUNNING_PAUSED;
+ countRetry = true;
+ Header header = response.getFirstHeader("Retry-After");
+ if (header != null) {
+ try {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Retry-After :" + header.getValue());
+ }
+ retryAfter = Integer.parseInt(header.getValue());
+ if (retryAfter < 0) {
+ retryAfter = 0;
+ } else {
+ if (retryAfter < Constants.MIN_RETRY_AFTER) {
+ retryAfter = Constants.MIN_RETRY_AFTER;
+ } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
+ retryAfter = Constants.MAX_RETRY_AFTER;
+ }
+ retryAfter += Helpers.rnd.nextInt(Constants.MIN_RETRY_AFTER + 1);
+ retryAfter *= 1000;
+ }
+ } catch (NumberFormatException ex) {
+ // ignored - retryAfter stays 0 in this case.
+ }
+ }
+ request.abort();
+ break http_request_loop;
+ }
+ if (statusCode == 301 ||
+ statusCode == 302 ||
+ statusCode == 303 ||
+ statusCode == 307) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
+ }
+ if (redirectCount >= Constants.MAX_REDIRECTS) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "too many redirects for download " + mInfo.id +
+ " at " + mInfo.uri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "too many redirects for download " + mInfo.id);
+ }
+ finalStatus = Downloads.STATUS_TOO_MANY_REDIRECTS;
+ request.abort();
+ break http_request_loop;
+ }
+ Header header = response.getFirstHeader("Location");
+ if (header != null) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Location :" + header.getValue());
+ }
+ newUri = new URI(mInfo.uri).resolve(new URI(header.getValue())).toString();
+ ++redirectCount;
+ finalStatus = Downloads.STATUS_RUNNING_PAUSED;
+ request.abort();
+ break http_request_loop;
+ }
+ }
if ((!continuingDownload && statusCode != Downloads.STATUS_SUCCESS)
|| (continuingDownload && statusCode != 206)) {
if (Constants.LOGV) {
- Log.d(TAG, "http error " + statusCode + " for " + mInfo.uri);
+ Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.uri);
} else if (Config.LOGD) {
- Log.d(TAG, "http error " + statusCode + " for download " + mInfo.id);
+ Log.d(Constants.TAG, "http error " + statusCode + " for download " +
+ mInfo.id);
}
if (Downloads.isStatusError(statusCode)) {
finalStatus = statusCode;
@@ -262,12 +313,12 @@ http_request_loop:
} else {
finalStatus = Downloads.STATUS_UNHANDLED_HTTP_CODE;
}
- requestA.abort();
+ request.abort();
break http_request_loop;
} else {
// Handles the response, saves the file
if (Constants.LOGV) {
- Log.v(TAG, "received response for " + mInfo.uri);
+ Log.v(Constants.TAG, "received response for " + mInfo.uri);
}
if (!continuingDownload) {
@@ -309,17 +360,19 @@ http_request_loop:
} else {
// Ignore content-length with transfer-encoding - 2616 4.4 3
if (Constants.LOGVV) {
- Log.v(TAG, "ignoring content-length because of xfer-encoding");
+ Log.v(Constants.TAG,
+ "ignoring content-length because of xfer-encoding");
}
}
if (Constants.LOGVV) {
- Log.v(TAG, "Accept-Ranges: " + headerAcceptRanges);
- Log.v(TAG, "Content-Disposition: " + headerContentDisposition);
- Log.v(TAG, "Content-Length: " + headerContentLength);
- Log.v(TAG, "Content-Location: " + headerContentLocation);
- Log.v(TAG, "Content-Type: " + mimeType);
- Log.v(TAG, "ETag: " + headerETag);
- Log.v(TAG, "Transfer-Encoding: " + headerTransferEncoding);
+ Log.v(Constants.TAG, "Accept-Ranges: " + headerAcceptRanges);
+ Log.v(Constants.TAG, "Content-Disposition: " +
+ headerContentDisposition);
+ Log.v(Constants.TAG, "Content-Length: " + headerContentLength);
+ Log.v(Constants.TAG, "Content-Location: " + headerContentLocation);
+ Log.v(Constants.TAG, "Content-Type: " + mimeType);
+ Log.v(Constants.TAG, "ETag: " + headerETag);
+ Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
}
if (!mInfo.noIntegrity && headerContentLength == null &&
@@ -327,10 +380,10 @@ http_request_loop:
|| !headerTransferEncoding.equalsIgnoreCase("chunked"))
) {
if (Config.LOGD) {
- Log.d(TAG, "can't know size of download, giving up");
+ Log.d(Constants.TAG, "can't know size of download, giving up");
}
finalStatus = Downloads.STATUS_LENGTH_REQUIRED;
- requestA.abort();
+ request.abort();
break http_request_loop;
}
@@ -342,25 +395,23 @@ http_request_loop:
headerContentLocation,
mimeType,
mInfo.destination,
- mInfo.otaUpdate,
- mInfo.noSystem,
(headerContentLength != null) ?
Integer.parseInt(headerContentLength) : 0);
if (fileInfo.filename == null) {
finalStatus = fileInfo.status;
- requestA.abort();
+ request.abort();
break http_request_loop;
}
filename = fileInfo.filename;
stream = fileInfo.stream;
if (Constants.LOGV) {
- Log.v(TAG, "writing " + mInfo.uri + " to " + filename);
+ Log.v(Constants.TAG, "writing " + mInfo.uri + " to " + filename);
}
ContentValues values = new ContentValues();
- values.put(Downloads.FILENAME, filename);
+ values.put(Downloads._DATA, filename);
if (headerETag != null) {
- values.put(Downloads.ETAG, headerETag);
+ values.put(Constants.ETAG, headerETag);
}
if (mimeType != null) {
values.put(Downloads.MIMETYPE, mimeType);
@@ -384,15 +435,15 @@ http_request_loop:
countRetry = true;
} else {
if (Constants.LOGV) {
- Log.d(TAG, "IOException getting entity for " + mInfo.uri +
+ Log.d(Constants.TAG, "IOException getting entity for " + mInfo.uri +
" : " + ex);
} else if (Config.LOGD) {
- Log.d(TAG, "IOException getting entity for download " + mInfo.id +
- " : " + ex);
+ Log.d(Constants.TAG, "IOException getting entity for download " +
+ mInfo.id + " : " + ex);
}
finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
}
- requestA.abort();
+ request.abort();
break http_request_loop;
}
for (;;) {
@@ -405,11 +456,11 @@ http_request_loop:
mContext.getContentResolver().update(contentUri, values, null, null);
if (!mInfo.noIntegrity && headerETag == null) {
if (Constants.LOGV) {
- Log.v(TAG, "download IOException for " + mInfo.uri +
- " : " + ex);
+ Log.v(Constants.TAG, "download IOException for " + mInfo.uri +
+ " : " + ex);
} else if (Config.LOGD) {
- Log.d(TAG, "download IOException for download " + mInfo.id +
- " : " + ex);
+ Log.d(Constants.TAG, "download IOException for download " +
+ mInfo.id + " : " + ex);
}
if (Config.LOGD) {
Log.d(Constants.TAG,
@@ -423,15 +474,15 @@ http_request_loop:
countRetry = true;
} else {
if (Constants.LOGV) {
- Log.v(TAG, "download IOException for " + mInfo.uri +
- " : " + ex);
+ Log.v(Constants.TAG, "download IOException for " + mInfo.uri +
+ " : " + ex);
} else if (Config.LOGD) {
- Log.d(TAG, "download IOException for download " + mInfo.id +
- " : " + ex);
+ Log.d(Constants.TAG, "download IOException for download " +
+ mInfo.id + " : " + ex);
}
finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
}
- requestA.abort();
+ request.abort();
break http_request_loop;
}
if (bytesRead == -1) { // success
@@ -444,12 +495,29 @@ http_request_loop:
if ((headerContentLength != null)
&& (bytesSoFar
!= Integer.parseInt(headerContentLength))) {
- if (Constants.LOGV) {
- Log.d(TAG, "mismatched content length " + mInfo.uri);
- } else if (Config.LOGD) {
- Log.d(TAG, "mismatched content length for " + mInfo.id);
+ if (!mInfo.noIntegrity && headerETag == null) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "mismatched content length " +
+ mInfo.uri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "mismatched content length for " +
+ mInfo.id);
+ }
+ finalStatus = Downloads.STATUS_LENGTH_REQUIRED;
+ } else if (!Helpers.isNetworkAvailable(mContext)) {
+ finalStatus = Downloads.STATUS_RUNNING_PAUSED;
+ } else if (mInfo.numFailed < Constants.MAX_RETRIES) {
+ finalStatus = Downloads.STATUS_RUNNING_PAUSED;
+ countRetry = true;
+ } else {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "closed socket for " + mInfo.uri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "closed socket for download " +
+ mInfo.id);
+ }
+ finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
}
- finalStatus = Downloads.STATUS_LENGTH_REQUIRED;
break http_request_loop;
}
break;
@@ -499,20 +567,30 @@ http_request_loop:
}
if (Constants.LOGVV) {
- Log.v(TAG, "downloaded " + bytesSoFar + " for " + mInfo.uri);
+ Log.v(Constants.TAG, "downloaded " + bytesSoFar + " for " + mInfo.uri);
+ }
+ synchronized(mInfo) {
+ if (mInfo.control == Downloads.CONTROL_PAUSED) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "paused " + mInfo.uri);
+ }
+ finalStatus = Downloads.STATUS_RUNNING_PAUSED;
+ request.abort();
+ break http_request_loop;
+ }
}
if (mInfo.status == Downloads.STATUS_CANCELED) {
if (Constants.LOGV) {
- Log.d(TAG, "canceled " + mInfo.uri);
+ Log.d(Constants.TAG, "canceled " + mInfo.uri);
} else if (Config.LOGD) {
- // Log.d(TAG, "canceled id " + mInfo.id);
+ // Log.d(Constants.TAG, "canceled id " + mInfo.id);
}
finalStatus = Downloads.STATUS_CANCELED;
break http_request_loop;
}
}
if (Constants.LOGV) {
- Log.v(TAG, "download completed for " + mInfo.uri);
+ Log.v(Constants.TAG, "download completed for " + mInfo.uri);
}
finalStatus = Downloads.STATUS_SUCCESS;
}
@@ -520,15 +598,15 @@ http_request_loop:
}
} catch (FileNotFoundException ex) {
if (Config.LOGD) {
- Log.d(TAG, "FileNotFoundException for " + filename + " : " + ex);
+ Log.d(Constants.TAG, "FileNotFoundException for " + filename + " : " + ex);
}
finalStatus = Downloads.STATUS_FILE_ERROR;
// falls through to the code that reports an error
} catch (Exception ex) { //sometimes the socket code throws unchecked exceptions
if (Constants.LOGV) {
- Log.d(TAG, "Exception for " + mInfo.uri + " : " + ex);
+ Log.d(Constants.TAG, "Exception for " + mInfo.uri, ex);
} else if (Config.LOGD) {
- Log.d(TAG, "Exception for id " + mInfo.id + " : " + ex);
+ Log.d(Constants.TAG, "Exception for id " + mInfo.id, ex);
}
finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
// falls through to the code that reports an error
@@ -565,7 +643,7 @@ http_request_loop:
File file = new File(filename);
Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
if (item == null) {
- Log.w(TAG, "unable to add file " + filename + " to DrmProvider");
+ Log.w(Constants.TAG, "unable to add file " + filename + " to DrmProvider");
finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
} else {
filename = item.getDataString();
@@ -578,7 +656,8 @@ http_request_loop:
FileUtils.setPermissions(filename, 0644, -1, -1);
}
}
- notifyDownloadCompleted(finalStatus, countRetry, gotData, filename, mimeType);
+ notifyDownloadCompleted(finalStatus, countRetry, retryAfter, redirectCount,
+ gotData, filename, newUri, mimeType);
}
}
@@ -586,46 +665,37 @@ http_request_loop:
* Stores information about the completed download, and notifies the initiating application.
*/
private void notifyDownloadCompleted(
- int status, boolean countRetry, boolean gotData, String filename, String mimeType) {
- notifyThroughDatabase(status, countRetry, gotData, filename, mimeType);
+ int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
+ String filename, String uri, String mimeType) {
+ notifyThroughDatabase(
+ status, countRetry, retryAfter, redirectCount, gotData, filename, uri, mimeType);
if (Downloads.isStatusCompleted(status)) {
notifyThroughIntent();
}
}
private void notifyThroughDatabase(
- int status, boolean countRetry, boolean gotData, String filename, String mimeType) {
- // Updates database when the download completes.
- Cursor cursor = null;
-
- String projection[] = {};
- cursor = mContext.getContentResolver().query(Downloads.CONTENT_URI,
- projection, Downloads._ID + "=" + mInfo.id, null, null);
-
- if (cursor != null) {
- // Looping makes the code more solid in case there are 2 entries with the same id
- while (cursor.moveToNext()) {
- cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.STATUS), status);
- cursor.updateString(cursor.getColumnIndexOrThrow(Downloads.FILENAME), filename);
- cursor.updateString(cursor.getColumnIndexOrThrow(Downloads.MIMETYPE), mimeType);
- cursor.updateLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION),
- System.currentTimeMillis());
- if (!countRetry) {
- // if there's no reason to get delayed retry, clear this field
- cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS), 0);
- } else if (gotData) {
- // if there's a reason to get a delayed retry but we got some data in this
- // try, reset the retry count.
- cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS), 1);
- } else {
- // should get a retry and didn't make any progress this time - increment count
- cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS),
- mInfo.numFailed + 1);
- }
- }
- cursor.commitUpdates();
- cursor.close();
+ int status, boolean countRetry, int retryAfter, int redirectCount, boolean gotData,
+ String filename, String uri, String mimeType) {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.STATUS, status);
+ values.put(Downloads._DATA, filename);
+ if (uri != null) {
+ values.put(Downloads.URI, uri);
+ }
+ values.put(Downloads.MIMETYPE, mimeType);
+ values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
+ values.put(Constants.RETRY_AFTER___REDIRECT_COUNT, retryAfter + (redirectCount << 28));
+ if (!countRetry) {
+ values.put(Constants.FAILED_CONNECTIONS, 0);
+ } else if (gotData) {
+ values.put(Constants.FAILED_CONNECTIONS, 1);
+ } else {
+ values.put(Constants.FAILED_CONNECTIONS, mInfo.numFailed + 1);
}
+
+ mContext.getContentResolver().update(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI, mInfo.id), values, null, null);
}
/**
@@ -634,9 +704,6 @@ http_request_loop:
*/
private void notifyThroughIntent() {
Uri uri = Uri.parse(Downloads.CONTENT_URI + "/" + mInfo.id);
- Intent intent = new Intent(Downloads.DOWNLOAD_COMPLETED_ACTION);
- intent.setData(uri);
- mContext.sendBroadcast(intent, "android.permission.ACCESS_DOWNLOAD_DATA");
mInfo.sendIntentIfRequested(uri, mContext);
}
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index f966a7f5..89a57313 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -16,6 +16,7 @@
package com.android.providers.downloads;
+import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -27,7 +28,9 @@ 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;
@@ -39,14 +42,15 @@ import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.Set;
/**
* Some helper functions for the download manager
*/
public class Helpers {
- /** Tag used for debugging/logging */
- private static final String TAG = Constants.TAG;
-
+
+ public static Random rnd = 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*\"([^\"]*)\"");
@@ -83,8 +87,6 @@ public class Helpers {
String contentLocation,
String mimeType,
int destination,
- boolean otaUpdate,
- boolean noSystem,
int contentLength) throws FileNotFoundException {
/*
@@ -94,13 +96,7 @@ public class Helpers {
|| 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");
+ Log.d(Constants.TAG, "external download with no mime type not allowed");
}
return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
}
@@ -121,7 +117,7 @@ public class Helpers {
intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
List<ResolveInfo> list = pm.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
- //Log.i(TAG, "*** FILENAME QUERY " + intent + ": " + list);
+ //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list);
if (list.size() == 0) {
if (Config.LOGD) {
@@ -132,7 +128,7 @@ public class Helpers {
}
}
String filename = chooseFilename(
- url, hint, contentDisposition, contentLocation, destination, otaUpdate);
+ url, hint, contentDisposition, contentLocation, destination);
// Split filename between base and extension
// Add an extension if filename does not have one
@@ -142,7 +138,7 @@ public class Helpers {
extension = chooseExtensionFromMimeType(mimeType, true);
} else {
extension = chooseExtensionFromFilename(
- mimeType, destination, otaUpdate, filename, dotIndex);
+ mimeType, destination, filename, dotIndex);
filename = filename.substring(0, dotIndex);
}
@@ -156,6 +152,7 @@ public class Helpers {
// 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());
@@ -173,7 +170,8 @@ public class Helpers {
if (!discardPurgeableFiles(context,
contentLength - blockSize * ((long) availableBlocks - 4))) {
if (Config.LOGD) {
- Log.d(TAG, "download aborted - not enough free space in internal storage");
+ Log.d(Constants.TAG,
+ "download aborted - not enough free space in internal storage");
}
return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
}
@@ -181,34 +179,22 @@ public class Helpers {
}
} else {
- if (destination == Downloads.DESTINATION_DATA_CACHE) {
- base = context.getCacheDir();
+ 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 "
+ 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());
} 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);
+ if (Config.LOGD) {
+ Log.d(Constants.TAG, "download aborted - no external storage");
}
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
}
/*
@@ -217,14 +203,13 @@ public class Helpers {
*/
if (stat.getBlockSize() * ((long) stat.getAvailableBlocks() - 4) < contentLength) {
if (Config.LOGD) {
- Log.d(TAG, "download aborted - not enough free space");
+ Log.d(Constants.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;
@@ -233,11 +218,11 @@ public class Helpers {
* Generate a unique filename, create the file, return it.
*/
if (Constants.LOGVV) {
- Log.v(TAG, "target file: " + filename + extension);
+ Log.v(Constants.TAG, "target file: " + filename + extension);
}
String fullFilename = chooseUniqueFilename(
- destination, otaUpdate, filename, extension, otaFilename, recoveryDir);
+ destination, filename, extension, recoveryDir);
if (fullFilename != null) {
return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
} else {
@@ -246,18 +231,13 @@ public class Helpers {
}
private static String chooseFilename(String url, String hint, String contentDisposition,
- String contentLocation, int destination, boolean otaUpdate) {
+ String contentLocation, int destination) {
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");
+ Log.v(Constants.TAG, "getting filename from hint");
}
int index = hint.lastIndexOf('/') + 1;
if (index > 0) {
@@ -272,7 +252,7 @@ public class Helpers {
filename = parseContentDisposition(contentDisposition);
if (filename != null) {
if (Constants.LOGVV) {
- Log.v(TAG, "getting filename from content-disposition");
+ Log.v(Constants.TAG, "getting filename from content-disposition");
}
int index = filename.lastIndexOf('/') + 1;
if (index > 0) {
@@ -288,7 +268,7 @@ public class Helpers {
&& !decodedContentLocation.endsWith("/")
&& decodedContentLocation.indexOf('?') < 0) {
if (Constants.LOGVV) {
- Log.v(TAG, "getting filename from content-location");
+ Log.v(Constants.TAG, "getting filename from content-location");
}
int index = decodedContentLocation.lastIndexOf('/') + 1;
if (index > 0) {
@@ -307,7 +287,7 @@ public class Helpers {
int index = decodedUrl.lastIndexOf('/') + 1;
if (index > 0) {
if (Constants.LOGVV) {
- Log.v(TAG, "getting filename from uri");
+ Log.v(Constants.TAG, "getting filename from uri");
}
filename = decodedUrl.substring(index);
}
@@ -317,10 +297,14 @@ public class Helpers {
// Finally, if couldn't get filename from URI, get a generic filename
if (filename == null) {
if (Constants.LOGVV) {
- Log.v(TAG, "using default filename");
+ Log.v(Constants.TAG, "using default filename");
}
filename = Constants.DEFAULT_DL_FILENAME;
}
+
+ filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_");
+
+
return filename;
}
@@ -330,12 +314,12 @@ public class Helpers {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
if (extension != null) {
if (Constants.LOGVV) {
- Log.v(TAG, "adding extension from type");
+ Log.v(Constants.TAG, "adding extension from type");
}
extension = "." + extension;
} else {
if (Constants.LOGVV) {
- Log.v(TAG, "couldn't find extension for " + mimeType);
+ Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
}
}
}
@@ -343,18 +327,18 @@ public class Helpers {
if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
if (mimeType.equalsIgnoreCase("text/html")) {
if (Constants.LOGVV) {
- Log.v(TAG, "adding default html extension");
+ Log.v(Constants.TAG, "adding default html extension");
}
extension = Constants.DEFAULT_DL_HTML_EXTENSION;
} else if (useDefaults) {
if (Constants.LOGVV) {
- Log.v(TAG, "adding default text extension");
+ Log.v(Constants.TAG, "adding default text extension");
}
extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
}
} else if (useDefaults) {
if (Constants.LOGVV) {
- Log.v(TAG, "adding default binary extension");
+ Log.v(Constants.TAG, "adding default binary extension");
}
extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
}
@@ -363,10 +347,9 @@ public class Helpers {
}
private static String chooseExtensionFromFilename(String mimeType, int destination,
- boolean otaUpdate, String filename, int dotIndex) {
+ String filename, int dotIndex) {
String extension = null;
- if (mimeType != null
- && !(destination == Downloads.DESTINATION_CACHE_PARTITION && otaUpdate)) {
+ 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('.');
@@ -376,63 +359,60 @@ public class Helpers {
extension = chooseExtensionFromMimeType(mimeType, false);
if (extension != null) {
if (Constants.LOGVV) {
- Log.v(TAG, "substituting extension from type");
+ Log.v(Constants.TAG, "substituting extension from type");
}
} else {
if (Constants.LOGVV) {
- Log.v(TAG, "couldn't find extension for " + mimeType);
+ Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
}
}
}
}
if (extension == null) {
if (Constants.LOGVV) {
- Log.v(TAG, "keeping extension");
+ Log.v(Constants.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) {
+ private static String chooseUniqueFilename(int destination, String filename,
+ String extension, 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))) {
+ destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE &&
+ destination != Downloads.DESTINATION_CACHE_PARTITION_NOROAMING))) {
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;
+ }
+ 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 += rnd.nextInt(magnitude) + 1;
}
}
return null;
@@ -460,15 +440,17 @@ public class Helpers {
try {
cursor.moveToFirst();
while (!cursor.isAfterLast() && totalFreed < targetBytes) {
- File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.FILENAME)));
+ 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();
- cursor.deleteRow(); // This moves the cursor to the next entry,
- // no need to call next()
+ long id = cursor.getLong(cursor.getColumnIndex(Downloads._ID));
+ context.getContentResolver().delete(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI, id), null, null);
+ cursor.moveToNext();
}
} finally {
cursor.close();
@@ -491,20 +473,322 @@ public class Helpers {
if (connectivity == null) {
Log.w(Constants.TAG, "couldn't get connectivity manager");
} else {
- NetworkInfo info = connectivity.getActiveNetworkInfo();
+ NetworkInfo[] info = connectivity.getAllNetworkInfo();
if (info != null) {
- if (info.getState() == NetworkInfo.State.CONNECTED) {
- if (Constants.LOGVV) {
- Log.v(TAG, "network is available");
+ 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;
}
- return true;
}
}
}
if (Constants.LOGVV) {
- Log.v(TAG, "network is not available");
+ 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) {
+ 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");
+ }
+
+ 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');
+ }
+ }
}