Code drop from //branches/cupcake/...@124589
The Android Open Source Project [Thu, 18 Dec 2008 02:06:03 +0000 (18:06 -0800)]
19 files changed:
AndroidManifest.xml
res/layout/status_bar_ongoing_event_progress_bar.xml
res/values-de-rDE/strings.xml [deleted file]
res/values-de/strings.xml [new file with mode: 0644]
res/values-en-rGB/strings.xml [deleted file]
res/values-es-rUS/strings.xml [deleted file]
res/values-fr-rFR/strings.xml [deleted file]
res/values-it-rIT/strings.xml [deleted file]
res/values-ja/strings.xml [new file with mode: 0644]
res/values-zh-rTW/strings.xml [deleted file]
res/values/strings.xml
src/com/android/providers/downloads/Constants.java
src/com/android/providers/downloads/DownloadInfo.java
src/com/android/providers/downloads/DownloadNotification.java
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/DownloadReceiver.java
src/com/android/providers/downloads/DownloadService.java
src/com/android/providers/downloads/DownloadThread.java
src/com/android/providers/downloads/Helpers.java

index d9873e6..7769171 100644 (file)
@@ -9,11 +9,11 @@
         android:description="@string/permdesc_downloadManager"
         android:protectionLevel="signatureOrSystem" />
 
-    <!-- Allows access to the Download Manager data (for UI purposes) -->
-    <permission android:name="android.permission.ACCESS_DOWNLOAD_DATA"
-        android:label="@string/permlab_downloadData"
-        android:description="@string/permdesc_downloadData"
-        android:protectionLevel="signature" />
+    <!-- Allows advanced access to the Download Manager -->
+    <permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED"
+        android:label="@string/permlab_downloadManagerAdvanced"
+        android:description="@string/permdesc_downloadManagerAdvanced"
+        android:protectionLevel="signatureOrSystem" />
 
     <!-- Allows filesystem access to /cache -->
     <permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM"
         android:description="@string/permdesc_cacheFilesystem"
         android:protectionLevel="signature" />
 
-    <!-- Allow to download to /cache/update.install -->
-    <permission android:name="android.permission.DOWNLOAD_OTA_UPDATE"
-        android:label="@string/permlab_downloadOtaUpdate"
-        android:description="@string/permdesc_downloadOtaUpdate"
-        android:protectionLevel="signature" />
-
     <!-- Allows to send download completed intents -->
     <permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS"
         android:label="@string/permlab_downloadCompletedIntent"
index c0cdcbf..d81c42b 100644 (file)
@@ -30,7 +30,7 @@
         android:orientation="horizontal"
         >
         
-        <LinearLayout android:id="@+id/app"
+        <LinearLayout
             android:layout_width="40dp"
             android:layout_height="fill_parent"
             android:orientation="vertical"
                 />
         </LinearLayout>
 
-        <RelativeLayout android:id="@+id/app"
+        <RelativeLayout
             android:layout_width="fill_parent"
             android:layout_height="fill_parent"
             android:orientation="vertical"
             android:focusable="true"
             android:clickable="true"
             >
-            <LinearLayout android:id="@+id/notification"
+            <LinearLayout
                 android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
                 android:orientation="horizontal"
diff --git a/res/values-de-rDE/strings.xml b/res/values-de-rDE/strings.xml
deleted file mode 100644 (file)
index e4954d8..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="download_pending">Herunterladen wird gestartet\u2026</string>
-  <string name="download_pending_network">Warten auf Netzwerk\u2026</string>
-  <string name="download_running">Herunterladen läuft </string>
-  <string name="download_running_paused">Warten auf Netzwerk\u2026 </string>
-  <string name="download_unknown_title">&lt;ohne Titel&gt;</string>
-  <string name="notification_download_complete">Herunterladen abgeschlossen</string>
-  <string name="notification_download_failed">Herunterladen nicht erfolgreich</string>
-  <string name="notification_filename_extras">" und %d mehr"</string>
-  <string name="notification_filename_separator">", "</string>
-  <string name="permdesc_cacheFilesystem">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.</string>
-  <string name="permdesc_downloadCompletedIntent">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.</string>
-  <string name="permdesc_downloadData">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.</string>
-  <string name="permdesc_downloadManager">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.</string>
-  <string name="permdesc_downloadOtaUpdate">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.</string>
-  <string name="permlab_cacheFilesystem">Systemcache verwenden.</string>
-  <string name="permlab_downloadCompletedIntent">Herunterladen-Benachrichtigungen
-        senden.</string>
-  <string name="permlab_downloadData">Auf heruntergeladene Daten zugreifen.</string>
-  <string name="permlab_downloadManager">Auf Download-Manager zugreifen.</string>
-  <string name="permlab_downloadOtaUpdate">OTA-Update herunterladen.</string>
-</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
new file mode 100644 (file)
index 0000000..a8f0dd2
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="permlab_downloadManager">"Auf Download-Manager zugreifen"</string>
+    <string name="permdesc_downloadManager">"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."</string>
+    <!-- no translation found for permlab_downloadManagerAdvanced (7103642833308809655) -->
+    <skip />
+    <!-- no translation found for permdesc_downloadManagerAdvanced (8761177317775872287) -->
+    <skip />
+    <string name="permlab_cacheFilesystem">"Systemcache verwenden"</string>
+    <string name="permdesc_cacheFilesystem">"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."</string>
+    <string name="permlab_downloadCompletedIntent">"Benachrichtigungen zu Ladevorgängen senden"</string>
+    <string name="permdesc_downloadCompletedIntent">"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."</string>
+    <string name="download_unknown_title">"&lt;Unbenannt&gt;"</string>
+    <string name="notification_filename_separator">","</string>
+    <string name="notification_filename_extras">"und %d weitere"</string>
+    <string name="notification_download_complete">"Ladevorgang abgeschlossen"</string>
+    <string name="notification_download_failed">"Fehler beim Ladevorgang"</string>
+</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
deleted file mode 100644 (file)
index ffdaf04..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="download_pending">Starting download\u2026</string>
-  <string name="download_pending_network">Waiting for network\u2026</string>
-  <string name="download_running">Downloading </string>
-  <string name="download_running_paused">Waiting for network\u2026 </string>
-  <string name="download_unknown_title">&lt;Untitled&gt;</string>
-  <string name="notification_download_complete">Download complete</string>
-  <string name="notification_download_failed">Download unsuccessful</string>
-  <string name="notification_filename_extras">" and %d more"</string>
-  <string name="notification_filename_separator">", "</string>
-  <string name="permdesc_cacheFilesystem">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.</string>
-  <string name="permdesc_downloadCompletedIntent">Allows the application
-        to send notifications about completed downloads. Malicious applications
-        can use this to confuse other applications that download
-        files.</string>
-  <string name="permdesc_downloadData">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.</string>
-  <string name="permdesc_downloadManager">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.</string>
-  <string name="permdesc_downloadOtaUpdate">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.</string>
-  <string name="permlab_cacheFilesystem">Use system cache.</string>
-  <string name="permlab_downloadCompletedIntent">Send download
-        notifications.</string>
-  <string name="permlab_downloadData">Access download data.</string>
-  <string name="permlab_downloadManager">Access download manager.</string>
-  <string name="permlab_downloadOtaUpdate">Download OTA update.</string>
-</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
deleted file mode 100644 (file)
index 7453e8a..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="download_pending">Iniciando descarga\u2026</string>
-  <string name="download_pending_network">Esperando red\u2026</string>
-  <string name="download_running">Descargando </string>
-  <string name="download_running_paused">Esperando red\u2026 </string>
-  <string name="download_unknown_title">&lt;Sin título&gt;</string>
-  <string name="notification_download_complete">Descarga completa</string>
-  <string name="notification_download_failed">Error en la descarga</string>
-  <string name="notification_filename_extras">" y %d más"</string>
-  <string name="notification_filename_separator">", "</string>
-  <string name="permdesc_cacheFilesystem">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.</string>
-  <string name="permdesc_downloadCompletedIntent">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.</string>
-  <string name="permdesc_downloadData">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.</string>
-  <string name="permdesc_downloadManager">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.</string>
-  <string name="permdesc_downloadOtaUpdate">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.</string>
-  <string name="permlab_cacheFilesystem">Uso de la caché del sistema.</string>
-  <string name="permlab_downloadCompletedIntent">Enviar notificaciones de
-        descarga.</string>
-  <string name="permlab_downloadData">Acceso a datos de descarga. </string>
-  <string name="permlab_downloadManager">Acceso al administrador de descargas. </string>
-  <string name="permlab_downloadOtaUpdate">Descargar actualización de OTA.</string>
-</resources>
diff --git a/res/values-fr-rFR/strings.xml b/res/values-fr-rFR/strings.xml
deleted file mode 100644 (file)
index 30c4e62..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="download_pending">Début du téléchargement\u2026</string>
-  <string name="download_pending_network">Attente du réseau\u2026</string>
-  <string name="download_running">Téléchargement en cours </string>
-  <string name="download_running_paused">Attente du réseau\u2026 </string>
-  <string name="download_unknown_title">&lt;Sans titre&gt;</string>
-  <string name="notification_download_complete">Téléchargement terminé</string>
-  <string name="notification_download_failed">Échec du téléchargement</string>
-  <string name="notification_filename_extras">" et %d en plus"</string>
-  <string name="notification_filename_separator">", "</string>
-  <string name="permdesc_cacheFilesystem">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.</string>
-  <string name="permdesc_downloadCompletedIntent">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.</string>
-  <string name="permdesc_downloadData">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.</string>
-  <string name="permdesc_downloadManager">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.</string>
-  <string name="permdesc_downloadOtaUpdate">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.</string>
-  <string name="permlab_cacheFilesystem">Utilisez la cache système.</string>
-  <string name="permlab_downloadCompletedIntent">Envoyez les notifications
-        de téléchargement.</string>
-  <string name="permlab_downloadData">Accédez aux données de téléchargement.</string>
-  <string name="permlab_downloadManager">Accédez au gestionnaire de téléchargement.</string>
-  <string name="permlab_downloadOtaUpdate">Téléchargez la mise à jour OTA.</string>
-</resources>
diff --git a/res/values-it-rIT/strings.xml b/res/values-it-rIT/strings.xml
deleted file mode 100644 (file)
index c19c0bb..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="download_pending">Avvio del download in corso\u2026</string>
-  <string name="download_pending_network">In attesa della rete\u2026</string>
-  <string name="download_running">Download in corso </string>
-  <string name="download_running_paused">In attesa della rete\u2026 </string>
-  <string name="download_unknown_title">&lt;Senza titolo&gt;</string>
-  <string name="notification_download_complete">Download completato</string>
-  <string name="notification_download_failed">Download non riuscito</string>
-  <string name="notification_filename_extras">" e ulteriore %d"</string>
-  <string name="notification_filename_separator">", "</string>
-  <string name="permdesc_cacheFilesystem">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.</string>
-  <string name="permdesc_downloadCompletedIntent">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.</string>
-  <string name="permdesc_downloadData">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.</string>
-  <string name="permdesc_downloadManager">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.</string>
-  <string name="permdesc_downloadOtaUpdate">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.</string>
-  <string name="permlab_cacheFilesystem">Utilizzare la cache del sistema.</string>
-  <string name="permlab_downloadCompletedIntent">Inviare le notifiche sul
-        download.</string>
-  <string name="permlab_downloadData">Accedere ai dati del download.</string>
-  <string name="permlab_downloadManager">Accedere al gestore download.</string>
-  <string name="permlab_downloadOtaUpdate">Scaricare l'aggiornamento OTA.</string>
-</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
new file mode 100644 (file)
index 0000000..0494e18
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="permlab_downloadManager">"ダウンロードマネージャーにアクセスします。"</string>
+    <string name="permdesc_downloadManager">"アプリケーションでダウンロードマネージャーにアクセスしてファイルをダウンロードできるようにします。悪意のあるアプリケーションではこれを利用して、ダウンロードに深刻な影響を与えたり、個人データにアクセスしたりできます。"</string>
+    <!-- no translation found for permlab_downloadManagerAdvanced (7103642833308809655) -->
+    <skip />
+    <!-- no translation found for permdesc_downloadManagerAdvanced (8761177317775872287) -->
+    <skip />
+    <string name="permlab_cacheFilesystem">"システムキャッシュを使用します。"</string>
+    <string name="permdesc_cacheFilesystem">"アプリケーションでシステムキャッシュを直接アクセス、変更、削除できるようにします。悪意のあるアプリケーションではこれを利用して、ダウンロードや他のアプリケーションに深刻な影響を与えたり、個人データにアクセスしたりできます。"</string>
+    <string name="permlab_downloadCompletedIntent">"ダウンロード通知を送信します。"</string>
+    <string name="permdesc_downloadCompletedIntent">"ダウンロード完了の通知をアプリケーションから送信できるようにします。悪意のあるアプリケーションではこの通知を利用して、ファイルをダウンロードする他のアプリケーションの処理を妨害できます。"</string>
+    <string name="download_unknown_title">"&lt;無題&gt;"</string>
+    <string name="notification_filename_separator">"、"</string>
+    <string name="notification_filename_extras">"他%d件"</string>
+    <string name="notification_download_complete">"ダウンロード完了"</string>
+    <string name="notification_download_failed">"ダウンロードに失敗しました"</string>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
deleted file mode 100644 (file)
index c34475b..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="download_pending">正在開始下載\u2026</string>
-  <string name="download_pending_network">正在等待網路\u2026</string>
-  <string name="download_running">正在下載 </string>
-  <string name="download_running_paused">正在等待網路\u2026 </string>
-  <string name="download_unknown_title">&lt;未命名&gt;</string>
-  <string name="notification_download_complete">下載完成</string>
-  <string name="notification_download_failed">下載失敗</string>
-  <string name="notification_filename_extras">" 還有 %d"</string>
-  <string name="notification_filename_separator">", "</string>
-  <string name="permdesc_cacheFilesystem">允許應用程式
-        直接存取、修改及刪除系統快取。惡意的
-        應用程式可能會利用此方式嚴重干擾下載和
-        其它應用程式,及存取私人資料。</string>
-  <string name="permdesc_downloadCompletedIntent">允許應用程式
-        傳送完成下載的通知。惡意的應用程式
-        可能會利用此方式混淆下載
-        檔案的其它應用程式。</string>
-  <string name="permdesc_downloadData">允許應用程式
-        存取下載管理員中所有下載的存取資訊。
-        惡意的應用程式可能會利用此方式嚴重干擾下載,
-        及存取私人資訊。</string>
-  <string name="permdesc_downloadManager">允許應用程式
-        存取下載管理員並使用其下載檔案。
-        惡意的應用程式可能會利用此方式來干擾下載,及存取
-        私人資訊。</string>
-  <string name="permdesc_downloadOtaUpdate">允許應用程式
-        指定其想要下載內部快取中含有 OTA
-        更新專用之檔名的檔案。
-        惡意的應用程式可能會利用此方式來阻止
-        下載 OTA 更新。</string>
-  <string name="permlab_cacheFilesystem">使用系統快取。</string>
-  <string name="permlab_downloadCompletedIntent">傳送下載
-        通知。</string>
-  <string name="permlab_downloadData">存取下載資料。</string>
-  <string name="permlab_downloadManager">存取下載管理員。</string>
-  <string name="permlab_downloadOtaUpdate">下載 OTA 更新。</string>
-</resources>
index 657c92e..3a8fa07 100644 (file)
 -->
 
 <resources>
-    <!-- Used beneath the progress bar to indicate content downloaded -->
-    <string name="download_running">Downloading </string>
-    <!-- Used beneath the progress bar to indicate download about to start -->
-    <string name="download_pending">Starting download\u2026</string>
-    <!-- Used beneath the progress bar to indicate download has not started yet and is waiting for network -->
-    <string name="download_pending_network">Waiting for network\u2026</string>
-    <!-- Used beneath the progress bar to indicate download has started but is paused and is waiting for network -->
-    <string name="download_running_paused">Waiting for network\u2026 </string>
-    <!-- Title to show when the UI doesn't yet know the content that is being downloaded -->
-    <string name="download_unknown_title">&lt;Untitled&gt;</string>
-    
+    <!-- This is the short description of a permission associated with the
+        Android Download Manager. It is displayed as part of the description
+        of any application that was granted that permission.
+        This specific permission controls access to the Download Manager by
+        applications that initiate downloads. -->
     <string name="permlab_downloadManager">Access download manager.</string>
+    <!-- This is the long description of a permission associated with the
+        Android Download Manager. It is displayed as part of the description
+        of any application that was granted that permission.
+        This specific permission controls access to the Download Manager by
+        applications that initiate downloads. -->
     <string name="permdesc_downloadManager">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.</string>
 
-    <string name="permlab_downloadData">Access download data.</string>
-    <string name="permdesc_downloadData">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.</string>
+    <!-- This is the short description of a permission associated with the
+        Android Download Manager. It is displayed as part of the description
+        of any application that was granted that permission.
+        This specific permission controls access to some advanced (and
+        dangerous) features from the Download Manager that are needed by
+        system applications but aren't necessary for regular applications
+        that just initiate plain downloads. -->
+    <string name="permlab_downloadManagerAdvanced">Advanced download
+        manager functions.</string>
+    <!-- This is the long description of a permission associated with the
+        Android Download Manager. It is displayed as part of the description
+        of any application that was granted that permission.
+        This specific permission controls access to some advanced (and
+        dangerous) features from the Download Manager that are needed by
+        system applications but aren't necessary for regular applications
+        that just initiate plain downloads. -->
+    <string name="permdesc_downloadManagerAdvanced">Allows the application to
+        access the download manager's advanced functions.
+        Malicious applications can use this to disrupt downloads and access
+        private information.</string>
 
+    <!-- This is the short description of a permission associated with the
+        Android Download Manager. It is displayed as part of the description
+        of any application that was granted that permission.
+        This specific permission controls access to the Download Manager's
+        private storage area. -->
     <string name="permlab_cacheFilesystem">Use system cache.</string>
+    <!-- This is the long description of a permission associated with the
+        Android Download Manager. It is displayed as part of the description
+        of any application that was granted that permission.
+        This specific permission controls access to the Download Manager's
+        private storage area. -->
     <string name="permdesc_cacheFilesystem">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.</string>
 
-    <string name="permlab_downloadOtaUpdate">Download OTA update.</string>
-    <string name="permdesc_downloadOtaUpdate">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.</string>
-
     <string name="permlab_downloadCompletedIntent">Send download
         notifications.</string>
+    <!-- This is the long description of a permission associated with the
+        Android Download Manager. It is displayed as part of the description
+        of any application that was granted that permission.
+        This specific permission allows an application to tell other
+        applications that their downloads have completed. -->
     <string name="permdesc_downloadCompletedIntent">Allows the application
         to send notifications about completed downloads. Malicious applications
         can use this to confuse other applications that download
         files.</string>
 
-    <!-- used to separate filenames in the download notifications -->
+    <!-- This is the title that is used when displaying the notification
+    for a download that doesn't have a title associated with it. -->
+    <string name="download_unknown_title">&lt;Untitled&gt;</string>
+
+    <!-- When there are multiple simultaneous outstanding downloads from a
+        single application, they are displayed as a single notification,
+        and the expanded notification view displays the first two download
+        names separated with this string, i.e. "[title], [title]"
+        or "[title], [title] and [n] more". This is the comma + space
+        that separates the first two titles, and it's used both when there
+        are exactly two and more than two titles. -->
     <string name="notification_filename_separator">", "</string>
 
-    <!-- used to list that there are more than 2 files in a notification -->
+    <!-- When there are three or more simultaneous outstanding downloads from a
+        single application, they are displayed as a single notification,
+        and the expanded notification view uses this string to indicate
+        downloads beyond the first two, i.e. "[title], [title] and [n] more".
+        This is the " and [n] more" part, including the leading space, and it's
+        used regardless of the number of additional downloads. -->
     <string name="notification_filename_extras">" and %d more"</string>
 
-    <!-- information line shown in the notifications for completed downloads -->
+    <!-- When a download completes, a notification is displayed, and this
+        string is used to indicate that the download successfully completed.
+        Note that such a download could have been initiated by a variety of
+        applications, including (but not limited to) the browser, an email
+        application, a content marketplace. -->
     <string name="notification_download_complete">Download complete</string>
 
-    <!-- information line shown in the notifications for failed downloads -->
+    <!-- When a download completes, a notification is displayed, and this
+        string is used to indicate that the download failed.
+        Note that such a download could have been initiated by a variety of
+        applications, including (but not limited to) the browser, an email
+        application, a content marketplace. -->
     <string name="notification_download_failed">Download unsuccessful</string>
 
 
index f3dd08c..cffda04 100644 (file)
@@ -28,11 +28,26 @@ public class Constants {
     /** Tag used for debugging/logging */
     public static final String TAG = "DownloadManager";
 
-    /** The permission that allows to access data about all downloads */
-    public static final String UI_PERMISSION = "android.permission.ACCESS_DOWNLOAD_DATA";
+    /** The column that used to be used for the HTTP method of the request */
+    public static final String RETRY_AFTER___REDIRECT_COUNT = "method";
 
-    /** The permission that allows to download a system image */
-    public static final String OTA_UPDATE_PERMISSION = "android.permission.DOWNLOAD_OTA_UPDATE";
+    /** The column that used to be used for the magic OTA update filename */
+    public static final String OTA_UPDATE = "otaupdate";
+
+    /** The column that used to be used to reject system filetypes */
+    public static final String NO_SYSTEM_FILES = "no_system";
+
+    /** The column that is used for the downloads's ETag */
+    public static final String ETAG = "etag";
+
+    /** The column that is used for the initiating app's UID */
+    public static final String UID = "uid";
+
+    /** The column that is used to remember whether the media scanner was invoked */
+    public static final String MEDIA_SCANNED = "scanned";
+
+    /** The column that is used to count retries */
+    public static final String FAILED_CONNECTIONS = "numfailed";
 
     /** The intent that gets sent when the service must wake up for a retry */
     public static final String ACTION_RETRY = "android.intent.action.DOWNLOAD_WAKEUP";
@@ -73,9 +88,6 @@ public class Constants {
     /** A magic filename that is allowed to exist within the system cache */
     public static final String RECOVERY_DIRECTORY = "recovery";
 
-    /** The magic filename for OTA updates */
-    public static final String OTA_UPDATE_FILENAME = "update.install";
-
     /** The default user agent used for downloads */
     public static final String DEFAULT_USER_AGENT = "AndroidDownloadManager";
 
@@ -105,6 +117,23 @@ public class Constants {
     public static final int MAX_RETRIES = 5;
 
     /**
+     * The minimum amount of time that the download manager accepts for
+     * a Retry-After response header with a parameter in delta-seconds.
+     */
+    public static final int MIN_RETRY_AFTER = 30; // 30s
+
+    /**
+     * The maximum amount of time that the download manager accepts for
+     * a Retry-After response header with a parameter in delta-seconds.
+     */
+    public static final int MAX_RETRY_AFTER = 24 * 60 * 60; // 24h
+
+    /**
+     * The maximum number of redirects.
+     */
+    public static final int MAX_REDIRECTS = 5; // can't be more than 7.
+
+    /**
      * The time between a failure and the first retry after an IOException.
      * Each subsequent retry grows exponentially, doubling each time.
      * The time is in seconds.
@@ -112,7 +141,7 @@ public class Constants {
     public static final int RETRY_FIRST_DELAY = 30;
 
     /** Enable verbose logging - use with "setprop log.tag.DownloadManager VERBOSE" */
-    private static final boolean LOCAL_LOGV = false;
+    private static final boolean LOCAL_LOGV = true;
     public static final boolean LOGV = Config.LOGV
             || (Config.LOGD && LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE));
 
index b8cead6..e051f41 100644 (file)
@@ -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;
+        }
+    }
 }
index 38cd84f..ed17ab7 100644 (file)
@@ -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);
index c85c94a..d86fdf9 100644 (file)
@@ -22,7 +22,10 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.UriMatcher;
 import android.content.pm.PackageManager;
+import android.database.CrossProcessCursor;
 import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.CursorWrapper;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
@@ -31,25 +34,29 @@ import android.net.Uri;
 import android.os.Binder;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
-import android.provider.BaseColumns;
 import android.provider.Downloads;
 import android.util.Config;
 import android.util.Log;
 
+import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashSet;
+
 
 /**
  * Allows application to interact with the download manager.
  */
 public final class DownloadProvider extends ContentProvider {
 
-    /** Tag used in logging */
-    private static final String TAG = Constants.TAG;
-
     /** Database filename */
     private static final String DB_NAME = "downloads.db";
-    /** Current database vesion */
-    private static final int DB_VERSION = 31;
+    /** Current database version */
+    private static final int DB_VERSION = 100;
+    /** Database version from which upgrading is a nop */
+    private static final int DB_VERSION_NOP_UPGRADE_FROM = 31;
+    /** Database version to which upgrading is a nop */
+    private static final int DB_VERSION_NOP_UPGRADE_TO = 100;
     /** Name of table in the database */
     private static final String DB_TABLE = "downloads";
 
@@ -69,6 +76,31 @@ public final class DownloadProvider extends ContentProvider {
         sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
     }
 
+    private static final String[] sAppReadableColumnsArray = new String[] {
+        Downloads._ID,
+        Downloads.APP_DATA,
+        Downloads._DATA,
+        Downloads.MIMETYPE,
+        Downloads.VISIBILITY,
+        Downloads.CONTROL,
+        Downloads.STATUS,
+        Downloads.LAST_MODIFICATION,
+        Downloads.NOTIFICATION_PACKAGE,
+        Downloads.NOTIFICATION_CLASS,
+        Downloads.TOTAL_BYTES,
+        Downloads.CURRENT_BYTES,
+        Downloads.TITLE,
+        Downloads.DESCRIPTION
+    };
+
+    private static HashSet<String> sAppReadableColumnsSet;
+    static {
+        sAppReadableColumnsSet = new HashSet<String>();
+        for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
+            sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
+        }
+    }
+
     /** The database that lies underneath this content provider */
     private SQLiteOpenHelper mOpenHelper = null;
 
@@ -113,8 +145,16 @@ public final class DownloadProvider extends ContentProvider {
         //       to gracefully handle upgrades we should be careful about
         //       what to do on downgrades.
         @Override
-        public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
-            Log.i(TAG, "Upgrading downloads database from version " + oldV + " to " + newV
+        public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
+            if (oldV == DB_VERSION_NOP_UPGRADE_FROM) {
+                if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op upgrade.
+                    return;
+                }
+                // NOP_FROM and NOP_TO are identical, just in different codelines. Upgrading
+                //     from NOP_FROM is the same as upgrading from NOP_TO.
+                oldV = DB_VERSION_NOP_UPGRADE_TO;
+            }
+            Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV + " to " + newV
                     + ", which will destroy all old data");
             dropTable(db);
             createTable(db);
@@ -159,21 +199,21 @@ public final class DownloadProvider extends ContentProvider {
     private void createTable(SQLiteDatabase db) {
         try {
             db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
-                    BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    Downloads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                     Downloads.URI + " TEXT, " +
-                    Downloads.METHOD + " INTEGER, " +
-                    Downloads.ENTITY + " TEXT, " +
+                    Constants.RETRY_AFTER___REDIRECT_COUNT + " INTEGER, " +
+                    Downloads.APP_DATA + " TEXT, " +
                     Downloads.NO_INTEGRITY + " BOOLEAN, " +
                     Downloads.FILENAME_HINT + " TEXT, " +
-                    Downloads.OTA_UPDATE + " BOOLEAN, " +
-                    Downloads.FILENAME + " TEXT, " +
+                    Constants.OTA_UPDATE + " BOOLEAN, " +
+                    Downloads._DATA + " TEXT, " +
                     Downloads.MIMETYPE + " TEXT, " +
                     Downloads.DESTINATION + " INTEGER, " +
-                    Downloads.NO_SYSTEM_FILES + " BOOLEAN, " +
+                    Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
                     Downloads.VISIBILITY + " INTEGER, " +
                     Downloads.CONTROL + " INTEGER, " +
                     Downloads.STATUS + " INTEGER, " +
-                    Downloads.FAILED_CONNECTIONS + " INTEGER, " +
+                    Constants.FAILED_CONNECTIONS + " INTEGER, " +
                     Downloads.LAST_MODIFICATION + " BIGINT, " +
                     Downloads.NOTIFICATION_PACKAGE + " TEXT, " +
                     Downloads.NOTIFICATION_CLASS + " TEXT, " +
@@ -183,12 +223,12 @@ public final class DownloadProvider extends ContentProvider {
                     Downloads.REFERER + " TEXT, " +
                     Downloads.TOTAL_BYTES + " INTEGER, " +
                     Downloads.CURRENT_BYTES + " INTEGER, " +
-                    Downloads.ETAG + " TEXT, " +
-                    Downloads.UID + " INTEGER, " +
+                    Constants.ETAG + " TEXT, " +
+                    Constants.UID + " INTEGER, " +
                     Downloads.OTHER_UID + " INTEGER, " +
                     Downloads.TITLE + " TEXT, " +
                     Downloads.DESCRIPTION + " TEXT, " +
-                    Downloads.MEDIA_SCANNED + " BOOLEAN);");
+                    Constants.MEDIA_SCANNED + " BOOLEAN);");
         } catch (SQLException ex) {
             Log.e(Constants.TAG, "couldn't create table in downloads database");
             throw ex;
@@ -221,41 +261,73 @@ public final class DownloadProvider extends ContentProvider {
             throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
         }
 
-        boolean hasUID = values.containsKey(Downloads.UID);
-        if (hasUID && Binder.getCallingUid() != 0) {
-            values.remove(Downloads.UID);
-            hasUID = false;
-        }
-        if (!hasUID) {
-            values.put(Downloads.UID, Binder.getCallingUid());
+        ContentValues filteredValues = new ContentValues();
+
+        copyString(Downloads.URI, values, filteredValues);
+        copyString(Downloads.APP_DATA, values, filteredValues);
+        copyBoolean(Downloads.NO_INTEGRITY, values, filteredValues);
+        copyString(Downloads.FILENAME_HINT, values, filteredValues);
+        copyString(Downloads.MIMETYPE, values, filteredValues);
+        Integer i = values.getAsInteger(Downloads.DESTINATION);
+        if (i != null) {
+            if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
+                    != PackageManager.PERMISSION_GRANTED
+                    && i != Downloads.DESTINATION_EXTERNAL
+                    && i != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
+                throw new SecurityException("unauthorized destination code");
+            }
+            filteredValues.put(Downloads.DESTINATION, i);
+            if (i != Downloads.DESTINATION_EXTERNAL &&
+                    values.getAsInteger(Downloads.VISIBILITY) == null) {
+                filteredValues.put(Downloads.VISIBILITY, Downloads.VISIBILITY_HIDDEN);
+            }
         }
-        if (Constants.LOGVV) {
-            Log.v(TAG, "initiating download with UID " + Binder.getCallingUid());
-            if (values.containsKey(Downloads.OTHER_UID)) {
-                Log.v(TAG, "other UID " + values.getAsInteger(Downloads.OTHER_UID));
+        copyInteger(Downloads.VISIBILITY, values, filteredValues);
+        copyInteger(Downloads.CONTROL, values, filteredValues);
+        filteredValues.put(Downloads.STATUS, Downloads.STATUS_PENDING);
+        filteredValues.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
+        String pckg = values.getAsString(Downloads.NOTIFICATION_PACKAGE);
+        String clazz = values.getAsString(Downloads.NOTIFICATION_CLASS);
+        if (pckg != null && clazz != null) {
+            int uid = Binder.getCallingUid();
+            try {
+                if (uid == 0 ||
+                        getContext().getPackageManager().getApplicationInfo(pckg, 0).uid == uid) {
+                    filteredValues.put(Downloads.NOTIFICATION_PACKAGE, pckg);
+                    filteredValues.put(Downloads.NOTIFICATION_CLASS, clazz);
+                }
+            } catch (PackageManager.NameNotFoundException ex) {
+                /* ignored for now */
             }
         }
-
-        if (values.containsKey(Downloads.LAST_MODIFICATION)) {
-            values.remove(Downloads.LAST_MODIFICATION);
+        copyString(Downloads.NOTIFICATION_EXTRAS, values, filteredValues);
+        copyString(Downloads.COOKIE_DATA, values, filteredValues);
+        copyString(Downloads.USER_AGENT, values, filteredValues);
+        copyString(Downloads.REFERER, values, filteredValues);
+        if (getContext().checkCallingPermission(Downloads.PERMISSION_ACCESS_ADVANCED)
+                == PackageManager.PERMISSION_GRANTED) {
+            copyInteger(Downloads.OTHER_UID, values, filteredValues);
         }
-        values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
-
-        if (values.containsKey(Downloads.STATUS)) {
-            values.remove(Downloads.STATUS);
+        filteredValues.put(Constants.UID, Binder.getCallingUid());
+        if (Binder.getCallingUid() == 0) {
+            copyInteger(Constants.UID, values, filteredValues);
         }
-        values.put(Downloads.STATUS, Downloads.STATUS_PENDING);
+        copyString(Downloads.TITLE, values, filteredValues);
+        copyString(Downloads.DESCRIPTION, values, filteredValues);
 
-        if (values.containsKey(Downloads.OTA_UPDATE)
-                && getContext().checkCallingPermission(Constants.OTA_UPDATE_PERMISSION)
-                        != PackageManager.PERMISSION_GRANTED) {
-            values.remove(Downloads.OTA_UPDATE);
+        if (Constants.LOGVV) {
+            Log.v(Constants.TAG, "initiating download with UID "
+                    + filteredValues.getAsInteger(Constants.UID));
+            if (filteredValues.containsKey(Downloads.OTHER_UID)) {
+                Log.v(Constants.TAG, "other UID " +
+                        filteredValues.getAsInteger(Downloads.OTHER_UID));
+            }
         }
 
         Context context = getContext();
         context.startService(new Intent(context, DownloadService.class));
 
-        long rowID = db.insert(DB_TABLE, null, values);
+        long rowID = db.insert(DB_TABLE, null, filteredValues);
 
         Uri ret = null;
 
@@ -265,7 +337,7 @@ public final class DownloadProvider extends ContentProvider {
             context.getContentResolver().notifyChange(uri, null);
         } else {
             if (Config.LOGD) {
-                Log.d(TAG, "couldn't insert into downloads database");
+                Log.d(Constants.TAG, "couldn't insert into downloads database");
             }
         }
 
@@ -276,9 +348,12 @@ public final class DownloadProvider extends ContentProvider {
      * Starts a database query
      */
     @Override
-    public Cursor query(final Uri uri, final String[] projection,
+    public Cursor query(final Uri uri, String[] projection,
              final String selection, final String[] selectionArgs,
              final String sort) {
+
+        Helpers.validateSelection(selection, sAppReadableColumnsSet);
+
         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
 
         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
@@ -292,29 +367,37 @@ public final class DownloadProvider extends ContentProvider {
             }
             case DOWNLOADS_ID: {
                 qb.setTables(DB_TABLE);
-                qb.appendWhere(BaseColumns._ID + "=");
+                qb.appendWhere(Downloads._ID + "=");
                 qb.appendWhere(uri.getPathSegments().get(1));
                 emptyWhere = false;
                 break;
             }
             default: {
                 if (Constants.LOGV) {
-                    Log.v(TAG, "querying unknown URI: " + uri);
+                    Log.v(Constants.TAG, "querying unknown URI: " + uri);
                 }
                 throw new IllegalArgumentException("Unknown URI: " + uri);
             }
         }
 
-        if (Binder.getCallingPid() != Process.myPid()
-                && Binder.getCallingUid() != 0
-                && getContext().checkCallingPermission(Constants.UI_PERMISSION)
-                        != PackageManager.PERMISSION_GRANTED) {
+        if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
             if (!emptyWhere) {
                 qb.appendWhere(" AND ");
             }
-            qb.appendWhere("( " + Downloads.UID + "=" +  Binder.getCallingUid() + " OR "
+            qb.appendWhere("( " + Constants.UID + "=" +  Binder.getCallingUid() + " OR "
                     + Downloads.OTHER_UID + "=" +  Binder.getCallingUid() + " )");
             emptyWhere = false;
+
+            if (projection == null) {
+                projection = sAppReadableColumnsArray;
+            } else {
+                for (int i = 0; i < projection.length; ++i) {
+                    if (!sAppReadableColumnsSet.contains(projection[i])) {
+                        throw new IllegalArgumentException(
+                                "column " + projection[i] + " is not allowed in queries");
+                    }
+                }
+            }
         }
 
         if (Constants.LOGVV) {
@@ -356,13 +439,17 @@ public final class DownloadProvider extends ContentProvider {
             sb.append("sort is ");
             sb.append(sort);
             sb.append(".");
-            Log.v(TAG, sb.toString());
+            Log.v(Constants.TAG, sb.toString());
         }
 
         Cursor ret = qb.query(db, projection, selection, selectionArgs,
                               null, null, sort);
 
         if (ret != null) {
+           ret = new ReadOnlyCursorWrapper(ret);
+        }
+
+        if (ret != null) {
             ret.setNotificationUri(getContext().getContentResolver(), uri);
             if (Constants.LOGVV) {
                 Log.v(Constants.TAG,
@@ -370,7 +457,7 @@ public final class DownloadProvider extends ContentProvider {
             }
         } else {
             if (Constants.LOGV) {
-                Log.v(TAG, "query failed in downloads database");
+                Log.v(Constants.TAG, "query failed in downloads database");
             }
         }
 
@@ -383,12 +470,30 @@ public final class DownloadProvider extends ContentProvider {
     @Override
     public int update(final Uri uri, final ContentValues values,
             final String where, final String[] whereArgs) {
+
+        Helpers.validateSelection(where, sAppReadableColumnsSet);
+
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
         int count;
         long rowId = 0;
-        if (values.containsKey(Downloads.UID)) {
-            values.remove(Downloads.UID);
+        boolean startService = false;
+
+        ContentValues filteredValues;
+        if (Binder.getCallingPid() != Process.myPid()) {
+            filteredValues = new ContentValues();
+            copyString(Downloads.APP_DATA, values, filteredValues);
+            copyInteger(Downloads.VISIBILITY, values, filteredValues);
+            Integer i = values.getAsInteger(Downloads.CONTROL);
+            if (i != null) {
+                filteredValues.put(Downloads.CONTROL, i);
+                startService = true;
+            }
+            copyInteger(Downloads.CONTROL, values, filteredValues);
+            copyString(Downloads.TITLE, values, filteredValues);
+            copyString(Downloads.DESCRIPTION, values, filteredValues);
+        } else {
+            filteredValues = values;
         }
         int match = sURIMatcher.match(uri);
         switch (match) {
@@ -397,9 +502,9 @@ public final class DownloadProvider extends ContentProvider {
                 String myWhere;
                 if (where != null) {
                     if (match == DOWNLOADS) {
-                        myWhere = where;
+                        myWhere = "( " + where + " )";
                     } else {
-                        myWhere = where + " AND ";
+                        myWhere = "( " + where + " ) AND ";
                     }
                 } else {
                     myWhere = "";
@@ -407,26 +512,31 @@ public final class DownloadProvider extends ContentProvider {
                 if (match == DOWNLOADS_ID) {
                     String segment = uri.getPathSegments().get(1);
                     rowId = Long.parseLong(segment);
-                    myWhere += Downloads._ID + " = " + rowId;
+                    myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
                 }
-                if (Binder.getCallingPid() != Process.myPid()
-                        && Binder.getCallingUid() != 0
-                        && getContext().checkCallingPermission(Constants.UI_PERMISSION)
-                                != PackageManager.PERMISSION_GRANTED) {
-                    myWhere += " AND ( " + Downloads.UID + "=" +  Binder.getCallingUid() + " OR "
+                if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
+                    myWhere += " AND ( " + Constants.UID + "=" +  Binder.getCallingUid() + " OR "
                             + Downloads.OTHER_UID + "=" +  Binder.getCallingUid() + " )";
                 }
-                count = db.update(DB_TABLE, values, myWhere, whereArgs);
+                if (filteredValues.size() > 0) {
+                    count = db.update(DB_TABLE, filteredValues, myWhere, whereArgs);
+                } else {
+                    count = 0;
+                }
                 break;
             }
             default: {
                 if (Config.LOGD) {
-                    Log.d(TAG, "updating unknown/invalid URI: " + uri);
+                    Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
                 }
                 throw new UnsupportedOperationException("Cannot update URI: " + uri);
             }
         }
         getContext().getContentResolver().notifyChange(uri, null);
+        if (startService) {
+            Context context = getContext();
+            context.startService(new Intent(context, DownloadService.class));
+        }
         return count;
     }
 
@@ -436,6 +546,9 @@ public final class DownloadProvider extends ContentProvider {
     @Override
     public int delete(final Uri uri, final String where,
             final String[] whereArgs) {
+
+        Helpers.validateSelection(where, sAppReadableColumnsSet);
+
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         int count;
         int match = sURIMatcher.match(uri);
@@ -445,9 +558,9 @@ public final class DownloadProvider extends ContentProvider {
                 String myWhere;
                 if (where != null) {
                     if (match == DOWNLOADS) {
-                        myWhere = where;
+                        myWhere = "( " + where + " )";
                     } else {
-                        myWhere = where + " AND ";
+                        myWhere = "( " + where + " ) AND ";
                     }
                 } else {
                     myWhere = "";
@@ -455,13 +568,10 @@ public final class DownloadProvider extends ContentProvider {
                 if (match == DOWNLOADS_ID) {
                     String segment = uri.getPathSegments().get(1);
                     long rowId = Long.parseLong(segment);
-                    myWhere += Downloads._ID + " = " + rowId;
+                    myWhere += " ( " + Downloads._ID + " = " + rowId + " ) ";
                 }
-                if (Binder.getCallingPid() != Process.myPid()
-                        && Binder.getCallingUid() != 0
-                        && getContext().checkCallingPermission(Constants.UI_PERMISSION)
-                                != PackageManager.PERMISSION_GRANTED) {
-                    myWhere += " AND ( " + Downloads.UID + "=" +  Binder.getCallingUid() + " OR "
+                if (Binder.getCallingPid() != Process.myPid() && Binder.getCallingUid() != 0) {
+                    myWhere += " AND ( " + Constants.UID + "=" +  Binder.getCallingUid() + " OR "
                             + Downloads.OTHER_UID + "=" +  Binder.getCallingUid() + " )";
                 }
                 count = db.delete(DB_TABLE, myWhere, whereArgs);
@@ -469,7 +579,7 @@ public final class DownloadProvider extends ContentProvider {
             }
             default: {
                 if (Config.LOGD) {
-                    Log.d(TAG, "deleting unknown/invalid URI: " + uri);
+                    Log.d(Constants.TAG, "deleting unknown/invalid URI: " + uri);
                 }
                 throw new UnsupportedOperationException("Cannot delete URI: " + uri);
             }
@@ -485,42 +595,75 @@ public final class DownloadProvider extends ContentProvider {
     public ParcelFileDescriptor openFile(Uri uri, String mode)
             throws FileNotFoundException {
         if (Constants.LOGVV) {
-            Log.v(TAG, "openFile uri: " + uri + ", mode: " + mode
+            Log.v(Constants.TAG, "openFile uri: " + uri + ", mode: " + mode
                     + ", uid: " + Binder.getCallingUid());
             Cursor cursor = query(Downloads.CONTENT_URI, new String[] { "_id" }, null, null, "_id");
             if (cursor == null) {
-                Log.v(TAG, "null cursor in openFile");
+                Log.v(Constants.TAG, "null cursor in openFile");
             } else {
                 if (!cursor.moveToFirst()) {
-                    Log.v(TAG, "empty cursor in openFile");
+                    Log.v(Constants.TAG, "empty cursor in openFile");
                 } else {
                     do {
-                        Log.v(TAG, "row " + cursor.getInt(0) + " available");
+                        Log.v(Constants.TAG, "row " + cursor.getInt(0) + " available");
                     } while(cursor.moveToNext());
                 }
                 cursor.close();
             }
             cursor = query(uri, new String[] { "_data" }, null, null, null);
             if (cursor == null) {
-                Log.v(TAG, "null cursor in openFile");
+                Log.v(Constants.TAG, "null cursor in openFile");
             } else {
                 if (!cursor.moveToFirst()) {
-                    Log.v(TAG, "empty cursor in openFile");
+                    Log.v(Constants.TAG, "empty cursor in openFile");
                 } else {
                     String filename = cursor.getString(0);
-                    Log.v(TAG, "filename in openFile: " + filename);
+                    Log.v(Constants.TAG, "filename in openFile: " + filename);
                     if (new java.io.File(filename).isFile()) {
-                        Log.v(TAG, "file exists in openFile");
+                        Log.v(Constants.TAG, "file exists in openFile");
                     }
                 }
                cursor.close();
             }
         }
-        ParcelFileDescriptor ret = openFileHelper(uri, mode);
+
+        // This logic is mostly copied form openFileHelper. If openFileHelper eventually
+        //     gets split into small bits (to extract the filename and the modebits),
+        //     this code could use the separate bits and be deeply simplified.
+        Cursor c = query(uri, new String[]{"_data"}, null, null, null);
+        int count = (c != null) ? c.getCount() : 0;
+        if (count != 1) {
+            // If there is not exactly one result, throw an appropriate exception.
+            if (c != null) {
+                c.close();
+            }
+            if (count == 0) {
+                throw new FileNotFoundException("No entry for " + uri);
+            }
+            throw new FileNotFoundException("Multiple items at " + uri);
+        }
+
+        c.moveToFirst();
+        String path = c.getString(0);
+        c.close();
+        if (path == null) {
+            throw new FileNotFoundException("No filename found.");
+        }
+        if (!Helpers.isFilenameValid(path)) {
+            throw new FileNotFoundException("Invalid filename.");
+        }
+
+        if (!"r".equals(mode)) {
+            throw new FileNotFoundException("Bad mode for " + uri + ": " + mode);
+        }
+        ParcelFileDescriptor ret = ParcelFileDescriptor.open(new File(path),
+                ParcelFileDescriptor.MODE_READ_ONLY);
+
         if (ret == null) {
-            if (Config.LOGD) {
-                Log.d(TAG, "couldn't open file");
+            if (Constants.LOGV) {
+                Log.v(Constants.TAG, "couldn't open file");
             }
+            throw new FileNotFoundException("couldn't open file");
         } else {
             ContentValues values = new ContentValues();
             values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
@@ -529,4 +672,54 @@ public final class DownloadProvider extends ContentProvider {
         return ret;
     }
 
+    private static final void copyInteger(String key, ContentValues from, ContentValues to) {
+        Integer i = from.getAsInteger(key);
+        if (i != null) {
+            to.put(key, i);
+        }
+    }
+
+    private static final void copyBoolean(String key, ContentValues from, ContentValues to) {
+        Boolean b = from.getAsBoolean(key);
+        if (b != null) {
+            to.put(key, b);
+        }
+    }
+
+    private static final void copyString(String key, ContentValues from, ContentValues to) {
+        String s = from.getAsString(key);
+        if (s != null) {
+            to.put(key, s);
+        }
+    }
+
+    private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor {
+        public ReadOnlyCursorWrapper(Cursor cursor) {
+            super(cursor);
+            mCursor = (CrossProcessCursor) cursor;
+        }
+
+        public boolean deleteRow() {
+            throw new SecurityException("Download manager cursors are read-only");
+        }
+
+        public boolean commitUpdates() {
+            throw new SecurityException("Download manager cursors are read-only");
+        }
+
+        public void fillWindow(int pos, CursorWindow window) {
+            mCursor.fillWindow(pos, window);
+        }
+
+        public CursorWindow getWindow() {
+            return mCursor.getWindow();
+        }
+
+        public boolean onMove(int oldPosition, int newPosition) {
+            return mCursor.onMove(oldPosition, newPosition);
+        }
+
+        private CrossProcessCursor mCursor;
+    }
+
 }
index e5bc4e1..03a3718 100644 (file)
@@ -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();
index 0d3650c..d4b5f1e 100644 (file)
@@ -40,7 +40,6 @@ import android.os.Environment;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Process;
-import android.provider.BaseColumns;
 import android.provider.Downloads;
 import android.util.Config;
 import android.util.Log;
@@ -59,9 +58,6 @@ public class DownloadService extends Service {
 
     /* ------------ Constants ------------ */
 
-    /** Tag used for debugging/logging */
-    private static final String TAG = Constants.TAG;
-
     /* ------------ Members ------------ */
 
     /** Observer to get notified when the content observer's data changes */
@@ -130,7 +126,7 @@ public class DownloadService extends Service {
          */
         public void onChange(final boolean selfChange) {
             if (Constants.LOGVV) {
-                Log.v(TAG, "Service ContentObserver received notification");
+                Log.v(Constants.TAG, "Service ContentObserver received notification");
             }
             updateFromProvider();
         }
@@ -144,7 +140,7 @@ public class DownloadService extends Service {
     public class MediaScannerConnection implements ServiceConnection {
         public void onServiceConnected(ComponentName className, IBinder service) {
             if (Constants.LOGVV) {
-                Log.v(TAG, "Connected to Media Scanner");
+                Log.v(Constants.TAG, "Connected to Media Scanner");
             }
             mMediaScannerConnecting = false;
             synchronized (DownloadService.this) {
@@ -160,7 +156,7 @@ public class DownloadService extends Service {
                 if (mMediaScannerService != null) {
                     mMediaScannerService = null;
                     if (Constants.LOGVV) {
-                        Log.v(TAG, "Disconnecting from Media Scanner");
+                        Log.v(Constants.TAG, "Disconnecting from Media Scanner");
                     }
                     try {
                         unbindService(this);
@@ -201,7 +197,7 @@ public class DownloadService extends Service {
     public void onCreate() {
         super.onCreate();
         if (Constants.LOGVV) {
-            Log.v(TAG, "Service onCreate");
+            Log.v(Constants.TAG, "Service onCreate");
         }
 
         mDownloads = Lists.newArrayList();
@@ -229,7 +225,7 @@ public class DownloadService extends Service {
     public void onStart(Intent intent, int startId) {
         super.onStart(intent, startId);
         if (Constants.LOGVV) {
-            Log.v(TAG, "Service onStart");
+            Log.v(Constants.TAG, "Service onStart");
         }
 
         updateFromProvider();
@@ -241,7 +237,7 @@ public class DownloadService extends Service {
     public void onDestroy() {
         getContentResolver().unregisterContentObserver(mObserver);
         if (Constants.LOGVV) {
-            Log.v(TAG, "Service onDestroy");
+            Log.v(Constants.TAG, "Service onDestroy");
         }
         super.onDestroy();
     }
@@ -308,10 +304,11 @@ public class DownloadService extends Service {
                     pendingUpdate = false;
                 }
                 boolean networkAvailable = Helpers.isNetworkAvailable(DownloadService.this);
+                boolean networkRoaming = Helpers.isNetworkRoaming(DownloadService.this);
                 long now = System.currentTimeMillis();
 
                 Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
-                        null, null, null, BaseColumns._ID);
+                        null, null, null, Downloads._ID);
 
                 if (cursor == null) {
                     return;
@@ -327,7 +324,7 @@ public class DownloadService extends Service {
 
                 boolean isAfterLast = cursor.isAfterLast();
 
-                int idColumn = cursor.getColumnIndexOrThrow(BaseColumns._ID);
+                int idColumn = cursor.getColumnIndexOrThrow(Downloads._ID);
 
                 /*
                  * Walk the cursor and the local array to keep them in sync. The key
@@ -352,7 +349,8 @@ public class DownloadService extends Service {
                         //     stuff in the local array, which can only be junk
                         if (Constants.LOGVV) {
                             int arrayId = ((DownloadInfo) mDownloads.get(arrayPos)).id;
-                            Log.v(TAG, "Array update: trimming " + arrayId + " @ "  + arrayPos);
+                            Log.v(Constants.TAG, "Array update: trimming " +
+                                    arrayId + " @ "  + arrayPos);
                         }
                         if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
                             scanFile(null, arrayPos);
@@ -362,9 +360,10 @@ public class DownloadService extends Service {
                         int id = cursor.getInt(idColumn);
 
                         if (arrayPos == mDownloads.size()) {
-                            insertDownload(cursor, arrayPos, networkAvailable, now);
+                            insertDownload(cursor, arrayPos, networkAvailable, networkRoaming, now);
                             if (Constants.LOGVV) {
-                                Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
+                                Log.v(Constants.TAG, "Array update: inserting " +
+                                        id + " @ " + arrayPos);
                             }
                             if (shouldScanFile(arrayPos)
                                     && (!mediaScannerConnected() || !scanFile(cursor, arrayPos))) {
@@ -389,7 +388,7 @@ public class DownloadService extends Service {
                             if (arrayId < id) {
                                 // The array entry isn't in the cursor
                                 if (Constants.LOGVV) {
-                                    Log.v(TAG, "Array update: removing " + arrayId
+                                    Log.v(Constants.TAG, "Array update: removing " + arrayId
                                             + " @ " + arrayPos);
                                 }
                                 if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
@@ -398,7 +397,9 @@ public class DownloadService extends Service {
                                 deleteDownload(arrayPos); // this advances in the array
                             } else if (arrayId == id) {
                                 // This cursor row already exists in the stored array
-                                updateDownload(cursor, arrayPos, networkAvailable, now);
+                                updateDownload(
+                                        cursor, arrayPos,
+                                        networkAvailable, networkRoaming, now);
                                 if (shouldScanFile(arrayPos)
                                         && (!mediaScannerConnected()
                                                 || !scanFile(cursor, arrayPos))) {
@@ -420,9 +421,12 @@ public class DownloadService extends Service {
                             } else {
                                 // This cursor entry didn't exist in the stored array
                                 if (Constants.LOGVV) {
-                                    Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
+                                    Log.v(Constants.TAG, "Array update: appending " +
+                                            id + " @ " + arrayPos);
                                 }
-                                insertDownload(cursor, arrayPos, networkAvailable, now);
+                                insertDownload(
+                                        cursor, arrayPos,
+                                        networkAvailable, networkRoaming, now);
                                 if (shouldScanFile(arrayPos)
                                         && (!mediaScannerConnected()
                                                 || !scanFile(cursor, arrayPos))) {
@@ -460,9 +464,6 @@ public class DownloadService extends Service {
                     mMediaScannerConnection.disconnectMediaScanner();
                 }
 
-                if (!cursor.commitUpdates()) {
-                    Log.e(Constants.TAG, "commitUpdates failed in updateFromProvider");
-                }
                 cursor.close();
             }
         }
@@ -490,7 +491,7 @@ public class DownloadService extends Service {
         }
 
         Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
-                new String[] { Downloads.FILENAME }, null, null, null);
+                new String[] { Downloads._DATA }, null, null, null);
         if (cursor != null) {
             if (cursor.moveToFirst()) {
                 do {
@@ -515,16 +516,24 @@ public class DownloadService extends Service {
     private void trimDatabase() {
         Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
                 new String[] { Downloads._ID },
-                Downloads.STATUS + " >= 200", null,
+                Downloads.STATUS + " >= '200'", null,
                 Downloads.LAST_MODIFICATION);
         if (cursor == null) {
             // This isn't good - if we can't do basic queries in our database, nothing's gonna work
-            Log.e(TAG, "null cursor in trimDatabase");
+            Log.e(Constants.TAG, "null cursor in trimDatabase");
             return;
         }
         if (cursor.moveToFirst()) {
-            while (cursor.getCount() > Constants.MAX_DOWNLOADS) {
-                cursor.deleteRow();
+            int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
+            int columnId = cursor.getColumnIndexOrThrow(Downloads._ID);
+            while (numDelete > 0) {
+                getContentResolver().delete(
+                        ContentUris.withAppendedId(Downloads.CONTENT_URI, cursor.getLong(columnId)),
+                        null, null);
+                if (!cursor.moveToNext()) {
+                    break;
+                }
+                numDelete--;
             }
         }
         cursor.close();
@@ -534,25 +543,27 @@ public class DownloadService extends Service {
      * Keeps a local copy of the info about a download, and initiates the
      * download if appropriate.
      */
-    private void insertDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
+    private void insertDownload(
+            Cursor cursor, int arrayPos,
+            boolean networkAvailable, boolean networkRoaming, long now) {
         int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
-        int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
+        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
+        int retryRedirect =
+                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER___REDIRECT_COUNT));
         DownloadInfo info = new DownloadInfo(
                 cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID)),
                 cursor.getString(cursor.getColumnIndexOrThrow(Downloads.URI)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.METHOD)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.ENTITY)),
                 cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1,
                 cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME_HINT)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.FILENAME)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.OTA_UPDATE)) == 1,
+                cursor.getString(cursor.getColumnIndexOrThrow(Downloads._DATA)),
                 cursor.getString(cursor.getColumnIndexOrThrow(Downloads.MIMETYPE)),
                 cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_SYSTEM_FILES)) == 1,
                 cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY)),
                 cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL)),
                 cursor.getInt(statusColumn),
                 cursor.getInt(failedColumn),
+                retryRedirect & 0xfffffff,
+                retryRedirect >> 28,
                 cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION)),
                 cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_PACKAGE)),
                 cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_CLASS)),
@@ -562,36 +573,34 @@ public class DownloadService extends Service {
                 cursor.getString(cursor.getColumnIndexOrThrow(Downloads.REFERER)),
                 cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.TOTAL_BYTES)),
                 cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CURRENT_BYTES)),
-                cursor.getString(cursor.getColumnIndexOrThrow(Downloads.ETAG)),
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1);
+                cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG)),
+                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1);
 
         if (Constants.LOGVV) {
-            Log.v(TAG, "Service adding new entry");
-            Log.v(TAG, "ID      : " + info.id);
-            Log.v(TAG, "URI     : " + ((info.uri != null) ? "yes" : "no"));
-            Log.v(TAG, "METHOD  : " + info.method);
-            Log.v(TAG, "ENTITY  : " + ((info.entity != null) ? "yes" : "no"));
-            Log.v(TAG, "NO_INTEG: " + info.noIntegrity);
-            Log.v(TAG, "HINT    : " + info.hint);
-            Log.v(TAG, "FILENAME: " + info.filename);
-            Log.v(TAG, "SYSIMAGE: " + info.otaUpdate);
-            Log.v(TAG, "MIMETYPE: " + info.mimetype);
-            Log.v(TAG, "DESTINAT: " + info.destination);
-            Log.v(TAG, "NO_SYSTE: " + info.noSystem);
-            Log.v(TAG, "VISIBILI: " + info.visibility);
-            Log.v(TAG, "CONTROL : " + info.control);
-            Log.v(TAG, "STATUS  : " + info.status);
-            Log.v(TAG, "FAILED_C: " + info.numFailed);
-            Log.v(TAG, "LAST_MOD: " + info.lastMod);
-            Log.v(TAG, "PACKAGE : " + info.pckg);
-            Log.v(TAG, "CLASS   : " + info.clazz);
-            Log.v(TAG, "COOKIES : " + ((info.cookies != null) ? "yes" : "no"));
-            Log.v(TAG, "AGENT   : " + info.userAgent);
-            Log.v(TAG, "REFERER : " + ((info.referer != null) ? "yes" : "no"));
-            Log.v(TAG, "TOTAL   : " + info.totalBytes);
-            Log.v(TAG, "CURRENT : " + info.currentBytes);
-            Log.v(TAG, "ETAG    : " + info.etag);
-            Log.v(TAG, "SCANNED : " + info.mediaScanned);
+            Log.v(Constants.TAG, "Service adding new entry");
+            Log.v(Constants.TAG, "ID      : " + info.id);
+            Log.v(Constants.TAG, "URI     : " + ((info.uri != null) ? "yes" : "no"));
+            Log.v(Constants.TAG, "NO_INTEG: " + info.noIntegrity);
+            Log.v(Constants.TAG, "HINT    : " + info.hint);
+            Log.v(Constants.TAG, "FILENAME: " + info.filename);
+            Log.v(Constants.TAG, "MIMETYPE: " + info.mimetype);
+            Log.v(Constants.TAG, "DESTINAT: " + info.destination);
+            Log.v(Constants.TAG, "VISIBILI: " + info.visibility);
+            Log.v(Constants.TAG, "CONTROL : " + info.control);
+            Log.v(Constants.TAG, "STATUS  : " + info.status);
+            Log.v(Constants.TAG, "FAILED_C: " + info.numFailed);
+            Log.v(Constants.TAG, "RETRY_AF: " + info.retryAfter);
+            Log.v(Constants.TAG, "REDIRECT: " + info.redirectCount);
+            Log.v(Constants.TAG, "LAST_MOD: " + info.lastMod);
+            Log.v(Constants.TAG, "PACKAGE : " + info.pckg);
+            Log.v(Constants.TAG, "CLASS   : " + info.clazz);
+            Log.v(Constants.TAG, "COOKIES : " + ((info.cookies != null) ? "yes" : "no"));
+            Log.v(Constants.TAG, "AGENT   : " + info.userAgent);
+            Log.v(Constants.TAG, "REFERER : " + ((info.referer != null) ? "yes" : "no"));
+            Log.v(Constants.TAG, "TOTAL   : " + info.totalBytes);
+            Log.v(Constants.TAG, "CURRENT : " + info.currentBytes);
+            Log.v(Constants.TAG, "ETAG    : " + info.etag);
+            Log.v(Constants.TAG, "SCANNED : " + info.mediaScanned);
         }
 
         mDownloads.add(arrayPos, info);
@@ -616,29 +625,28 @@ public class DownloadService extends Service {
             mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mimetype);
             List<ResolveInfo> list = getPackageManager().queryIntentActivities(mimetypeIntent,
                     PackageManager.MATCH_DEFAULT_ONLY);
-            //Log.i(TAG, "*** QUERY " + mimetypeIntent + ": " + list);
+            //Log.i(Constants.TAG, "*** QUERY " + mimetypeIntent + ": " + list);
             
-            if (list.size() == 0
-                    || (info.noSystem && info.mimetype.equalsIgnoreCase(Constants.MIMETYPE_APK))) {
+            if (list.size() == 0) {
                 if (Config.LOGD) {
                     Log.d(Constants.TAG, "no application to handle MIME type " + info.mimetype);
                 }
                 info.status = Downloads.STATUS_NOT_ACCEPTABLE;
-                cursor.updateInt(statusColumn, Downloads.STATUS_NOT_ACCEPTABLE);
 
-                Uri uri = Uri.parse(Downloads.CONTENT_URI + "/" + info.id);
-                Intent intent = new Intent(Downloads.DOWNLOAD_COMPLETED_ACTION);
-                intent.setData(uri);
-                sendBroadcast(intent, "android.permission.ACCESS_DOWNLOAD_DATA");
+                Uri uri = ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id);
+                ContentValues values = new ContentValues();
+                values.put(Downloads.STATUS, Downloads.STATUS_NOT_ACCEPTABLE);
+                getContentResolver().update(uri, values, null, null);
                 info.sendIntentIfRequested(uri, this);
                 return;
             }
         }
 
-        if (networkAvailable) {
+        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
             if (info.isReadyToStart(now)) {
                 if (Constants.LOGV) {
-                    Log.v(TAG, "Service spawning thread to handle new download " + info.id);
+                    Log.v(Constants.TAG, "Service spawning thread to handle new download " +
+                            info.id);
                 }
                 if (info.hasActiveThread) {
                     throw new IllegalStateException("Multiple threads on same download on insert");
@@ -660,7 +668,10 @@ public class DownloadService extends Service {
                     || info.status == Downloads.STATUS_PENDING
                     || info.status == Downloads.STATUS_RUNNING) {
                 info.status = Downloads.STATUS_RUNNING_PAUSED;
-                cursor.updateInt(statusColumn, Downloads.STATUS_RUNNING_PAUSED);
+                Uri uri = ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id);
+                ContentValues values = new ContentValues();
+                values.put(Downloads.STATUS, Downloads.STATUS_RUNNING_PAUSED);
+                getContentResolver().update(uri, values, null, null);
             }
         }
     }
@@ -668,23 +679,20 @@ public class DownloadService extends Service {
     /**
      * Updates the local copy of the info about a download.
      */
-    private void updateDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
+    private void updateDownload(
+            Cursor cursor, int arrayPos,
+            boolean networkAvailable, boolean networkRoaming, long now) {
         DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
         int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
-        int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
+        int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
         info.id = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads._ID));
         info.uri = stringFromCursor(info.uri, cursor, Downloads.URI);
-        info.method = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.METHOD));
-        info.entity = stringFromCursor(info.entity, cursor, Downloads.ENTITY);
         info.noIntegrity =
                 cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_INTEGRITY)) == 1;
         info.hint = stringFromCursor(info.hint, cursor, Downloads.FILENAME_HINT);
-        info.filename = stringFromCursor(info.filename, cursor, Downloads.FILENAME);
-        info.otaUpdate = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.OTA_UPDATE)) == 1;
+        info.filename = stringFromCursor(info.filename, cursor, Downloads._DATA);
         info.mimetype = stringFromCursor(info.mimetype, cursor, Downloads.MIMETYPE);
         info.destination = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.DESTINATION));
-        info.noSystem =
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.NO_SYSTEM_FILES)) == 1;
         int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.VISIBILITY));
         if (info.visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
                 && newVisibility != Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
@@ -692,13 +700,19 @@ public class DownloadService extends Service {
             mNotifier.mNotificationMgr.cancel(info.id);
         }
         info.visibility = newVisibility;
-        info.control = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL));
+        synchronized(info) {
+            info.control = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CONTROL));
+        }
         int newStatus = cursor.getInt(statusColumn);
         if (!Downloads.isStatusCompleted(info.status) && Downloads.isStatusCompleted(newStatus)) {
             mNotifier.mNotificationMgr.cancel(info.id);
         }
         info.status = newStatus;
         info.numFailed = cursor.getInt(failedColumn);
+        int retryRedirect =
+                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER___REDIRECT_COUNT));
+        info.retryAfter = retryRedirect & 0xfffffff;
+        info.redirectCount = retryRedirect >> 28;
         info.lastMod = cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION));
         info.pckg = stringFromCursor(info.pckg, cursor, Downloads.NOTIFICATION_PACKAGE);
         info.clazz = stringFromCursor(info.clazz, cursor, Downloads.NOTIFICATION_CLASS);
@@ -707,14 +721,15 @@ public class DownloadService extends Service {
         info.referer = stringFromCursor(info.referer, cursor, Downloads.REFERER);
         info.totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.TOTAL_BYTES));
         info.currentBytes = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.CURRENT_BYTES));
-        info.etag = stringFromCursor(info.etag, cursor, Downloads.ETAG);
+        info.etag = stringFromCursor(info.etag, cursor, Constants.ETAG);
         info.mediaScanned =
-                cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1;
+                cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
 
-        if (networkAvailable) {
+        if (info.canUseNetwork(networkAvailable, networkRoaming)) {
             if (info.isReadyToRestart(now)) {
                 if (Constants.LOGV) {
-                    Log.v(TAG, "Service spawning thread to handle updated download " + info.id);
+                    Log.v(Constants.TAG, "Service spawning thread to handle updated download " +
+                            info.id);
                 }
                 if (info.hasActiveThread) {
                     throw new IllegalStateException("Multiple threads on same download on update");
@@ -839,16 +854,21 @@ public class DownloadService extends Service {
             if (mMediaScannerService != null) {
                 try {
                     if (Constants.LOGV) {
-                        Log.v(TAG, "Scanning file " + info.filename);
+                        Log.v(Constants.TAG, "Scanning file " + info.filename);
                     }
                     mMediaScannerService.scanFile(info.filename, info.mimetype);
                     if (cursor != null) {
-                        cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED), 1);
+                        ContentValues values = new ContentValues();
+                        values.put(Constants.MEDIA_SCANNED, 1);
+                        getContentResolver().update(
+                                ContentUris.withAppendedId(Downloads.CONTENT_URI,
+                                       cursor.getLong(cursor.getColumnIndexOrThrow(Downloads._ID))),
+                                values, null, null);
                     }
                     return true;
                 } catch (RemoteException e) {
                     if (Config.LOGD) {
-                        Log.d(TAG, "Failed to scan file " + info.filename);
+                        Log.d(Constants.TAG, "Failed to scan file " + info.filename);
                     }
                 }
             }
index 66417b3..923e36d 100644 (file)
@@ -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);
     }
 
index f966a7f..89a5731 100644 (file)
@@ -16,6 +16,7 @@
 
 package com.android.providers.downloads;
 
+import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -27,7 +28,9 @@ import android.net.NetworkInfo;
 import android.net.Uri;
 import android.os.Environment;
 import android.os.StatFs;
+import android.os.SystemClock;
 import android.provider.Downloads;
+import android.telephony.TelephonyManager;
 import android.util.Config;
 import android.util.Log;
 import android.webkit.MimeTypeMap;
@@ -39,14 +42,15 @@ import java.util.List;
 import java.util.Random;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.Set;
 
 /**
  * Some helper functions for the download manager
  */
 public class Helpers {
-    /** Tag used for debugging/logging */
-    private static final String TAG = Constants.TAG;
+
+    public static Random rnd = new Random(SystemClock.uptimeMillis());
+
     /** Regex used to parse content-disposition headers */
     private static final Pattern CONTENT_DISPOSITION_PATTERN =
             Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
@@ -83,8 +87,6 @@ public class Helpers {
             String contentLocation,
             String mimeType,
             int destination,
-            boolean otaUpdate,
-            boolean noSystem,
             int contentLength) throws FileNotFoundException {
 
         /*
@@ -94,13 +96,7 @@ public class Helpers {
                 || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
             if (mimeType == null) {
                 if (Config.LOGD) {
-                    Log.d(TAG, "external download with no mime type not allowed");
-                }
-                return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
-            }
-            if (noSystem && mimeType.equalsIgnoreCase(Constants.MIMETYPE_APK)) {
-                if (Config.LOGD) {
-                    Log.d(TAG, "system files not allowed by initiating application");
+                    Log.d(Constants.TAG, "external download with no mime type not allowed");
                 }
                 return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
             }
@@ -121,7 +117,7 @@ public class Helpers {
                 intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
                 List<ResolveInfo> list = pm.queryIntentActivities(intent,
                         PackageManager.MATCH_DEFAULT_ONLY);
-                //Log.i(TAG, "*** FILENAME QUERY " + intent + ": " + list);
+                //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list);
 
                 if (list.size() == 0) {
                     if (Config.LOGD) {
@@ -132,7 +128,7 @@ public class Helpers {
             }
         }
         String filename = chooseFilename(
-                url, hint, contentDisposition, contentLocation, destination, otaUpdate);
+                url, hint, contentDisposition, contentLocation, destination);
 
         // Split filename between base and extension
         // Add an extension if filename does not have one
@@ -142,7 +138,7 @@ public class Helpers {
             extension = chooseExtensionFromMimeType(mimeType, true);
         } else {
             extension = chooseExtensionFromFilename(
-                    mimeType, destination, otaUpdate, filename, dotIndex);
+                    mimeType, destination, filename, dotIndex);
             filename = filename.substring(0, dotIndex);
         }
 
@@ -156,6 +152,7 @@ public class Helpers {
         // the DRM content provider
         if (destination == Downloads.DESTINATION_CACHE_PARTITION
                 || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE
+                || destination == Downloads.DESTINATION_CACHE_PARTITION_NOROAMING
                 || DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
             base = Environment.getDownloadCacheDirectory();
             stat = new StatFs(base.getPath());
@@ -173,7 +170,8 @@ public class Helpers {
                 if (!discardPurgeableFiles(context,
                         contentLength - blockSize * ((long) availableBlocks - 4))) {
                     if (Config.LOGD) {
-                        Log.d(TAG, "download aborted - not enough free space in internal storage");
+                        Log.d(Constants.TAG,
+                                "download aborted - not enough free space in internal storage");
                     }
                     return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
                 }
@@ -181,34 +179,22 @@ public class Helpers {
             }
 
         } else {
-            if (destination == Downloads.DESTINATION_DATA_CACHE) {
-                base = context.getCacheDir();
+            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+                String root = Environment.getExternalStorageDirectory().getPath();
+                base = new File(root + Constants.DEFAULT_DL_SUBDIR);
                 if (!base.isDirectory() && !base.mkdir()) {
-                      if (Config.LOGD) {
-                        Log.d(TAG, "download aborted - can't create base directory "
+                    if (Config.LOGD) {
+                        Log.d(Constants.TAG, "download aborted - can't create base directory "
                                 + base.getPath());
                     }
                     return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
                 }
                 stat = new StatFs(base.getPath());
             } else {
-                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
-                    String root = Environment.getExternalStorageDirectory().getPath();
-                    base = new File(root + Constants.DEFAULT_DL_SUBDIR);
-                    if (!base.isDirectory() && !base.mkdir()) {
-                        if (Config.LOGD) {
-                            Log.d(TAG, "download aborted - can't create base directory "
-                                    + base.getPath());
-                        }
-                        return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
-                    }
-                    stat = new StatFs(base.getPath());
-                } else {
-                    if (Config.LOGD) {
-                        Log.d(TAG, "download aborted - no external storage");
-                    }
-                    return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+                if (Config.LOGD) {
+                    Log.d(Constants.TAG, "download aborted - no external storage");
                 }
+                return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
             }
 
             /*
@@ -217,14 +203,13 @@ public class Helpers {
              */
             if (stat.getBlockSize() * ((long) stat.getAvailableBlocks() - 4) < contentLength) {
                 if (Config.LOGD) {
-                    Log.d(TAG, "download aborted - not enough free space");
+                    Log.d(Constants.TAG, "download aborted - not enough free space");
                 }
                 return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
             }
 
         }
 
-        boolean otaFilename = Constants.OTA_UPDATE_FILENAME.equalsIgnoreCase(filename + extension);
         boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
 
         filename = base.getPath() + File.separator + filename;
@@ -233,11 +218,11 @@ public class Helpers {
          * Generate a unique filename, create the file, return it.
          */
         if (Constants.LOGVV) {
-            Log.v(TAG, "target file: " + filename + extension);
+            Log.v(Constants.TAG, "target file: " + filename + extension);
         }
 
         String fullFilename = chooseUniqueFilename(
-                destination, otaUpdate, filename, extension, otaFilename, recoveryDir);
+                destination, filename, extension, recoveryDir);
         if (fullFilename != null) {
             return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
         } else {
@@ -246,18 +231,13 @@ public class Helpers {
     }
 
     private static String chooseFilename(String url, String hint, String contentDisposition,
-            String contentLocation, int destination, boolean otaUpdate) {
+            String contentLocation, int destination) {
         String filename = null;
 
-        // Before we even start, special-case the OTA updates
-        if (destination == Downloads.DESTINATION_CACHE_PARTITION && otaUpdate) {
-            filename = Constants.OTA_UPDATE_FILENAME;
-        }
-
         // First, try to use the hint from the application, if there's one
         if (filename == null && hint != null && !hint.endsWith("/")) {
             if (Constants.LOGVV) {
-                Log.v(TAG, "getting filename from hint");
+                Log.v(Constants.TAG, "getting filename from hint");
             }
             int index = hint.lastIndexOf('/') + 1;
             if (index > 0) {
@@ -272,7 +252,7 @@ public class Helpers {
             filename = parseContentDisposition(contentDisposition);
             if (filename != null) {
                 if (Constants.LOGVV) {
-                    Log.v(TAG, "getting filename from content-disposition");
+                    Log.v(Constants.TAG, "getting filename from content-disposition");
                 }
                 int index = filename.lastIndexOf('/') + 1;
                 if (index > 0) {
@@ -288,7 +268,7 @@ public class Helpers {
                     && !decodedContentLocation.endsWith("/")
                     && decodedContentLocation.indexOf('?') < 0) {
                 if (Constants.LOGVV) {
-                    Log.v(TAG, "getting filename from content-location");
+                    Log.v(Constants.TAG, "getting filename from content-location");
                 }
                 int index = decodedContentLocation.lastIndexOf('/') + 1;
                 if (index > 0) {
@@ -307,7 +287,7 @@ public class Helpers {
                 int index = decodedUrl.lastIndexOf('/') + 1;
                 if (index > 0) {
                     if (Constants.LOGVV) {
-                        Log.v(TAG, "getting filename from uri");
+                        Log.v(Constants.TAG, "getting filename from uri");
                     }
                     filename = decodedUrl.substring(index);
                 }
@@ -317,10 +297,14 @@ public class Helpers {
         // Finally, if couldn't get filename from URI, get a generic filename
         if (filename == null) {
             if (Constants.LOGVV) {
-                Log.v(TAG, "using default filename");
+                Log.v(Constants.TAG, "using default filename");
             }
             filename = Constants.DEFAULT_DL_FILENAME;
         }
+
+        filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_");
+
+
         return filename;
     }
 
@@ -330,12 +314,12 @@ public class Helpers {
             extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
             if (extension != null) {
                 if (Constants.LOGVV) {
-                    Log.v(TAG, "adding extension from type");
+                    Log.v(Constants.TAG, "adding extension from type");
                 }
                 extension = "." + extension;
             } else {
                 if (Constants.LOGVV) {
-                    Log.v(TAG, "couldn't find extension for " + mimeType);
+                    Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
                 }
             }
         }
@@ -343,18 +327,18 @@ public class Helpers {
             if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
                 if (mimeType.equalsIgnoreCase("text/html")) {
                     if (Constants.LOGVV) {
-                        Log.v(TAG, "adding default html extension");
+                        Log.v(Constants.TAG, "adding default html extension");
                     }
                     extension = Constants.DEFAULT_DL_HTML_EXTENSION;
                 } else if (useDefaults) {
                     if (Constants.LOGVV) {
-                        Log.v(TAG, "adding default text extension");
+                        Log.v(Constants.TAG, "adding default text extension");
                     }
                     extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
                 }
             } else if (useDefaults) {
                 if (Constants.LOGVV) {
-                    Log.v(TAG, "adding default binary extension");
+                    Log.v(Constants.TAG, "adding default binary extension");
                 }
                 extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
             }
@@ -363,10 +347,9 @@ public class Helpers {
     }
 
     private static String chooseExtensionFromFilename(String mimeType, int destination,
-            boolean otaUpdate, String filename, int dotIndex) {
+            String filename, int dotIndex) {
         String extension = null;
-        if (mimeType != null
-                && !(destination == Downloads.DESTINATION_CACHE_PARTITION && otaUpdate)) {
+        if (mimeType != null) {
             // Compare the last segment of the extension against the mime type.
             // If there's a mismatch, discard the entire extension.
             int lastDotIndex = filename.lastIndexOf('.');
@@ -376,63 +359,60 @@ public class Helpers {
                 extension = chooseExtensionFromMimeType(mimeType, false);
                 if (extension != null) {
                     if (Constants.LOGVV) {
-                        Log.v(TAG, "substituting extension from type");
+                        Log.v(Constants.TAG, "substituting extension from type");
                     }
                 } else {
                     if (Constants.LOGVV) {
-                        Log.v(TAG, "couldn't find extension for " + mimeType);
+                        Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
                     }
                 }
             }
         }
         if (extension == null) {
             if (Constants.LOGVV) {
-                Log.v(TAG, "keeping extension");
+                Log.v(Constants.TAG, "keeping extension");
             }
             extension = filename.substring(dotIndex);
         }
         return extension;
     }
 
-    private static String chooseUniqueFilename(int destination, boolean otaUpdate, String filename,
-            String extension, boolean otaFilename, boolean recoveryDir) {
+    private static String chooseUniqueFilename(int destination, String filename,
+            String extension, boolean recoveryDir) {
         String fullFilename = filename + extension;
         if (!new File(fullFilename).exists()
                 && (!recoveryDir ||
                 (destination != Downloads.DESTINATION_CACHE_PARTITION &&
-                        destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE))
-                && (!otaFilename ||
-                (otaUpdate && destination == Downloads.DESTINATION_CACHE_PARTITION))) {
+                        destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE &&
+                        destination != Downloads.DESTINATION_CACHE_PARTITION_NOROAMING))) {
             return fullFilename;
-        } else if (!(otaUpdate && destination == Downloads.DESTINATION_CACHE_PARTITION)) {
-            filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
-            /*
-            * This number is used to generate partially randomized filenames to avoid
-            * collisions.
-            * It starts at 1.
-            * The next 9 iterations increment it by 1 at a time (up to 10).
-            * The next 9 iterations increment it by 1 to 10 (random) at a time.
-            * The next 9 iterations increment it by 1 to 100 (random) at a time.
-            * ... Up to the point where it increases by 100000000 at a time.
-            * (the maximum value that can be reached is 1000000000)
-            * As soon as a number is reached that generates a filename that doesn't exist,
-            *     that filename is used.
-            * If the filename coming in is [base].[ext], the generated filenames are
-            *     [base]-[sequence].[ext].
-            */
-            int sequence = 1;
-            Random random = new Random();
-            for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
-                for (int iteration = 0; iteration < 9; ++iteration) {
-                    fullFilename = filename + sequence + extension;
-                    if (!new File(fullFilename).exists()) {
-                        return fullFilename;
-                    }
-                    if (Constants.LOGVV) {
-                        Log.v(TAG, "file with sequence number " + sequence + " exists");
-                    }
-                    sequence += random.nextInt(magnitude) + 1;
+        }
+        filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
+        /*
+        * This number is used to generate partially randomized filenames to avoid
+        * collisions.
+        * It starts at 1.
+        * The next 9 iterations increment it by 1 at a time (up to 10).
+        * The next 9 iterations increment it by 1 to 10 (random) at a time.
+        * The next 9 iterations increment it by 1 to 100 (random) at a time.
+        * ... Up to the point where it increases by 100000000 at a time.
+        * (the maximum value that can be reached is 1000000000)
+        * As soon as a number is reached that generates a filename that doesn't exist,
+        *     that filename is used.
+        * If the filename coming in is [base].[ext], the generated filenames are
+        *     [base]-[sequence].[ext].
+        */
+        int sequence = 1;
+        for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
+            for (int iteration = 0; iteration < 9; ++iteration) {
+                fullFilename = filename + sequence + extension;
+                if (!new File(fullFilename).exists()) {
+                    return fullFilename;
+                }
+                if (Constants.LOGVV) {
+                    Log.v(Constants.TAG, "file with sequence number " + sequence + " exists");
                 }
+                sequence += rnd.nextInt(magnitude) + 1;
             }
         }
         return null;
@@ -460,15 +440,17 @@ public class Helpers {
         try {
             cursor.moveToFirst();
             while (!cursor.isAfterLast() && totalFreed < targetBytes) {
-                File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.FILENAME)));
+                File file = new File(cursor.getString(cursor.getColumnIndex(Downloads._DATA)));
                 if (Constants.LOGVV) {
                     Log.v(Constants.TAG, "purging " + file.getAbsolutePath() + " for " +
                             file.length() + " bytes");
                 }
                 totalFreed += file.length();
                 file.delete();
-                cursor.deleteRow(); // This moves the cursor to the next entry,
-                                    //         no need to call next()
+                long id = cursor.getLong(cursor.getColumnIndex(Downloads._ID));
+                context.getContentResolver().delete(
+                        ContentUris.withAppendedId(Downloads.CONTENT_URI, id), null, null);
+                cursor.moveToNext();
             }
         } finally {
             cursor.close();
@@ -491,20 +473,322 @@ public class Helpers {
         if (connectivity == null) {
             Log.w(Constants.TAG, "couldn't get connectivity manager");
         } else {
-            NetworkInfo info = connectivity.getActiveNetworkInfo();
+            NetworkInfo[] info = connectivity.getAllNetworkInfo();
             if (info != null) {
-                if (info.getState() == NetworkInfo.State.CONNECTED) {
-                    if (Constants.LOGVV) {
-                        Log.v(TAG, "network is available");
+                for (int i = 0; i < info.length; i++) {
+                    if (info[i].getState() == NetworkInfo.State.CONNECTED) {
+                        if (Constants.LOGVV) {
+                            Log.v(Constants.TAG, "network is available");
+                        }
+                        return true;
                     }
-                    return true;
                 }
             }
         }
         if (Constants.LOGVV) {
-            Log.v(TAG, "network is not available");
+            Log.v(Constants.TAG, "network is not available");
+        }
+        return false;
+    }
+
+    /**
+     * Returns whether the network is roaming
+     */
+    public static boolean isNetworkRoaming(Context context) {
+        ConnectivityManager connectivity =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            Log.w(Constants.TAG, "couldn't get connectivity manager");
+        } else {
+            NetworkInfo info = connectivity.getActiveNetworkInfo();
+            if (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE) {
+                if (TelephonyManager.getDefault().isNetworkRoaming()) {
+                    if (Constants.LOGVV) {
+                        Log.v(Constants.TAG, "network is roaming");
+                    }
+                    return true;
+                } else {
+                    if (Constants.LOGVV) {
+                        Log.v(Constants.TAG, "network is not roaming");
+                    }
+                }
+            } else {
+                if (Constants.LOGVV) {
+                    Log.v(Constants.TAG, "not using mobile network");
+                }
+            }
         }
         return false;
     }
 
+    /**
+     * Checks whether the filename looks legitimate
+     */
+    public static boolean isFilenameValid(String filename) {
+        File dir = new File(filename).getParentFile();
+        return dir.equals(Environment.getDownloadCacheDirectory())
+                || dir.equals(new File(Environment.getExternalStorageDirectory()
+                        + Constants.DEFAULT_DL_SUBDIR));
+    }
+
+    /**
+     * Checks whether this looks like a legitimate selection parameter
+     */
+    public static void validateSelection(String selection, Set<String> allowedColumns) {
+        try {
+            if (selection == null) {
+                return;
+            }
+            Lexer lexer = new Lexer(selection, allowedColumns);
+            parseExpression(lexer);
+            if (lexer.currentToken() != Lexer.TOKEN_END) {
+                throw new IllegalArgumentException("syntax error");
+            }
+        } catch (RuntimeException ex) {
+            if (Constants.LOGV) {
+                Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
+            } else if (Config.LOGD) {
+                Log.d(Constants.TAG, "invalid selection triggered " + ex);
+            }
+            throw ex;
+        }
+
+    }
+
+    // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] *
+    //             | statement [AND_OR expression]*
+    private static void parseExpression(Lexer lexer) {
+        for (;;) {
+            // ( expression )
+            if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) {
+                lexer.advance();
+                parseExpression(lexer);
+                if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) {
+                    throw new IllegalArgumentException("syntax error, unmatched parenthese");
+                }
+                lexer.advance();
+            } else {
+                // statement
+                parseStatement(lexer);
+            }
+            if (lexer.currentToken() != Lexer.TOKEN_AND_OR) {
+                break;
+            }
+            lexer.advance();
+        }
+    }
+
+    // statement <- COLUMN COMPARE VALUE
+    //            | COLUMN IS NULL
+    private static void parseStatement(Lexer lexer) {
+        // both possibilities start with COLUMN
+        if (lexer.currentToken() != Lexer.TOKEN_COLUMN) {
+            throw new IllegalArgumentException("syntax error, expected column name");
+        }
+        lexer.advance();
+
+        // statement <- COLUMN COMPARE VALUE
+        if (lexer.currentToken() == Lexer.TOKEN_COMPARE) {
+            lexer.advance();
+            if (lexer.currentToken() != Lexer.TOKEN_VALUE) {
+                throw new IllegalArgumentException("syntax error, expected quoted string");
+            }
+            lexer.advance();
+            return;
+        }
+
+        // statement <- COLUMN IS NULL
+        if (lexer.currentToken() == Lexer.TOKEN_IS) {
+            lexer.advance();
+            if (lexer.currentToken() != Lexer.TOKEN_NULL) {
+                throw new IllegalArgumentException("syntax error, expected NULL");
+            }
+            lexer.advance();
+            return;
+        }
+
+        // didn't get anything good after COLUMN
+        throw new IllegalArgumentException("syntax error after column name");
+    }
+
+    /**
+     * A simple lexer that recognizes the words of our restricted subset of SQL where clauses
+     */
+    private static class Lexer {
+        public static final int TOKEN_START = 0;
+        public static final int TOKEN_OPEN_PAREN = 1;
+        public static final int TOKEN_CLOSE_PAREN = 2;
+        public static final int TOKEN_AND_OR = 3;
+        public static final int TOKEN_COLUMN = 4;
+        public static final int TOKEN_COMPARE = 5;
+        public static final int TOKEN_VALUE = 6;
+        public static final int TOKEN_IS = 7;
+        public static final int TOKEN_NULL = 8;
+        public static final int TOKEN_END = 9;
+
+        private final String mSelection;
+        private final Set<String> mAllowedColumns;
+        private int mOffset = 0;
+        private int mCurrentToken = TOKEN_START;
+        private final char[] mChars;
+
+        public Lexer(String selection, Set<String> allowedColumns) {
+            mSelection = selection;
+            mAllowedColumns = allowedColumns;
+            mChars = new char[mSelection.length()];
+            mSelection.getChars(0, mChars.length, mChars, 0);
+            advance();
+        }
+
+        public int currentToken() {
+            return mCurrentToken;
+        }
+
+        public void advance() {
+            char[] chars = mChars;
+
+            // consume whitespace
+            while (mOffset < chars.length && chars[mOffset] == ' ') {
+                ++mOffset;
+            }
+
+            // end of input
+            if (mOffset == chars.length) {
+                mCurrentToken = TOKEN_END;
+                return;
+            }
+
+            // "("
+            if (chars[mOffset] == '(') {
+                ++mOffset;
+                mCurrentToken = TOKEN_OPEN_PAREN;
+                return;
+            }
+
+            // ")"
+            if (chars[mOffset] == ')') {
+                ++mOffset;
+                mCurrentToken = TOKEN_CLOSE_PAREN;
+                return;
+            }
+
+            // "?"
+            if (chars[mOffset] == '?') {
+                ++mOffset;
+                mCurrentToken = TOKEN_VALUE;
+                return;
+            }
+
+            // "=" and "=="
+            if (chars[mOffset] == '=') {
+                ++mOffset;
+                mCurrentToken = TOKEN_COMPARE;
+                if (mOffset < chars.length && chars[mOffset] == '=') {
+                    ++mOffset;
+                }
+                return;
+            }
+
+            // ">" and ">="
+            if (chars[mOffset] == '>') {
+                ++mOffset;
+                mCurrentToken = TOKEN_COMPARE;
+                if (mOffset < chars.length && chars[mOffset] == '=') {
+                    ++mOffset;
+                }
+                return;
+            }
+
+            // "<", "<=" and "<>"
+            if (chars[mOffset] == '<') {
+                ++mOffset;
+                mCurrentToken = TOKEN_COMPARE;
+                if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) {
+                    ++mOffset;
+                }
+                return;
+            }
+
+            // "!="
+            if (chars[mOffset] == '!') {
+                ++mOffset;
+                mCurrentToken = TOKEN_COMPARE;
+                if (mOffset < chars.length && chars[mOffset] == '=') {
+                    ++mOffset;
+                    return;
+                }
+                throw new IllegalArgumentException("Unexpected character after !");
+            }
+
+            // columns and keywords
+            // first look for anything that looks like an identifier or a keyword
+            //     and then recognize the individual words.
+            // no attempt is made at discarding sequences of underscores with no alphanumeric
+            //     characters, even though it's not clear that they'd be legal column names.
+            if (isIdentifierStart(chars[mOffset])) {
+                int startOffset = mOffset;
+                ++mOffset;
+                while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) {
+                    ++mOffset;
+                }
+                String word = mSelection.substring(startOffset, mOffset);
+                if (mOffset - startOffset <= 4) {
+                    if (word.equals("IS")) {
+                        mCurrentToken = TOKEN_IS;
+                        return;
+                    }
+                    if (word.equals("OR") || word.equals("AND")) {
+                        mCurrentToken = TOKEN_AND_OR;
+                        return;
+                    }
+                    if (word.equals("NULL")) {
+                        mCurrentToken = TOKEN_NULL;
+                        return;
+                    }
+                }
+                if (mAllowedColumns.contains(word)) {
+                    mCurrentToken = TOKEN_COLUMN;
+                    return;
+                }
+                throw new IllegalArgumentException("unrecognized column or keyword");
+            }
+
+            // quoted strings
+            if (chars[mOffset] == '\'') {
+                ++mOffset;
+                while(mOffset < chars.length) {
+                    if (chars[mOffset] == '\'') {
+                        if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') {
+                            ++mOffset;
+                        } else {
+                            break;
+                        }
+                    }
+                    ++mOffset;
+                }
+                if (mOffset == chars.length) {
+                    throw new IllegalArgumentException("unterminated string");
+                }
+                ++mOffset;
+                mCurrentToken = TOKEN_VALUE;
+                return;
+            }
+
+            // anything we don't recognize
+            throw new IllegalArgumentException("illegal character");
+        }
+
+        private static final boolean isIdentifierStart(char c) {
+            return c == '_' ||
+                    (c >= 'A' && c <= 'Z') ||
+                    (c >= 'a' && c <= 'z');
+        }
+
+        private static final boolean isIdentifierChar(char c) {
+            return c == '_' ||
+                    (c >= 'A' && c <= 'Z') ||
+                    (c >= 'a' && c <= 'z') ||
+                    (c >= '0' && c <= '9');
+        }
+    }
 }