From 1fbad9cfa532f13f6cf03f542febf2e4689edec5 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Wed, 17 Dec 2008 18:06:03 -0800 Subject: Code drop from //branches/cupcake/...@124589 --- src/com/android/providers/downloads/Constants.java | 45 +- .../android/providers/downloads/DownloadInfo.java | 61 ++- .../providers/downloads/DownloadNotification.java | 20 +- .../providers/downloads/DownloadProvider.java | 363 +++++++++++---- .../providers/downloads/DownloadReceiver.java | 30 +- .../providers/downloads/DownloadService.java | 198 +++++---- .../providers/downloads/DownloadThread.java | 327 ++++++++------ src/com/android/providers/downloads/Helpers.java | 492 ++++++++++++++++----- 8 files changed, 1074 insertions(+), 462 deletions(-) (limited to 'src') 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"; @@ -104,6 +116,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. @@ -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 sAppReadableColumnsSet; + static { + sAppReadableColumnsSet = new HashSet(); + 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,12 +439,16 @@ 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) { @@ -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 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 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 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 mAllowedColumns; + private int mOffset = 0; + private int mCurrentToken = TOKEN_START; + private final char[] mChars; + + public Lexer(String selection, Set 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'); + } + } } -- cgit v1.2.3