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 --- AndroidManifest.xml | 16 +- .../status_bar_ongoing_event_progress_bar.xml | 6 +- res/values-de-rDE/strings.xml | 40 -- res/values-de/strings.xml | 19 + res/values-en-rGB/strings.xml | 40 -- res/values-es-rUS/strings.xml | 40 -- res/values-fr-rFR/strings.xml | 40 -- res/values-it-rIT/strings.xml | 40 -- res/values-ja/strings.xml | 19 + res/values-zh-rTW/strings.xml | 40 -- res/values/strings.xml | 99 +++-- 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 ++++++++++++++++----- 19 files changed, 1192 insertions(+), 743 deletions(-) delete mode 100644 res/values-de-rDE/strings.xml create mode 100644 res/values-de/strings.xml delete mode 100644 res/values-en-rGB/strings.xml delete mode 100644 res/values-es-rUS/strings.xml delete mode 100644 res/values-fr-rFR/strings.xml delete mode 100644 res/values-it-rIT/strings.xml create mode 100644 res/values-ja/strings.xml delete mode 100644 res/values-zh-rTW/strings.xml diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d9873e61..7769171b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -9,11 +9,11 @@ android:description="@string/permdesc_downloadManager" android:protectionLevel="signatureOrSystem" /> - - + + - - - - - - - - Herunterladen wird gestartet\u2026 - Warten auf Netzwerk\u2026 - Herunterladen läuft - Warten auf Netzwerk\u2026 - <ohne Titel> - Herunterladen abgeschlossen - Herunterladen nicht erfolgreich - " und %d mehr" - ", " - Ermöglicht einer Anwendung - direkt auf den System-Cache-Speicher zuzugreifen und ihn zu ändern und zu löschen. Schädliche - Anwendungen können dies nutzen, um Herunterladen und - andere Anwendungen ernsthaft zu stören und auf private Daten zuzugreifen. - Ermöglicht einer Anwendung - Benachrichtigungen über abgeschlossenes Herunterladen zu senden. Schädliche Anwendungen können dies nutzen, - um andere Anwendungen zu stören, - die Dateien herunterladen. - Ermöglicht einer Anwendung - auf Informationen über alles Herunterladen im Download-Manager zuzugreifen. - Schädliche Anwendungen können dies nutzen, um Herunterladen ernsthaft zu stören - und auf private Daten zuzugreifen. - Ermöglicht einer Anwendung - auf den Download-Manager zuzugreifen und ihn zum Herunterladen von Dateien zu verwenden. - Schädliche Anwendungen können dies nutzen, um Herunterladen zu stören und auf - private Daten zuzugreifen. - Ermöglicht einer Anwendung - festzulegen, dass sie Dateien in den internen - Cache-Speicher mit dem Dateinamen herunterlädt, der für OTA-Updates reserviert ist. - Schädliche Anwendungen können dies nutzen, um das Herunterladen von OTA-Updates - zu verhindern. - Systemcache verwenden. - Herunterladen-Benachrichtigungen - senden. - Auf heruntergeladene Daten zugreifen. - Auf Download-Manager zugreifen. - OTA-Update herunterladen. - diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml new file mode 100644 index 00000000..a8f0dd2a --- /dev/null +++ b/res/values-de/strings.xml @@ -0,0 +1,19 @@ + + + "Auf Download-Manager zugreifen" + "Ermöglicht der Anwendung den Zugriff auf den Download-Manager zum Herunterladen von Dateien. Diese Funktion kann von bösartigen Anwendungen dazu verwendet werden, Ladevorgänge zu unterbrechen und auf private Daten zuzugreifen." + + + + + "Systemcache verwenden" + "Ermöglicht es der Anwendung, direkt auf den Systemcache zuzugreifen und diesen zu ändern oder zu löschen. Diese Funktion kann von bösartigen Anwendungen dazu verwendet werden, Ladevorgänge und andere Anwendungen schwerwiegend zu stören sowie auf private Daten zuzugreifen." + "Benachrichtigungen zu Ladevorgängen senden" + "Ermöglicht es der Anwendung, Benachrichtigungen zu abgeschlossenen Ladevorgängen zu senden. Diese Funktion kann von bösartigen Anwendungen dazu verwendet werden, den Ladevorgang anderer Anwendungen zu stören." + "<Unbenannt>" + "," + "und %d weitere" + "Ladevorgang abgeschlossen" + "Fehler beim Ladevorgang" + diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml deleted file mode 100644 index ffdaf047..00000000 --- a/res/values-en-rGB/strings.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - Starting download\u2026 - Waiting for network\u2026 - Downloading - Waiting for network\u2026 - <Untitled> - Download complete - Download unsuccessful - " and %d more" - ", " - Allows the application - to directly access, modify and delete the system cache. Malicious - applications can use this to severely disrupt downloads and - other applications, and to access private data. - Allows the application - to send notifications about completed downloads. Malicious applications - can use this to confuse other applications that download - files. - Allows the application to - access information about all downloads in the download manager. - Malicious applications can use this to severely disrupt downloads - and access private information. - Allows the application to - access the download manager and to use it to download files. - Malicious applications can use this to disrupt downloads and access - private information. - Allows the application - to specify that it wants to download files in the internal - cache with the filename that is reserved for OTA updates. - Malicious applications can use this to prevent OTA updates from - getting downloaded. - Use system cache. - Send download - notifications. - Access download data. - Access download manager. - Download OTA update. - diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml deleted file mode 100644 index 7453e8a5..00000000 --- a/res/values-es-rUS/strings.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - Iniciando descarga\u2026 - Esperando red\u2026 - Descargando - Esperando red\u2026 - <Sin título> - Descarga completa - Error en la descarga - " y %d más" - ", " - Permite a la aplicación - acceder directamente, modificar y eliminar la caché del sistema. Las aplicaciones - maliciosas pueden utilizar esta función para dañar las descargas y - otras aplicaciones, o para acceder a datos privados. - Permite a la aplicación - enviar notificaciones sobre las descargas realizadas. Las aplicaciones maliciosas - pueden utilizar esta función para confundir a otras aplicaciones que descargan - archivos. - Permite a la aplicación - acceder a información sobre todas las descargas en el administrador de descargas. - Las aplicaciones maliciosas pueden utilizar esta función para alterar gravemente las descargas - y acceder a información privada. - Permite a la aplicación - acceder al administrador de descargas y utilizarlo para descargar archivos. - Las aplicaciones maliciosas pueden utilizar esta función para alterar las descargas y acceder - a información privada. - Permite a la aplicación - especificar que desea descargar archivos en la caché - interna con el nombre de archivo reservado para las actualizaciones OTA. - Las aplicaciones maliciosas puede utilizar esta función para evitar que las actualizaciones OTA - se descarguen. - Uso de la caché del sistema. - Enviar notificaciones de - descarga. - Acceso a datos de descarga. - Acceso al administrador de descargas. - Descargar actualización de OTA. - diff --git a/res/values-fr-rFR/strings.xml b/res/values-fr-rFR/strings.xml deleted file mode 100644 index 30c4e62d..00000000 --- a/res/values-fr-rFR/strings.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - Début du téléchargement\u2026 - Attente du réseau\u2026 - Téléchargement en cours - Attente du réseau\u2026 - <Sans titre> - Téléchargement terminé - Échec du téléchargement - " et %d en plus" - ", " - Permet à l\'application - de directement accéder, modifier et supprimer la cache système. - Les applications malicieuses peuvent utiliser cela pour désorganiser - sérieusement les téléchargements et les autres applications, et pour accéder aux données privées. - Permet à l\'application - d\'envoyer des notifications sur les téléchargements terminés. Les applications - malicieuses peuvent utiliser cela pour tromper les autres - applications qui téléchargent des fichiers. - Permet à l\'application - d\'accéder aux informations de téléchargement dans le gestionnaire de - téléchargements. Les applications malicieuses peuvent utiliser cela - pour désorganiser sérieusement les téléchargements et accéder aux informations privées. - Permet à l\'application - d\'accéder au gestionnaire de téléchargements et de l\'utiliser pour. - télécharger les fichiers. Les applications malicieuses peuvent utiliser cela - pour désorganiser les téléchargements et accéder aux informations privées. - Permet à l\'application - de spécifier qu\'elle veut télécharger des fichiers dans la - cache interne avec le nom de fichier réservé aux mises à - jour OTA. Les applications malicieuses peuvent utiliser cela pour - empêcher aux mises à jour OTA d\'être téléchargées. - Utilisez la cache système. - Envoyez les notifications - de téléchargement. - Accédez aux données de téléchargement. - Accédez au gestionnaire de téléchargement. - Téléchargez la mise à jour OTA. - diff --git a/res/values-it-rIT/strings.xml b/res/values-it-rIT/strings.xml deleted file mode 100644 index c19c0bb3..00000000 --- a/res/values-it-rIT/strings.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - Avvio del download in corso\u2026 - In attesa della rete\u2026 - Download in corso - In attesa della rete\u2026 - <Senza titolo> - Download completato - Download non riuscito - " e ulteriore %d" - ", " - Consente all'applicazione di - accedere direttamente, modificare ed eliminare la cache del sistema. Le applicazioni - dannose possono utilizzare questa autorizzazione per interrompere i download e le altre applicazioni - e accedere ai dati privati. - Consente all'applicazione - di inviare le notifiche sui download completati. Le applicazioni nocive - possono utilizzare questa autorizzazione per confondere le applicazioni che scaricano i - file. - Consente all'applicazione di - accedere alle informazioni sui download nel gestore download. - Le applicazioni nocive possono utilizzare questa autorizzazione per interrompere i download - e accedere alle informazioni private. - Consente all'applicazione di - accedere al gestore download e utilizzarlo per scaricare i file. - Le applicazioni dannose possono utilizzare questa autorizzazione per interrompere i download e accedere alle - informazioni private. - Consente all'applicazione - di specificare che desidera scaricare i file nella cache interna - con il nome file riservato agli aggiornamenti OTA. - Le applicazioni nocive possono utilizzare questa autorizzazione per impedire lo scaricamento di aggiornamenti - OTA. - Utilizzare la cache del sistema. - Inviare le notifiche sul - download. - Accedere ai dati del download. - Accedere al gestore download. - Scaricare l'aggiornamento OTA. - diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml new file mode 100644 index 00000000..0494e18f --- /dev/null +++ b/res/values-ja/strings.xml @@ -0,0 +1,19 @@ + + + "ダウンロードマネージャーにアクセスします。" + "アプリケーションでダウンロードマネージャーにアクセスしてファイルをダウンロードできるようにします。悪意のあるアプリケーションではこれを利用して、ダウンロードに深刻な影響を与えたり、個人データにアクセスしたりできます。" + + + + + "システムキャッシュを使用します。" + "アプリケーションでシステムキャッシュを直接アクセス、変更、削除できるようにします。悪意のあるアプリケーションではこれを利用して、ダウンロードや他のアプリケーションに深刻な影響を与えたり、個人データにアクセスしたりできます。" + "ダウンロード通知を送信します。" + "ダウンロード完了の通知をアプリケーションから送信できるようにします。悪意のあるアプリケーションではこの通知を利用して、ファイルをダウンロードする他のアプリケーションの処理を妨害できます。" + "<無題>" + "、" + "他%d件" + "ダウンロード完了" + "ダウンロードに失敗しました" + diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml deleted file mode 100644 index c34475bb..00000000 --- a/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - 正在開始下載\u2026 - 正在等待網路\u2026 - 正在下載 - 正在等待網路\u2026 - <未命名> - 下載完成 - 下載失敗 - " 還有 %d" - ", " - 允許應用程式 - 直接存取、修改及刪除系統快取。惡意的 - 應用程式可能會利用此方式嚴重干擾下載和 - 其它應用程式,及存取私人資料。 - 允許應用程式 - 傳送完成下載的通知。惡意的應用程式 - 可能會利用此方式混淆下載 - 檔案的其它應用程式。 - 允許應用程式 - 存取下載管理員中所有下載的存取資訊。 - 惡意的應用程式可能會利用此方式嚴重干擾下載, - 及存取私人資訊。 - 允許應用程式 - 存取下載管理員並使用其下載檔案。 - 惡意的應用程式可能會利用此方式來干擾下載,及存取 - 私人資訊。 - 允許應用程式 - 指定其想要下載內部快取中含有 OTA - 更新專用之檔名的檔案。 - 惡意的應用程式可能會利用此方式來阻止 - 下載 OTA 更新。 - 使用系統快取。 - 傳送下載 - 通知。 - 存取下載資料。 - 存取下載管理員。 - 下載 OTA 更新。 - diff --git a/res/values/strings.xml b/res/values/strings.xml index 657c92eb..3a8fa076 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -15,59 +15,104 @@ --> - - Downloading - - Starting download\u2026 - - Waiting for network\u2026 - - Waiting for network\u2026 - - <Untitled> - + Access download manager. + Allows the application to access the download manager and to use it to download files. Malicious applications can use this to disrupt downloads and access private information. - Access download data. - Allows the application to - access information about all downloads in the download manager. - Malicious applications can use this to severely disrupt downloads - and access private information. + + Advanced download + manager functions. + + Allows the application to + access the download manager's advanced functions. + Malicious applications can use this to disrupt downloads and access + private information. + Use system cache. + Allows the application to directly access, modify and delete the system cache. Malicious applications can use this to severely disrupt downloads and other applications, and to access private data. - Download OTA update. - Allows the application - to specify that it wants to download files in the internal - cache with the filename that is reserved for OTA updates. - Malicious applications can use this to prevent OTA updates from - getting downloaded. - Send download notifications. + Allows the application to send notifications about completed downloads. Malicious applications can use this to confuse other applications that download files. - + + <Untitled> + + ", " - + " and %d more" - + Download complete - + Download unsuccessful 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