summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk11
-rw-r--r--AndroidManifest.xml58
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--NOTICE190
-rw-r--r--res/layout/status_bar_ongoing_event_progress_bar.xml109
-rw-r--r--res/values-de-rDE/strings.xml40
-rw-r--r--res/values-en-rGB/strings.xml40
-rw-r--r--res/values-es-rUS/strings.xml40
-rw-r--r--res/values-fr-rFR/strings.xml40
-rw-r--r--res/values-it-rIT/strings.xml40
-rw-r--r--res/values-zh-rTW/strings.xml40
-rw-r--r--res/values/strings.xml74
-rw-r--r--src/com/android/providers/downloads/Constants.java122
-rw-r--r--src/com/android/providers/downloads/DownloadFileInfo.java34
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java185
-rw-r--r--src/com/android/providers/downloads/DownloadNotification.java300
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java532
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java167
-rw-r--r--src/com/android/providers/downloads/DownloadService.java859
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java643
-rw-r--r--src/com/android/providers/downloads/Helpers.java510
21 files changed, 4034 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 00000000..82f0d585
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,11 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := user development
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := DownloadProvider
+LOCAL_CERTIFICATE := media
+
+include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 00000000..d9873e61
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.downloads"
+ android:sharedUserId="android.media">
+
+ <!-- Allows access to the Download Manager -->
+ <permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER"
+ android:label="@string/permlab_downloadManager"
+ 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 filesystem access to /cache -->
+ <permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM"
+ android:label="@string/permlab_cacheFilesystem"
+ 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"
+ android:description="@string/permdesc_downloadCompletedIntent"
+ android:protectionLevel="signature" />
+
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
+ <uses-permission android:name="android.permission.ACCESS_DRM" />
+ <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
+ <uses-permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+ <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_DATA" />
+ <application android:process="android.process.media"
+ android:label="Download Manager">
+ <provider android:name=".DownloadProvider"
+ android:authorities="downloads"
+ android:permission="android.permission.ACCESS_DOWNLOAD_MANAGER" />
+ <service android:name=".DownloadService"
+ android:permission="android.permission.ACCESS_DOWNLOAD_MANAGER" />
+ <receiver android:name=".DownloadReceiver" android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 00000000..c5b1efa7
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/res/layout/status_bar_ongoing_event_progress_bar.xml b/res/layout/status_bar_ongoing_event_progress_bar.xml
new file mode 100644
index 00000000..c0cdcbf1
--- /dev/null
+++ b/res/layout/status_bar_ongoing_event_progress_bar.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:background="@android:drawable/status_bar_item_app_background"
+ >
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal"
+ >
+
+ <LinearLayout android:id="@+id/app"
+ android:layout_width="40dp"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:paddingTop="8dp"
+ android:focusable="true"
+ android:clickable="true"
+ >
+ <com.android.server.status.AnimatedImageView
+ android:id="@+id/appIcon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@android:drawable/sym_def_app_icon"
+ />
+ <TextView android:id="@+id/progress_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="#ff000000"
+ android:singleLine="true"
+ android:textSize="14sp"
+ android:layout_gravity="center_horizontal"
+ />
+ </LinearLayout>
+
+ <RelativeLayout android:id="@+id/app"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:focusable="true"
+ android:clickable="true"
+ >
+ <LinearLayout android:id="@+id/notification"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:focusable="true"
+ android:clickable="true"
+ android:layout_alignParentTop="true"
+ android:paddingTop="10dp"
+ >
+ <TextView android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textSize="18sp"
+ android:textColor="#ff000000"
+ android:paddingLeft="2dp"
+ />
+ <TextView android:id="@+id/description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="#ff000000"
+ android:singleLine="true"
+ android:textSize="14sp"
+ android:paddingLeft="5dp"
+ />
+ </LinearLayout>
+ <ProgressBar android:id="@+id/progress_bar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:paddingBottom="8dp"
+ android:paddingRight="25dp"
+ />
+ </RelativeLayout>
+ </LinearLayout>
+
+ <com.android.server.status.AnimatedImageView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:src="@android:drawable/divider_horizontal_bright"
+ />
+
+</LinearLayout>
+
diff --git a/res/values-de-rDE/strings.xml b/res/values-de-rDE/strings.xml
new file mode 100644
index 00000000..e4954d80
--- /dev/null
+++ b/res/values-de-rDE/strings.xml
@@ -0,0 +1,40 @@
+<?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-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
new file mode 100644
index 00000000..ffdaf047
--- /dev/null
+++ b/res/values-en-rGB/strings.xml
@@ -0,0 +1,40 @@
+<?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
new file mode 100644
index 00000000..7453e8a5
--- /dev/null
+++ b/res/values-es-rUS/strings.xml
@@ -0,0 +1,40 @@
+<?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
new file mode 100644
index 00000000..30c4e62d
--- /dev/null
+++ b/res/values-fr-rFR/strings.xml
@@ -0,0 +1,40 @@
+<?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
new file mode 100644
index 00000000..c19c0bb3
--- /dev/null
+++ b/res/values-it-rIT/strings.xml
@@ -0,0 +1,40 @@
+<?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-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
new file mode 100644
index 00000000..c34475bb
--- /dev/null
+++ b/res/values-zh-rTW/strings.xml
@@ -0,0 +1,40 @@
+<?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>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 00000000..657c92eb
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<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>
+
+ <string name="permlab_downloadManager">Access download manager.</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="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>
+
+ <string name="permlab_cacheFilesystem">Use system cache.</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="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>
+ <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 -->
+ <string name="notification_filename_separator">", "</string>
+
+ <!-- used to list that there are more than 2 files in a notification -->
+ <string name="notification_filename_extras">" and %d more"</string>
+
+ <!-- information line shown in the notifications for completed downloads -->
+ <string name="notification_download_complete">Download complete</string>
+
+ <!-- information line shown in the notifications for failed downloads -->
+ <string name="notification_download_failed">Download unsuccessful</string>
+
+
+</resources>
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
new file mode 100644
index 00000000..f3dd08c7
--- /dev/null
+++ b/src/com/android/providers/downloads/Constants.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.util.Config;
+import android.util.Log;
+
+/**
+ * Contains the internal constants that are used in the download manager.
+ * As a general rule, modifying these constants should be done with care.
+ */
+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 permission that allows to download a system image */
+ public static final String OTA_UPDATE_PERMISSION = "android.permission.DOWNLOAD_OTA_UPDATE";
+
+ /** 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";
+
+ /** the intent that gets sent when clicking a successful download */
+ public static final String ACTION_OPEN = "android.intent.action.DOWNLOAD_OPEN";
+
+ /** the intent that gets sent when clicking an incomplete/failed download */
+ public static final String ACTION_LIST = "android.intent.action.DOWNLOAD_LIST";
+
+ /** the intent that gets sent when deleting the notification of a completed download */
+ public static final String ACTION_HIDE = "android.intent.action.DOWNLOAD_HIDE";
+
+ /** The default base name for downloaded files if we can't get one at the HTTP level */
+ public static final String DEFAULT_DL_FILENAME = "downloadfile";
+
+ /** The default extension for html files if we can't get one at the HTTP level */
+ public static final String DEFAULT_DL_HTML_EXTENSION = ".html";
+
+ /** The default extension for text files if we can't get one at the HTTP level */
+ public static final String DEFAULT_DL_TEXT_EXTENSION = ".txt";
+
+ /** The default extension for binary files if we can't get one at the HTTP level */
+ public static final String DEFAULT_DL_BINARY_EXTENSION = ".bin";
+
+ /**
+ * When a number has to be appended to the filename, this string is used to separate the
+ * base filename from the sequence number
+ */
+ public static final String FILENAME_SEQUENCE_SEPARATOR = "-";
+
+ /** Where we store downloaded files on the external storage */
+ public static final String DEFAULT_DL_SUBDIR = "/download";
+
+ /** A magic filename that is allowed to exist within the system cache */
+ public static final String KNOWN_SPURIOUS_FILENAME = "lost+found";
+
+ /** 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";
+
+ /** The MIME type of special DRM files */
+ public static final String MIMETYPE_DRM_MESSAGE =
+ android.drm.mobile1.DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING;
+
+ /** The MIME type of APKs */
+ public static final String MIMETYPE_APK = "application/vnd.android.package";
+
+ /** The buffer size used to stream the data */
+ public static final int BUFFER_SIZE = 4096;
+
+ /** The minimum amount of progress that has to be done before the progress bar gets updated */
+ public static final int MIN_PROGRESS_STEP = 4096;
+
+ /** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */
+ public static final long MIN_PROGRESS_TIME = 1500;
+
+ /** The maximum number of rows in the database (FIFO) */
+ public static final int MAX_DOWNLOADS = 1000;
+
+ /**
+ * The number of times that the download manager will retry its network
+ * operations when no progress is happening before it gives up.
+ */
+ public static final int MAX_RETRIES = 5;
+
+ /**
+ * 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.
+ */
+ 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;
+ public static final boolean LOGV = Config.LOGV
+ || (Config.LOGD && LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE));
+
+ /** Enable super-verbose logging */
+ private static final boolean LOCAL_LOGVV = false;
+ public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
+}
diff --git a/src/com/android/providers/downloads/DownloadFileInfo.java b/src/com/android/providers/downloads/DownloadFileInfo.java
new file mode 100644
index 00000000..29cbd940
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadFileInfo.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import java.io.FileOutputStream;
+
+/**
+ * Stores information about the file in which a download gets saved.
+ */
+public class DownloadFileInfo {
+ public DownloadFileInfo(String filename, FileOutputStream stream, int status) {
+ this.filename = filename;
+ this.stream = stream;
+ this.status = status;
+ }
+
+ String filename;
+ FileOutputStream stream;
+ int status;
+}
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
new file mode 100644
index 00000000..b8cead65
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.net.Uri;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Downloads;
+
+/**
+ * Stores information about an individual download.
+ */
+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 long lastMod;
+ public String pckg;
+ public String clazz;
+ public String extras;
+ public String cookies;
+ public String userAgent;
+ public String referer;
+ public int totalBytes;
+ public int currentBytes;
+ public String etag;
+ public boolean mediaScanned;
+
+ 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,
+ 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.lastMod = lastMod;
+ this.pckg = pckg;
+ this.clazz = clazz;
+ this.extras = extras;
+ this.cookies = cookies;
+ this.userAgent = userAgent;
+ this.referer = referer;
+ this.totalBytes = totalBytes;
+ this.currentBytes = currentBytes;
+ this.etag = etag;
+ this.mediaScanned = mediaScanned;
+ }
+
+ public void sendIntentIfRequested(Uri contentUri, Context context) {
+ if (pckg != null && clazz != null) {
+ Intent intent = new Intent(Downloads.DOWNLOAD_COMPLETED_ACTION);
+ intent.setClassName(pckg, clazz);
+ if (extras != null) {
+ intent.putExtra(Downloads.NOTIFICATION_EXTRAS, extras);
+ }
+ // We only send the content: URI, for security reasons. Otherwise, malicious
+ // applications would have an easier time spoofing download results by
+ // sending spoofed intents.
+ intent.setData(contentUri);
+ context.sendBroadcast(intent);
+ }
+ }
+
+ /**
+ * Returns the time when a download should be restarted. Must only
+ * be called when numFailed > 0.
+ */
+ public long restartTime() {
+ return lastMod + Constants.RETRY_FIRST_DELAY * 1000 * (1 << (numFailed - 1));
+ }
+
+ /**
+ * Returns whether this download should be started at the time when
+ * it's first inserted in the database.
+ */
+ public boolean isReadyToStart(long now) {
+ if (status == 0) {
+ // status hasn't been initialized yet, this is a new download
+ return true;
+ }
+ if (status == Downloads.STATUS_PENDING) {
+ // download is explicit marked as ready to start
+ return true;
+ }
+ if (status == Downloads.STATUS_RUNNING) {
+ // download was interrupted (process killed, loss of power) while it was running,
+ // without a chance to update the database
+ return true;
+ }
+ if (status == Downloads.STATUS_RUNNING_PAUSED) {
+ if (numFailed == 0) {
+ // download is waiting for network connectivity to return before it can resume
+ return true;
+ }
+ if (restartTime() < now) {
+ // download was waiting for a delayed restart, and the delay has expired
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether this download should be restarted at the time when
+ * it was already known by the download manager
+ */
+ public boolean isReadyToRestart(long now) {
+ if (status == 0) {
+ // download hadn't been initialized yet
+ return true;
+ }
+ if (status == Downloads.STATUS_PENDING) {
+ // download is explicit marked as ready to start
+ return true;
+ }
+ if (status == Downloads.STATUS_RUNNING_PAUSED) {
+ if (numFailed == 0) {
+ // download is waiting for network connectivity to return before it can resume
+ return true;
+ }
+ if (restartTime() < now) {
+ // download was waiting for a delayed restart, and the delay has expired
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether this download has a visible notification after
+ * completion.
+ */
+ public boolean hasCompletionNotification() {
+ if (!Downloads.isStatusCompleted(status)) {
+ return false;
+ }
+ if (visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/providers/downloads/DownloadNotification.java b/src/com/android/providers/downloads/DownloadNotification.java
new file mode 100644
index 00000000..38cd84f2
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadNotification.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Downloads;
+import android.widget.RemoteViews;
+
+import java.util.HashMap;
+
+/**
+ * This class handles the updating of the Notification Manager for the
+ * cases where there is an ongoing download. Once the download is complete
+ * (be it successful or unsuccessful) it is no longer the responsibility
+ * of this component to show the download in the notification manager.
+ *
+ */
+class DownloadNotification {
+
+ Context mContext;
+ public NotificationManager mNotificationMgr;
+ HashMap <String, NotificationItem> mNotifications;
+
+ 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;
+
+
+ /**
+ * This inner class is used to collate downloads that are owned by
+ * the same application. This is so that only one notification line
+ * item is used for all downloads of a given application.
+ *
+ */
+ static class NotificationItem {
+ int id; // This first db _id for the download for the app
+ int totalCurrent = 0;
+ int totalTotal = 0;
+ int titleCount = 0;
+ String packageName; // App package name
+ String description;
+ String[] titles = new String[2]; // download titles.
+
+ /*
+ * Add a second download to this notification item.
+ */
+ void addItem(String title, int currentBytes, int totalBytes) {
+ totalCurrent += currentBytes;
+ if (totalBytes <= 0 || totalTotal == -1) {
+ totalTotal = -1;
+ } else {
+ totalTotal += totalBytes;
+ }
+ if (titleCount < 2) {
+ titles[titleCount] = title;
+ }
+ titleCount++;
+ }
+ }
+
+
+ /**
+ * Constructor
+ * @param ctx The context to use to obtain access to the
+ * Notification Service
+ */
+ DownloadNotification(Context ctx) {
+ mContext = ctx;
+ mNotificationMgr = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mNotifications = new HashMap<String, NotificationItem>();
+ }
+
+ /*
+ * Update the notification ui.
+ */
+ public void updateNotification() {
+ updateActiveNotification();
+ updateCompletedNotification();
+ }
+
+ private void updateActiveNotification() {
+ // Active downloads
+ Cursor c = mContext.getContentResolver().query(
+ Downloads.CONTENT_URI, new String [] {
+ Downloads._ID, Downloads.TITLE, Downloads.DESCRIPTION,
+ Downloads.NOTIFICATION_PACKAGE,
+ Downloads.NOTIFICATION_CLASS,
+ Downloads.CURRENT_BYTES, Downloads.TOTAL_BYTES,
+ Downloads.STATUS, Downloads.FILENAME
+ },
+ WHERE_RUNNING, null, Downloads._ID);
+
+ if (c == null) {
+ return;
+ }
+
+ // Columns match projection in query above
+ final int idColumn = 0;
+ final int titleColumn = 1;
+ final int descColumn = 2;
+ final int ownerColumn = 3;
+ final int classOwnerColumn = 4;
+ final int currentBytesColumn = 5;
+ final int totalBytesColumn = 6;
+ final int statusColumn = 7;
+ final int filenameColumnId = 8;
+
+ // Collate the notifications
+ mNotifications.clear();
+ for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
+ String packageName = c.getString(ownerColumn);
+ int max = c.getInt(totalBytesColumn);
+ int progress = c.getInt(currentBytesColumn);
+ String title = c.getString(titleColumn);
+ if (title == null || title.length() == 0) {
+ title = mContext.getResources().getString(
+ R.string.download_unknown_title);
+ }
+ if (mNotifications.containsKey(packageName)) {
+ mNotifications.get(packageName).addItem(title, progress, max);
+ } else {
+ NotificationItem item = new NotificationItem();
+ item.id = c.getInt(idColumn);
+ item.packageName = packageName;
+ item.description = c.getString(descColumn);
+ String className = c.getString(classOwnerColumn);
+ item.addItem(title, progress, max);
+ mNotifications.put(packageName, item);
+ }
+
+ }
+ c.close();
+
+ // Add the notifications
+ for (NotificationItem item : mNotifications.values()) {
+ // Build the notification object
+ Notification n = new Notification();
+ n.icon = android.R.drawable.stat_sys_download;
+
+ n.flags |= Notification.FLAG_ONGOING_EVENT;
+
+ // Build the RemoteView object
+ RemoteViews expandedView = new RemoteViews(
+ "com.android.providers.downloads",
+ R.layout.status_bar_ongoing_event_progress_bar);
+ StringBuilder title = new StringBuilder(item.titles[0]);
+ if (item.titleCount > 1) {
+ title.append(mContext.getString(R.string.notification_filename_separator));
+ title.append(item.titles[1]);
+ n.number = item.titleCount;
+ if (item.titleCount > 2) {
+ title.append(mContext.getString(R.string.notification_filename_extras,
+ new Object[] { Integer.valueOf(item.titleCount - 2) }));
+ }
+ } else {
+ expandedView.setTextViewText(R.id.description,
+ item.description);
+ }
+ expandedView.setTextViewText(R.id.title, title);
+ expandedView.setProgressBar(R.id.progress_bar,
+ item.totalTotal,
+ item.totalCurrent,
+ item.totalTotal == -1);
+ expandedView.setTextViewText(R.id.progress_text,
+ getDownloadingText(item.totalTotal, item.totalCurrent));
+ expandedView.setImageViewResource(R.id.appIcon,
+ android.R.drawable.stat_sys_download);
+ n.contentView = expandedView;
+
+ Intent intent = new Intent(Constants.ACTION_LIST);
+ intent.setClassName("com.android.providers.downloads",
+ DownloadReceiver.class.getName());
+ intent.setData(Uri.parse(Downloads.CONTENT_URI + "/" + item.id));
+ intent.putExtra("multiple", item.titleCount > 1);
+
+ n.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+
+ mNotificationMgr.notify(item.id, n);
+
+ }
+ }
+
+ private void updateCompletedNotification() {
+ // Completed downloads
+ Cursor c = mContext.getContentResolver().query(
+ Downloads.CONTENT_URI, new String [] {
+ Downloads._ID, Downloads.TITLE, Downloads.DESCRIPTION,
+ Downloads.NOTIFICATION_PACKAGE,
+ Downloads.NOTIFICATION_CLASS,
+ Downloads.CURRENT_BYTES, Downloads.TOTAL_BYTES,
+ Downloads.STATUS, Downloads.FILENAME,
+ Downloads.LAST_MODIFICATION, Downloads.DESTINATION
+ },
+ WHERE_COMPLETED, null, Downloads._ID);
+
+ if (c == null) {
+ return;
+ }
+
+ // Columns match projection in query above
+ final int idColumn = 0;
+ final int titleColumn = 1;
+ final int descColumn = 2;
+ final int ownerColumn = 3;
+ final int classOwnerColumn = 4;
+ final int currentBytesColumn = 5;
+ final int totalBytesColumn = 6;
+ final int statusColumn = 7;
+ final int filenameColumnId = 8;
+ final int lastModColumnId = 9;
+ final int destinationColumnId = 10;
+
+ for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
+ // Add the notifications
+ Notification n = new Notification();
+ n.icon = android.R.drawable.stat_sys_download_done;
+
+ String title = c.getString(titleColumn);
+ if (title == null || title.length() == 0) {
+ title = mContext.getResources().getString(
+ R.string.download_unknown_title);
+ }
+ Uri contentUri = Uri.parse(Downloads.CONTENT_URI + "/" + c.getInt(idColumn));
+ String caption;
+ Intent intent;
+ if (Downloads.isStatusError(c.getInt(statusColumn))) {
+ caption = mContext.getResources()
+ .getString(R.string.notification_download_failed);
+ intent = new Intent(Constants.ACTION_LIST);
+ } else {
+ caption = mContext.getResources()
+ .getString(R.string.notification_download_complete);
+ if (c.getInt(destinationColumnId) == Downloads.DESTINATION_EXTERNAL) {
+ intent = new Intent(Constants.ACTION_OPEN);
+ } else {
+ intent = new Intent(Constants.ACTION_LIST);
+ }
+ }
+ intent.setClassName("com.android.providers.downloads",
+ DownloadReceiver.class.getName());
+ intent.setData(contentUri);
+ n.setLatestEventInfo(mContext, title, caption,
+ PendingIntent.getBroadcast(mContext, 0, intent, 0));
+
+ intent = new Intent(Constants.ACTION_HIDE);
+ intent.setClassName("com.android.providers.downloads",
+ DownloadReceiver.class.getName());
+ intent.setData(contentUri);
+ n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+
+ n.when = c.getLong(lastModColumnId);
+
+ mNotificationMgr.notify(c.getInt(idColumn), n);
+ }
+ c.close();
+ }
+
+ /*
+ * Helper function to build the downloading text.
+ */
+ private String getDownloadingText(long totalBytes, long currentBytes) {
+ if (totalBytes <= 0) {
+ return "";
+ }
+ long progress = currentBytes * 100 / totalBytes;
+ StringBuilder sb = new StringBuilder();
+ sb.append(progress);
+ sb.append('%');
+ return sb.toString();
+ }
+
+}
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
new file mode 100644
index 00000000..c85c94a9
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.SQLException;
+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.FileNotFoundException;
+
+/**
+ * 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;
+ /** Name of table in the database */
+ private static final String DB_TABLE = "downloads";
+
+ /** MIME type for the entire download list */
+ private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download";
+ /** MIME type for an individual download */
+ private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download";
+
+ /** URI matcher used to recognize URIs sent by applications */
+ private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ /** URI matcher constant for the URI of the entire download list */
+ private static final int DOWNLOADS = 1;
+ /** URI matcher constant for the URI of an individual download */
+ private static final int DOWNLOADS_ID = 2;
+ static {
+ sURIMatcher.addURI("downloads", "download", DOWNLOADS);
+ sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
+ }
+
+ /** The database that lies underneath this content provider */
+ private SQLiteOpenHelper mOpenHelper = null;
+
+ /**
+ * Creates and updated database on demand when opening it.
+ * Helper class to create database the first time the provider is
+ * initialized and upgrade it when a new version of the provider needs
+ * an updated version of the database.
+ */
+ private final class DatabaseHelper extends SQLiteOpenHelper {
+
+ public DatabaseHelper(final Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ }
+
+ /**
+ * Creates database the first time we try to open it.
+ */
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "populating new database");
+ }
+ createTable(db);
+ }
+
+ /* (not a javadoc comment)
+ * Checks data integrity when opening the database.
+ */
+ /*
+ * @Override
+ * public void onOpen(final SQLiteDatabase db) {
+ * super.onOpen(db);
+ * }
+ */
+
+ /**
+ * Updates the database format when a content provider is used
+ * with a database that was created with a different format.
+ */
+ // Note: technically, this could also be a downgrade, so if we want
+ // 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
+ + ", which will destroy all old data");
+ dropTable(db);
+ createTable(db);
+ }
+ }
+
+ /**
+ * Initializes the content provider when it is created.
+ */
+ @Override
+ public boolean onCreate() {
+ mOpenHelper = new DatabaseHelper(getContext());
+ return true;
+ }
+
+ /**
+ * Returns the content-provider-style MIME types of the various
+ * types accessible through this content provider.
+ */
+ @Override
+ public String getType(final Uri uri) {
+ int match = sURIMatcher.match(uri);
+ switch (match) {
+ case DOWNLOADS: {
+ return DOWNLOAD_LIST_TYPE;
+ }
+ case DOWNLOADS_ID: {
+ return DOWNLOAD_TYPE;
+ }
+ default: {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
+ }
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+ }
+
+ /**
+ * Creates the table that'll hold the download information.
+ */
+ private void createTable(SQLiteDatabase db) {
+ try {
+ db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Downloads.URI + " TEXT, " +
+ Downloads.METHOD + " INTEGER, " +
+ Downloads.ENTITY + " TEXT, " +
+ Downloads.NO_INTEGRITY + " BOOLEAN, " +
+ Downloads.FILENAME_HINT + " TEXT, " +
+ Downloads.OTA_UPDATE + " BOOLEAN, " +
+ Downloads.FILENAME + " TEXT, " +
+ Downloads.MIMETYPE + " TEXT, " +
+ Downloads.DESTINATION + " INTEGER, " +
+ Downloads.NO_SYSTEM_FILES + " BOOLEAN, " +
+ Downloads.VISIBILITY + " INTEGER, " +
+ Downloads.CONTROL + " INTEGER, " +
+ Downloads.STATUS + " INTEGER, " +
+ Downloads.FAILED_CONNECTIONS + " INTEGER, " +
+ Downloads.LAST_MODIFICATION + " BIGINT, " +
+ Downloads.NOTIFICATION_PACKAGE + " TEXT, " +
+ Downloads.NOTIFICATION_CLASS + " TEXT, " +
+ Downloads.NOTIFICATION_EXTRAS + " TEXT, " +
+ Downloads.COOKIE_DATA + " TEXT, " +
+ Downloads.USER_AGENT + " TEXT, " +
+ Downloads.REFERER + " TEXT, " +
+ Downloads.TOTAL_BYTES + " INTEGER, " +
+ Downloads.CURRENT_BYTES + " INTEGER, " +
+ Downloads.ETAG + " TEXT, " +
+ Downloads.UID + " INTEGER, " +
+ Downloads.OTHER_UID + " INTEGER, " +
+ Downloads.TITLE + " TEXT, " +
+ Downloads.DESCRIPTION + " TEXT, " +
+ Downloads.MEDIA_SCANNED + " BOOLEAN);");
+ } catch (SQLException ex) {
+ Log.e(Constants.TAG, "couldn't create table in downloads database");
+ throw ex;
+ }
+ }
+
+ /**
+ * Deletes the table that holds the download information.
+ */
+ private void dropTable(SQLiteDatabase db) {
+ try {
+ db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
+ } catch (SQLException ex) {
+ Log.e(Constants.TAG, "couldn't drop table in downloads database");
+ throw ex;
+ }
+ }
+
+ /**
+ * Inserts a row in the database
+ */
+ @Override
+ public Uri insert(final Uri uri, final ContentValues values) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ if (sURIMatcher.match(uri) != DOWNLOADS) {
+ if (Config.LOGD) {
+ Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
+ }
+ 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());
+ }
+ 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));
+ }
+ }
+
+ if (values.containsKey(Downloads.LAST_MODIFICATION)) {
+ values.remove(Downloads.LAST_MODIFICATION);
+ }
+ values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
+
+ if (values.containsKey(Downloads.STATUS)) {
+ values.remove(Downloads.STATUS);
+ }
+ values.put(Downloads.STATUS, Downloads.STATUS_PENDING);
+
+ if (values.containsKey(Downloads.OTA_UPDATE)
+ && getContext().checkCallingPermission(Constants.OTA_UPDATE_PERMISSION)
+ != PackageManager.PERMISSION_GRANTED) {
+ values.remove(Downloads.OTA_UPDATE);
+ }
+
+ Context context = getContext();
+ context.startService(new Intent(context, DownloadService.class));
+
+ long rowID = db.insert(DB_TABLE, null, values);
+
+ Uri ret = null;
+
+ if (rowID != -1) {
+ context.startService(new Intent(context, DownloadService.class));
+ ret = Uri.parse(Downloads.CONTENT_URI + "/" + rowID);
+ context.getContentResolver().notifyChange(uri, null);
+ } else {
+ if (Config.LOGD) {
+ Log.d(TAG, "couldn't insert into downloads database");
+ }
+ }
+
+ return ret;
+ }
+
+ /**
+ * Starts a database query
+ */
+ @Override
+ public Cursor query(final Uri uri, final String[] projection,
+ final String selection, final String[] selectionArgs,
+ final String sort) {
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+ int match = sURIMatcher.match(uri);
+ boolean emptyWhere = true;
+ switch (match) {
+ case DOWNLOADS: {
+ qb.setTables(DB_TABLE);
+ break;
+ }
+ case DOWNLOADS_ID: {
+ qb.setTables(DB_TABLE);
+ qb.appendWhere(BaseColumns._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ emptyWhere = false;
+ break;
+ }
+ default: {
+ if (Constants.LOGV) {
+ Log.v(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 (!emptyWhere) {
+ qb.appendWhere(" AND ");
+ }
+ qb.appendWhere("( " + Downloads.UID + "=" + Binder.getCallingUid() + " OR "
+ + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )");
+ emptyWhere = false;
+ }
+
+ if (Constants.LOGVV) {
+ java.lang.StringBuilder sb = new java.lang.StringBuilder();
+ sb.append("starting query, database is ");
+ if (db != null) {
+ sb.append("not ");
+ }
+ sb.append("null; ");
+ if (projection == null) {
+ sb.append("projection is null; ");
+ } else if (projection.length == 0) {
+ sb.append("projection is empty; ");
+ } else {
+ for (int i = 0; i < projection.length; ++i) {
+ sb.append("projection[");
+ sb.append(i);
+ sb.append("] is ");
+ sb.append(projection[i]);
+ sb.append("; ");
+ }
+ }
+ sb.append("selection is ");
+ sb.append(selection);
+ sb.append("; ");
+ if (selectionArgs == null) {
+ sb.append("selectionArgs is null; ");
+ } else if (selectionArgs.length == 0) {
+ sb.append("selectionArgs is empty; ");
+ } else {
+ for (int i = 0; i < selectionArgs.length; ++i) {
+ sb.append("selectionArgs[");
+ sb.append(i);
+ sb.append("] is ");
+ sb.append(selectionArgs[i]);
+ sb.append("; ");
+ }
+ }
+ sb.append("sort is ");
+ sb.append(sort);
+ sb.append(".");
+ Log.v(TAG, sb.toString());
+ }
+
+ Cursor ret = qb.query(db, projection, selection, selectionArgs,
+ null, null, sort);
+
+ if (ret != null) {
+ ret.setNotificationUri(getContext().getContentResolver(), uri);
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG,
+ "created cursor " + ret + " on behalf of " + Binder.getCallingPid());
+ }
+ } else {
+ if (Constants.LOGV) {
+ Log.v(TAG, "query failed in downloads database");
+ }
+ }
+
+ return ret;
+ }
+
+ /**
+ * Updates a row in the database
+ */
+ @Override
+ public int update(final Uri uri, final ContentValues values,
+ final String where, final String[] whereArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ int count;
+ long rowId = 0;
+ if (values.containsKey(Downloads.UID)) {
+ values.remove(Downloads.UID);
+ }
+ int match = sURIMatcher.match(uri);
+ switch (match) {
+ case DOWNLOADS:
+ case DOWNLOADS_ID: {
+ String myWhere;
+ if (where != null) {
+ if (match == DOWNLOADS) {
+ myWhere = where;
+ } else {
+ myWhere = where + " AND ";
+ }
+ } else {
+ myWhere = "";
+ }
+ if (match == DOWNLOADS_ID) {
+ String segment = uri.getPathSegments().get(1);
+ rowId = Long.parseLong(segment);
+ 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 "
+ + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )";
+ }
+ count = db.update(DB_TABLE, values, myWhere, whereArgs);
+ break;
+ }
+ default: {
+ if (Config.LOGD) {
+ Log.d(TAG, "updating unknown/invalid URI: " + uri);
+ }
+ throw new UnsupportedOperationException("Cannot update URI: " + uri);
+ }
+ }
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ /**
+ * Deletes a row in the database
+ */
+ @Override
+ public int delete(final Uri uri, final String where,
+ final String[] whereArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count;
+ int match = sURIMatcher.match(uri);
+ switch (match) {
+ case DOWNLOADS:
+ case DOWNLOADS_ID: {
+ String myWhere;
+ if (where != null) {
+ if (match == DOWNLOADS) {
+ myWhere = where;
+ } else {
+ myWhere = where + " AND ";
+ }
+ } else {
+ myWhere = "";
+ }
+ if (match == DOWNLOADS_ID) {
+ String segment = uri.getPathSegments().get(1);
+ long rowId = Long.parseLong(segment);
+ 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 "
+ + Downloads.OTHER_UID + "=" + Binder.getCallingUid() + " )";
+ }
+ count = db.delete(DB_TABLE, myWhere, whereArgs);
+ break;
+ }
+ default: {
+ if (Config.LOGD) {
+ Log.d(TAG, "deleting unknown/invalid URI: " + uri);
+ }
+ throw new UnsupportedOperationException("Cannot delete URI: " + uri);
+ }
+ }
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ /**
+ * Remotely opens a file
+ */
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ if (Constants.LOGVV) {
+ Log.v(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");
+ } else {
+ if (!cursor.moveToFirst()) {
+ Log.v(TAG, "empty cursor in openFile");
+ } else {
+ do {
+ Log.v(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");
+ } else {
+ if (!cursor.moveToFirst()) {
+ Log.v(TAG, "empty cursor in openFile");
+ } else {
+ String filename = cursor.getString(0);
+ Log.v(TAG, "filename in openFile: " + filename);
+ if (new java.io.File(filename).isFile()) {
+ Log.v(TAG, "file exists in openFile");
+ }
+ }
+ cursor.close();
+ }
+ }
+ ParcelFileDescriptor ret = openFileHelper(uri, mode);
+ if (ret == null) {
+ if (Config.LOGD) {
+ Log.d(TAG, "couldn't open file");
+ }
+ } else {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.LAST_MODIFICATION, System.currentTimeMillis());
+ update(uri, values, null, null);
+ }
+ return ret;
+ }
+
+}
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
new file mode 100644
index 00000000..e5bc4e1f
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.app.NotificationManager;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.provider.Downloads;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Receives system broadcasts (boot, network connectivity)
+ */
+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");
+ }
+ context.startService(new Intent(context, DownloadService.class));
+ } else if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Receiver onConnectivity");
+ }
+ NetworkInfo info = (NetworkInfo)
+ intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
+ if (info != null && info.isConnected()) {
+ context.startService(new Intent(context, DownloadService.class));
+ }
+ } else if (intent.getAction().equals(Constants.ACTION_RETRY)) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Receiver retry");
+ }
+ context.startService(new Intent(context, DownloadService.class));
+ } else if (intent.getAction().equals(Constants.ACTION_OPEN)
+ || intent.getAction().equals(Constants.ACTION_LIST)) {
+ if (Constants.LOGVV) {
+ if (intent.getAction().equals(Constants.ACTION_OPEN)) {
+ Log.v(Constants.TAG, "Receiver open for " + intent.getData());
+ } else {
+ Log.v(Constants.TAG, "Receiver list for " + intent.getData());
+ }
+ }
+ 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);
+ int visibilityColumn = cursor.getColumnIndexOrThrow(Downloads.VISIBILITY);
+ int visibility = cursor.getInt(visibilityColumn);
+ if (Downloads.isStatusCompleted(status)
+ && visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
+ cursor.updateInt(visibilityColumn, Downloads.VISIBILITY_VISIBLE);
+ mustCommit = true;
+ }
+
+ if (intent.getAction().equals(Constants.ACTION_OPEN)) {
+ int filenameColumn = cursor.getColumnIndexOrThrow(Downloads.FILENAME);
+ int mimetypeColumn = cursor.getColumnIndexOrThrow(Downloads.MIMETYPE);
+ String filename = cursor.getString(filenameColumn);
+ String mimetype = cursor.getString(mimetypeColumn);
+ Uri path = Uri.parse(filename);
+ // If there is no scheme, then it must be a file
+ if (path.getScheme() == null) {
+ path = Uri.fromFile(new File(filename));
+ }
+ Intent activityIntent = new Intent(Intent.ACTION_VIEW);
+ activityIntent.setDataAndType(path, mimetype);
+ activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ context.startActivity(activityIntent);
+ } catch (ActivityNotFoundException ex) {
+ if (Config.LOGD) {
+ Log.d(Constants.TAG, "no activity for " + mimetype, ex);
+ }
+ // nothing anyone can do about this, but we're in a clean state,
+ // swallow the exception entirely
+ }
+ } else {
+ int packageColumn =
+ cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_PACKAGE);
+ int classColumn =
+ cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_CLASS);
+ String pckg = cursor.getString(packageColumn);
+ String clazz = cursor.getString(classColumn);
+ if (pckg != null && clazz != null) {
+ Intent appIntent = new Intent(Downloads.NOTIFICATION_CLICKED_ACTION);
+ appIntent.setClassName(pckg, clazz);
+ if (intent.getBooleanExtra("multiple", true)) {
+ appIntent.setData(Downloads.CONTENT_URI);
+ } else {
+ appIntent.setData(intent.getData());
+ }
+ context.sendBroadcast(appIntent);
+ }
+ }
+ }
+ if (mustCommit) {
+ if (!cursor.commitUpdates()) {
+ Log.e(Constants.TAG, "commitUpdate failed in onReceive/OPEN-LIST");
+ }
+ }
+ cursor.close();
+ }
+ NotificationManager notMgr = (NotificationManager) context
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notMgr != null) {
+ notMgr.cancel((int) ContentUris.parseId(intent.getData()));
+ }
+ } else if (intent.getAction().equals(Constants.ACTION_HIDE)) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Receiver hide for " + intent.getData());
+ }
+ Cursor cursor = context.getContentResolver().query(
+ intent.getData(), null, null, null, null);
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
+ int status = cursor.getInt(statusColumn);
+ int visibilityColumn = cursor.getColumnIndexOrThrow(Downloads.VISIBILITY);
+ 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");
+ }
+ }
+ }
+ cursor.close();
+ }
+ }
+ }
+}
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
new file mode 100644
index 00000000..0d3650c0
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -0,0 +1,859 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import com.google.android.collect.Lists;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.ServiceConnection;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.CharArrayBuffer;
+import android.drm.mobile1.DrmRawContent;
+import android.media.IMediaScannerService;
+import android.net.Uri;
+import android.os.RemoteException;
+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;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Performs the background downloads requested by applications that use the Downloads provider.
+ */
+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 */
+ private DownloadManagerContentObserver mObserver;
+
+ /** Class to handle Notification Manager updates */
+ private DownloadNotification mNotifier;
+
+ /**
+ * The Service's view of the list of downloads. This is kept independently
+ * from the content provider, and the Service only initiates downloads
+ * based on this data, so that it can deal with situation where the data
+ * in the content provider changes or disappears.
+ */
+ private ArrayList<DownloadInfo> mDownloads;
+
+ /**
+ * The thread that updates the internal download list from the content
+ * provider.
+ */
+ private UpdateThread updateThread;
+
+ /**
+ * Whether the internal download list should be updated from the content
+ * provider.
+ */
+ private boolean pendingUpdate;
+
+ /**
+ * The ServiceConnection object that tells us when we're connected to and disconnected from
+ * the Media Scanner
+ */
+ private MediaScannerConnection mMediaScannerConnection;
+
+ private boolean mMediaScannerConnecting;
+
+ /**
+ * The IPC interface to the Media Scanner
+ */
+ private IMediaScannerService mMediaScannerService;
+
+ /**
+ * Array used when extracting strings from content provider
+ */
+ private CharArrayBuffer oldChars;
+
+ /**
+ * Array used when extracting strings from content provider
+ */
+ private CharArrayBuffer newChars;
+
+ /* ------------ Inner Classes ------------ */
+
+ /**
+ * Receives notifications when the data in the content provider changes
+ */
+ private class DownloadManagerContentObserver extends ContentObserver {
+
+ public DownloadManagerContentObserver() {
+ super(new Handler());
+ }
+
+ /**
+ * Receives notification when the data in the observed content
+ * provider changes.
+ */
+ public void onChange(final boolean selfChange) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service ContentObserver received notification");
+ }
+ updateFromProvider();
+ }
+
+ }
+
+ /**
+ * Gets called back when the connection to the media
+ * scanner is established or lost.
+ */
+ public class MediaScannerConnection implements ServiceConnection {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Connected to Media Scanner");
+ }
+ mMediaScannerConnecting = false;
+ synchronized (DownloadService.this) {
+ mMediaScannerService = IMediaScannerService.Stub.asInterface(service);
+ if (mMediaScannerService != null) {
+ updateFromProvider();
+ }
+ }
+ }
+
+ public void disconnectMediaScanner() {
+ synchronized (DownloadService.this) {
+ if (mMediaScannerService != null) {
+ mMediaScannerService = null;
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Disconnecting from Media Scanner");
+ }
+ try {
+ unbindService(this);
+ } catch (IllegalArgumentException ex) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "unbindService threw up: " + ex);
+ }
+ }
+ }
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Disconnected from Media Scanner");
+ }
+ synchronized (DownloadService.this) {
+ mMediaScannerService = null;
+ }
+ }
+ }
+
+ /* ------------ Methods ------------ */
+
+ /**
+ * Returns an IBinder instance when someone wants to connect to this
+ * service. Binding to this service is not allowed.
+ *
+ * @throws UnsupportedOperationException
+ */
+ public IBinder onBind(Intent i) {
+ throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
+ }
+
+ /**
+ * Initializes the service when it is first created
+ */
+ public void onCreate() {
+ super.onCreate();
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service onCreate");
+ }
+
+ mDownloads = Lists.newArrayList();
+
+ mObserver = new DownloadManagerContentObserver();
+ getContentResolver().registerContentObserver(Downloads.CONTENT_URI,
+ true, mObserver);
+
+ mMediaScannerService = null;
+ mMediaScannerConnecting = false;
+ mMediaScannerConnection = new MediaScannerConnection();
+
+ mNotifier = new DownloadNotification(this);
+ mNotifier.mNotificationMgr.cancelAll();
+ mNotifier.updateNotification();
+
+ trimDatabase();
+ removeSpuriousFiles();
+ updateFromProvider();
+ }
+
+ /**
+ * Responds to a call to startService
+ */
+ public void onStart(Intent intent, int startId) {
+ super.onStart(intent, startId);
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service onStart");
+ }
+
+ updateFromProvider();
+ }
+
+ /**
+ * Cleans up when the service is destroyed
+ */
+ public void onDestroy() {
+ getContentResolver().unregisterContentObserver(mObserver);
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Service onDestroy");
+ }
+ super.onDestroy();
+ }
+
+ /**
+ * Parses data from the content provider into private array
+ */
+ private void updateFromProvider() {
+ synchronized (this) {
+ pendingUpdate = true;
+ if (updateThread == null) {
+ updateThread = new UpdateThread();
+ updateThread.start();
+ }
+ }
+ }
+
+ private class UpdateThread extends Thread {
+ public UpdateThread() {
+ super("Download Service");
+ }
+
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ boolean keepService = false;
+ // for each update from the database, remember which download is
+ // supposed to get restarted soonest in the future
+ long wakeUp = Long.MAX_VALUE;
+ for (;;) {
+ synchronized (DownloadService.this) {
+ if (updateThread != this) {
+ throw new IllegalStateException(
+ "multiple UpdateThreads in DownloadService");
+ }
+ if (!pendingUpdate) {
+ updateThread = null;
+ if (!keepService) {
+ stopSelf();
+ }
+ if (wakeUp != Long.MAX_VALUE) {
+ AlarmManager alarms =
+ (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ if (alarms == null) {
+ Log.e(Constants.TAG, "couldn't get alarm manager");
+ } else {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "scheduling retry in " + wakeUp + "ms");
+ }
+ Intent intent = new Intent(Constants.ACTION_RETRY);
+ intent.setClassName("com.android.providers.downloads",
+ DownloadReceiver.class.getName());
+ alarms.set(
+ AlarmManager.RTC_WAKEUP,
+ System.currentTimeMillis() + wakeUp,
+ PendingIntent.getBroadcast(DownloadService.this, 0, intent,
+ PendingIntent.FLAG_ONE_SHOT));
+ }
+ }
+ oldChars = null;
+ newChars = null;
+ return;
+ }
+ pendingUpdate = false;
+ }
+ boolean networkAvailable = Helpers.isNetworkAvailable(DownloadService.this);
+ long now = System.currentTimeMillis();
+
+ Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
+ null, null, null, BaseColumns._ID);
+
+ if (cursor == null) {
+ return;
+ }
+
+ cursor.moveToFirst();
+
+ int arrayPos = 0;
+
+ boolean mustScan = false;
+ keepService = false;
+ wakeUp = Long.MAX_VALUE;
+
+ boolean isAfterLast = cursor.isAfterLast();
+
+ int idColumn = cursor.getColumnIndexOrThrow(BaseColumns._ID);
+
+ /*
+ * Walk the cursor and the local array to keep them in sync. The key
+ * to the algorithm is that the ids are unique and sorted both in
+ * the cursor and in the array, so that they can be processed in
+ * order in both sources at the same time: at each step, both
+ * sources point to the lowest id that hasn't been processed from
+ * that source, and the algorithm processes the lowest id from
+ * those two possibilities.
+ * At each step:
+ * -If the array contains an entry that's not in the cursor, remove the
+ * entry, move to next entry in the array.
+ * -If the array contains an entry that's in the cursor, nothing to do,
+ * move to next cursor row and next array entry.
+ * -If the cursor contains an entry that's not in the array, insert
+ * a new entry in the array, move to next cursor row and next
+ * array entry.
+ */
+ while (!isAfterLast || arrayPos < mDownloads.size()) {
+ if (isAfterLast) {
+ // We're beyond the end of the cursor but there's still some
+ // 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);
+ }
+ if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
+ scanFile(null, arrayPos);
+ }
+ deleteDownload(arrayPos); // this advances in the array
+ } else {
+ int id = cursor.getInt(idColumn);
+
+ if (arrayPos == mDownloads.size()) {
+ insertDownload(cursor, arrayPos, networkAvailable, now);
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
+ }
+ if (shouldScanFile(arrayPos)
+ && (!mediaScannerConnected() || !scanFile(cursor, arrayPos))) {
+ mustScan = true;
+ keepService = true;
+ }
+ if (visibleNotification(arrayPos)) {
+ keepService = true;
+ }
+ long next = nextAction(arrayPos, now);
+ if (next == 0) {
+ keepService = true;
+ } else if (next > 0 && next < wakeUp) {
+ wakeUp = next;
+ }
+ ++arrayPos;
+ cursor.moveToNext();
+ isAfterLast = cursor.isAfterLast();
+ } else {
+ int arrayId = mDownloads.get(arrayPos).id;
+
+ if (arrayId < id) {
+ // The array entry isn't in the cursor
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Array update: removing " + arrayId
+ + " @ " + arrayPos);
+ }
+ if (shouldScanFile(arrayPos) && mediaScannerConnected()) {
+ scanFile(null, arrayPos);
+ }
+ 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);
+ if (shouldScanFile(arrayPos)
+ && (!mediaScannerConnected()
+ || !scanFile(cursor, arrayPos))) {
+ mustScan = true;
+ keepService = true;
+ }
+ if (visibleNotification(arrayPos)) {
+ keepService = true;
+ }
+ long next = nextAction(arrayPos, now);
+ if (next == 0) {
+ keepService = true;
+ } else if (next > 0 && next < wakeUp) {
+ wakeUp = next;
+ }
+ ++arrayPos;
+ cursor.moveToNext();
+ isAfterLast = cursor.isAfterLast();
+ } else {
+ // This cursor entry didn't exist in the stored array
+ if (Constants.LOGVV) {
+ Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
+ }
+ insertDownload(cursor, arrayPos, networkAvailable, now);
+ if (shouldScanFile(arrayPos)
+ && (!mediaScannerConnected()
+ || !scanFile(cursor, arrayPos))) {
+ mustScan = true;
+ keepService = true;
+ }
+ if (visibleNotification(arrayPos)) {
+ keepService = true;
+ }
+ long next = nextAction(arrayPos, now);
+ if (next == 0) {
+ keepService = true;
+ } else if (next > 0 && next < wakeUp) {
+ wakeUp = next;
+ }
+ ++arrayPos;
+ cursor.moveToNext();
+ isAfterLast = cursor.isAfterLast();
+ }
+ }
+ }
+ }
+
+ mNotifier.updateNotification();
+
+ if (mustScan) {
+ if (!mMediaScannerConnecting) {
+ Intent intent = new Intent();
+ intent.setClassName("com.android.providers.media",
+ "com.android.providers.media.MediaScannerService");
+ mMediaScannerConnecting = true;
+ bindService(intent, mMediaScannerConnection, BIND_AUTO_CREATE);
+ }
+ } else {
+ mMediaScannerConnection.disconnectMediaScanner();
+ }
+
+ if (!cursor.commitUpdates()) {
+ Log.e(Constants.TAG, "commitUpdates failed in updateFromProvider");
+ }
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Removes files that may have been left behind in the cache directory
+ */
+ private void removeSpuriousFiles() {
+ File[] files = Environment.getDownloadCacheDirectory().listFiles();
+ if (files == null) {
+ // The cache folder doesn't appear to exist (this is likely the case
+ // when running the simulator).
+ return;
+ }
+ HashSet<String> fileSet = new HashSet();
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
+ continue;
+ }
+ if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
+ continue;
+ }
+ fileSet.add(files[i].getPath());
+ }
+
+ Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
+ new String[] { Downloads.FILENAME }, null, null, null);
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ do {
+ fileSet.remove(cursor.getString(0));
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ }
+ Iterator<String> iterator = fileSet.iterator();
+ while (iterator.hasNext()) {
+ String filename = iterator.next();
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "deleting spurious file " + filename);
+ }
+ new File(filename).delete();
+ }
+ }
+
+ /**
+ * Drops old rows from the database to prevent it from growing too large
+ */
+ private void trimDatabase() {
+ Cursor cursor = getContentResolver().query(Downloads.CONTENT_URI,
+ new String[] { Downloads._ID },
+ 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");
+ return;
+ }
+ if (cursor.moveToFirst()) {
+ while (cursor.getCount() > Constants.MAX_DOWNLOADS) {
+ cursor.deleteRow();
+ }
+ }
+ cursor.close();
+ }
+
+ /**
+ * 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) {
+ int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
+ int failedColumn = cursor.getColumnIndexOrThrow(Downloads.FAILED_CONNECTIONS);
+ 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.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),
+ cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_PACKAGE)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_CLASS)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.NOTIFICATION_EXTRAS)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.COOKIE_DATA)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.USER_AGENT)),
+ 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);
+
+ 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);
+ }
+
+ mDownloads.add(arrayPos, info);
+
+ if (info.status == 0
+ && (info.destination == Downloads.DESTINATION_EXTERNAL
+ || info.destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE)
+ && info.mimetype != null
+ && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mimetype)) {
+ // Check to see if we are allowed to download this file. Only files
+ // that can be handled by the platform can be downloaded.
+ // special case DRM files, which we should always allow downloading.
+ Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
+
+ // We can provide data as either content: or file: URIs,
+ // so allow both. (I think it would be nice if we just did
+ // everything as content: URIs)
+ // Actually, right now the download manager's UId restrictions
+ // prevent use from using content: so it's got to be file: or
+ // nothing
+
+ mimetypeIntent.setDataAndType(Uri.fromParts("file", "", null), info.mimetype);
+ List<ResolveInfo> list = getPackageManager().queryIntentActivities(mimetypeIntent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ //Log.i(TAG, "*** QUERY " + mimetypeIntent + ": " + list);
+
+ if (list.size() == 0
+ || (info.noSystem && info.mimetype.equalsIgnoreCase(Constants.MIMETYPE_APK))) {
+ 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");
+ info.sendIntentIfRequested(uri, this);
+ return;
+ }
+ }
+
+ if (networkAvailable) {
+ if (info.isReadyToStart(now)) {
+ if (Constants.LOGV) {
+ Log.v(TAG, "Service spawning thread to handle new download " + info.id);
+ }
+ if (info.hasActiveThread) {
+ throw new IllegalStateException("Multiple threads on same download on insert");
+ }
+ if (info.status != Downloads.STATUS_RUNNING) {
+ info.status = Downloads.STATUS_RUNNING;
+ ContentValues values = new ContentValues();
+ values.put(Downloads.STATUS, info.status);
+ getContentResolver().update(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id),
+ values, null, null);
+ }
+ DownloadThread downloader = new DownloadThread(this, info);
+ info.hasActiveThread = true;
+ downloader.start();
+ }
+ } else {
+ if (info.status == 0
+ || info.status == Downloads.STATUS_PENDING
+ || info.status == Downloads.STATUS_RUNNING) {
+ info.status = Downloads.STATUS_RUNNING_PAUSED;
+ cursor.updateInt(statusColumn, Downloads.STATUS_RUNNING_PAUSED);
+ }
+ }
+ }
+
+ /**
+ * Updates the local copy of the info about a download.
+ */
+ private void updateDownload(Cursor cursor, int arrayPos, boolean networkAvailable, long now) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ int statusColumn = cursor.getColumnIndexOrThrow(Downloads.STATUS);
+ int failedColumn = cursor.getColumnIndexOrThrow(Downloads.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.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
+ && Downloads.isStatusCompleted(info.status)) {
+ mNotifier.mNotificationMgr.cancel(info.id);
+ }
+ info.visibility = newVisibility;
+ 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);
+ 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);
+ info.cookies = stringFromCursor(info.cookies, cursor, Downloads.COOKIE_DATA);
+ info.userAgent = stringFromCursor(info.userAgent, cursor, Downloads.USER_AGENT);
+ 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.mediaScanned =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED)) == 1;
+
+ if (networkAvailable) {
+ if (info.isReadyToRestart(now)) {
+ if (Constants.LOGV) {
+ Log.v(TAG, "Service spawning thread to handle updated download " + info.id);
+ }
+ if (info.hasActiveThread) {
+ throw new IllegalStateException("Multiple threads on same download on update");
+ }
+ info.status = Downloads.STATUS_RUNNING;
+ ContentValues values = new ContentValues();
+ values.put(Downloads.STATUS, info.status);
+ getContentResolver().update(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI, info.id),
+ values, null, null);
+ DownloadThread downloader = new DownloadThread(this, info);
+ info.hasActiveThread = true;
+ downloader.start();
+ }
+ }
+ }
+
+ /**
+ * Returns a String that holds the current value of the column,
+ * optimizing for the case where the value hasn't changed.
+ */
+ private String stringFromCursor(String old, Cursor cursor, String column) {
+ int index = cursor.getColumnIndexOrThrow(column);
+ if (old == null) {
+ return cursor.getString(index);
+ }
+ if (newChars == null) {
+ newChars = new CharArrayBuffer(128);
+ }
+ cursor.copyStringToBuffer(index, newChars);
+ int length = newChars.sizeCopied;
+ if (length != old.length()) {
+ return cursor.getString(index);
+ }
+ if (oldChars == null || oldChars.sizeCopied < length) {
+ oldChars = new CharArrayBuffer(length);
+ }
+ char[] oldArray = oldChars.data;
+ char[] newArray = newChars.data;
+ old.getChars(0, length, oldArray, 0);
+ for (int i = length - 1; i >= 0; --i) {
+ if (oldArray[i] != newArray[i]) {
+ return new String(newArray, 0, length);
+ }
+ }
+ return old;
+ }
+
+ /**
+ * Removes the local copy of the info about a download.
+ */
+ private void deleteDownload(int arrayPos) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ if (info.status == Downloads.STATUS_RUNNING) {
+ info.status = Downloads.STATUS_CANCELED;
+ } else if (info.destination != Downloads.DESTINATION_EXTERNAL && info.filename != null) {
+ new File(info.filename).delete();
+ }
+ mNotifier.mNotificationMgr.cancel(info.id);
+
+ mDownloads.remove(arrayPos);
+ }
+
+ /**
+ * Returns the amount of time (as measured from the "now" parameter)
+ * at which a download will be active.
+ * 0 = immediately - service should stick around to handle this download.
+ * -1 = never - service can go away without ever waking up.
+ * positive value - service must wake up in the future, as specified in ms from "now"
+ */
+ private long nextAction(int arrayPos, long now) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ if (Downloads.isStatusCompleted(info.status)) {
+ return -1;
+ }
+ if (info.status != Downloads.STATUS_RUNNING_PAUSED) {
+ return 0;
+ }
+ if (info.numFailed == 0) {
+ return 0;
+ }
+ long when = info.restartTime();
+ if (when <= now) {
+ return 0;
+ }
+ return when - now;
+ }
+
+ /**
+ * Returns whether there's a visible notification for this download
+ */
+ private boolean visibleNotification(int arrayPos) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ return info.hasCompletionNotification();
+ }
+
+ /**
+ * Returns whether a file should be scanned
+ */
+ private boolean shouldScanFile(int arrayPos) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ return !info.mediaScanned
+ && info.destination == Downloads.DESTINATION_EXTERNAL
+ && Downloads.isStatusSuccess(info.status)
+ && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(info.mimetype);
+ }
+
+ /**
+ * Returns whether we have a live connection to the Media Scanner
+ */
+ private boolean mediaScannerConnected() {
+ return mMediaScannerService != null;
+ }
+
+ /**
+ * Attempts to scan the file if necessary.
+ * Returns true if the file has been properly scanned.
+ */
+ private boolean scanFile(Cursor cursor, int arrayPos) {
+ DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
+ synchronized (this) {
+ if (mMediaScannerService != null) {
+ try {
+ if (Constants.LOGV) {
+ Log.v(TAG, "Scanning file " + info.filename);
+ }
+ mMediaScannerService.scanFile(info.filename, info.mimetype);
+ if (cursor != null) {
+ cursor.updateInt(cursor.getColumnIndexOrThrow(Downloads.MEDIA_SCANNED), 1);
+ }
+ return true;
+ } catch (RemoteException e) {
+ if (Config.LOGD) {
+ Log.d(TAG, "Failed to scan file " + info.filename);
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
new file mode 100644
index 00000000..66417b3e
--- /dev/null
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -0,0 +1,643 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import org.apache.http.client.methods.AbortableHttpRequest;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.HttpClient;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.drm.mobile1.DrmRawContent;
+import android.net.http.AndroidHttpClient;
+import android.net.Uri;
+import android.os.FileUtils;
+import android.os.PowerManager;
+import android.os.Process;
+import android.provider.Downloads;
+import android.provider.DrmStore;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * 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;
+
+ public DownloadThread(Context context, DownloadInfo info) {
+ mContext = context;
+ mInfo = info;
+ }
+
+ /**
+ * Returns the user agent provided by the initiating app, or use the default one
+ */
+ private String userAgent() {
+ String userAgent = mInfo.userAgent;
+ if (userAgent != null) {
+ }
+ if (userAgent == null) {
+ userAgent = Constants.DEFAULT_USER_AGENT;
+ }
+ return userAgent;
+ }
+
+ /**
+ * Executes the download in a separate thread
+ */
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ int finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
+ boolean countRetry = false;
+ boolean gotData = false;
+ String filename = null;
+ String mimeType = mInfo.mimetype;
+ FileOutputStream stream = null;
+ AndroidHttpClient client = null;
+ PowerManager.WakeLock wakeLock = null;
+ Uri contentUri = Uri.parse(Downloads.CONTENT_URI + "/" + mInfo.id);
+
+ try {
+ boolean continuingDownload = false;
+ String headerAcceptRanges = null;
+ String headerContentDisposition = null;
+ String headerContentLength = null;
+ String headerContentLocation = null;
+ String headerETag = null;
+ String headerTransferEncoding = null;
+
+ byte data[] = new byte[Constants.BUFFER_SIZE];
+
+ int bytesSoFar = 0;
+
+ PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+ wakeLock.acquire();
+
+ if (mInfo.filename != null) {
+ // We're resuming a download that got interrupted
+ File f = new File(mInfo.filename);
+ if (f.exists()) {
+ long fileLength = f.length();
+ if (fileLength == 0) {
+ // The download hadn't actually started, we can restart from scratch
+ f.delete();
+ } 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");
+ }
+ f.delete();
+ finalStatus = Downloads.STATUS_PRECONDITION_FAILED;
+ notifyDownloadCompleted(
+ finalStatus, false, false, mInfo.filename, 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) {
+ headerContentLength = Integer.toString(mInfo.totalBytes);
+ }
+ headerETag = mInfo.etag;
+ continuingDownload = true;
+ }
+ }
+ }
+
+ int bytesNotified = bytesSoFar;
+ // starting with MIN_VALUE means that the first write will commit
+ // progress to the database
+ long timeLastNotification = 0;
+
+ client = AndroidHttpClient.newInstance(userAgent());
+
+ if (stream != null && mInfo.destination == Downloads.DESTINATION_EXTERNAL
+ && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
+ .equalsIgnoreCase(mimeType)) {
+ try {
+ stream.close();
+ stream = null;
+ } catch (IOException ex) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "exception when closing the file before download : " +
+ ex);
+ }
+ // nothing can really be done if the file can't be closed
+ }
+ }
+
+ /*
+ * This loop is run once for every individual HTTP request that gets sent.
+ * The very first HTTP request is a "virgin" request, while every subsequent
+ * request is done with the original ETag and a byte-range.
+ */
+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;
+ }
+
+ if (Constants.LOGV) {
+ Log.v(TAG, "initiating download for " + mInfo.uri);
+ }
+
+ if (mInfo.cookies != null) {
+ requestU.addHeader("Cookie", mInfo.cookies);
+ }
+ if (mInfo.referer != null) {
+ requestU.addHeader("Referer", mInfo.referer);
+ }
+ if (continuingDownload) {
+ if (headerETag != null) {
+ requestU.addHeader("If-Match", headerETag);
+ }
+ requestU.addHeader("Range", "bytes=" + bytesSoFar + "-");
+ }
+
+ HttpResponse response;
+ try {
+ response = client.execute(requestU);
+ } catch (IllegalArgumentException ex) {
+ if (Constants.LOGV) {
+ Log.d(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);
+ }
+ finalStatus = Downloads.STATUS_BAD_REQUEST;
+ requestA.abort();
+ break http_request_loop;
+ } catch (IOException ex) {
+ 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.d(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);
+ }
+ finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
+ }
+ requestA.abort();
+ break http_request_loop;
+ }
+
+ int statusCode = response.getStatusLine().getStatusCode();
+ if ((!continuingDownload && statusCode != Downloads.STATUS_SUCCESS)
+ || (continuingDownload && statusCode != 206)) {
+ if (Constants.LOGV) {
+ Log.d(TAG, "http error " + statusCode + " for " + mInfo.uri);
+ } else if (Config.LOGD) {
+ Log.d(TAG, "http error " + statusCode + " for download " + mInfo.id);
+ }
+ if (Downloads.isStatusError(statusCode)) {
+ finalStatus = statusCode;
+ } else if (statusCode >= 300 && statusCode < 400) {
+ finalStatus = Downloads.STATUS_UNHANDLED_REDIRECT;
+ } else if (continuingDownload && statusCode == Downloads.STATUS_SUCCESS) {
+ finalStatus = Downloads.STATUS_PRECONDITION_FAILED;
+ } else {
+ finalStatus = Downloads.STATUS_UNHANDLED_HTTP_CODE;
+ }
+ requestA.abort();
+ break http_request_loop;
+ } else {
+ // Handles the response, saves the file
+ if (Constants.LOGV) {
+ Log.v(TAG, "received response for " + mInfo.uri);
+ }
+
+ if (!continuingDownload) {
+ Header header = response.getFirstHeader("Accept-Ranges");
+ if (header != null) {
+ headerAcceptRanges = header.getValue();
+ }
+ header = response.getFirstHeader("Content-Disposition");
+ if (header != null) {
+ headerContentDisposition = header.getValue();
+ }
+ header = response.getFirstHeader("Content-Location");
+ if (header != null) {
+ headerContentLocation = header.getValue();
+ }
+ if (mimeType == null) {
+ header = response.getFirstHeader("Content-Type");
+ if (header != null) {
+ mimeType = header.getValue();
+ final int semicolonIndex = mimeType.indexOf(';');
+ if (semicolonIndex != -1) {
+ mimeType = mimeType.substring(0, semicolonIndex);
+ }
+ }
+ }
+ header = response.getFirstHeader("ETag");
+ if (header != null) {
+ headerETag = header.getValue();
+ }
+ header = response.getFirstHeader("Transfer-Encoding");
+ if (header != null) {
+ headerTransferEncoding = header.getValue();
+ }
+ if (headerTransferEncoding == null) {
+ header = response.getFirstHeader("Content-Length");
+ if (header != null) {
+ headerContentLength = header.getValue();
+ }
+ } else {
+ // Ignore content-length with transfer-encoding - 2616 4.4 3
+ if (Constants.LOGVV) {
+ Log.v(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);
+ }
+
+ if (!mInfo.noIntegrity && headerContentLength == null &&
+ (headerTransferEncoding == null
+ || !headerTransferEncoding.equalsIgnoreCase("chunked"))
+ ) {
+ if (Config.LOGD) {
+ Log.d(TAG, "can't know size of download, giving up");
+ }
+ finalStatus = Downloads.STATUS_LENGTH_REQUIRED;
+ requestA.abort();
+ break http_request_loop;
+ }
+
+ DownloadFileInfo fileInfo = Helpers.generateSaveFile(
+ mContext,
+ mInfo.uri,
+ mInfo.hint,
+ headerContentDisposition,
+ headerContentLocation,
+ mimeType,
+ mInfo.destination,
+ mInfo.otaUpdate,
+ mInfo.noSystem,
+ (headerContentLength != null) ?
+ Integer.parseInt(headerContentLength) : 0);
+ if (fileInfo.filename == null) {
+ finalStatus = fileInfo.status;
+ requestA.abort();
+ break http_request_loop;
+ }
+ filename = fileInfo.filename;
+ stream = fileInfo.stream;
+ if (Constants.LOGV) {
+ Log.v(TAG, "writing " + mInfo.uri + " to " + filename);
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Downloads.FILENAME, filename);
+ if (headerETag != null) {
+ values.put(Downloads.ETAG, headerETag);
+ }
+ if (mimeType != null) {
+ values.put(Downloads.MIMETYPE, mimeType);
+ }
+ int contentLength = -1;
+ if (headerContentLength != null) {
+ contentLength = Integer.parseInt(headerContentLength);
+ }
+ values.put(Downloads.TOTAL_BYTES, contentLength);
+ mContext.getContentResolver().update(contentUri, values, null, null);
+ }
+
+ InputStream entityStream;
+ try {
+ entityStream = response.getEntity().getContent();
+ } catch (IOException ex) {
+ 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.d(TAG, "IOException getting entity for " + mInfo.uri +
+ " : " + ex);
+ } else if (Config.LOGD) {
+ Log.d(TAG, "IOException getting entity for download " + mInfo.id +
+ " : " + ex);
+ }
+ finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
+ }
+ requestA.abort();
+ break http_request_loop;
+ }
+ for (;;) {
+ int bytesRead;
+ try {
+ bytesRead = entityStream.read(data);
+ } catch (IOException ex) {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.CURRENT_BYTES, bytesSoFar);
+ mContext.getContentResolver().update(contentUri, values, null, null);
+ if (!mInfo.noIntegrity && headerETag == null) {
+ if (Constants.LOGV) {
+ Log.v(TAG, "download IOException for " + mInfo.uri +
+ " : " + ex);
+ } else if (Config.LOGD) {
+ Log.d(TAG, "download IOException for download " + mInfo.id +
+ " : " + ex);
+ }
+ if (Config.LOGD) {
+ Log.d(Constants.TAG,
+ "can't resume interrupted download with no ETag");
+ }
+ finalStatus = Downloads.STATUS_PRECONDITION_FAILED;
+ } 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(TAG, "download IOException for " + mInfo.uri +
+ " : " + ex);
+ } else if (Config.LOGD) {
+ Log.d(TAG, "download IOException for download " + mInfo.id +
+ " : " + ex);
+ }
+ finalStatus = Downloads.STATUS_HTTP_DATA_ERROR;
+ }
+ requestA.abort();
+ break http_request_loop;
+ }
+ if (bytesRead == -1) { // success
+ ContentValues values = new ContentValues();
+ values.put(Downloads.CURRENT_BYTES, bytesSoFar);
+ if (headerContentLength == null) {
+ values.put(Downloads.TOTAL_BYTES, bytesSoFar);
+ }
+ mContext.getContentResolver().update(contentUri, values, null, null);
+ 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);
+ }
+ finalStatus = Downloads.STATUS_LENGTH_REQUIRED;
+ break http_request_loop;
+ }
+ break;
+ }
+ gotData = true;
+ for (;;) {
+ try {
+ if (stream == null) {
+ stream = new FileOutputStream(filename, true);
+ }
+ stream.write(data, 0, bytesRead);
+ if (mInfo.destination == Downloads.DESTINATION_EXTERNAL
+ && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
+ .equalsIgnoreCase(mimeType)) {
+ try {
+ stream.close();
+ stream = null;
+ } catch (IOException ex) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG,
+ "exception when closing the file " +
+ "during download : " + ex);
+ }
+ // nothing can really be done if the file can't be closed
+ }
+ }
+ break;
+ } catch (IOException ex) {
+ if (!Helpers.discardPurgeableFiles(
+ mContext, Constants.BUFFER_SIZE)) {
+ finalStatus = Downloads.STATUS_FILE_ERROR;
+ break http_request_loop;
+ }
+ }
+ }
+ bytesSoFar += bytesRead;
+ long now = System.currentTimeMillis();
+ if (bytesSoFar - bytesNotified > Constants.MIN_PROGRESS_STEP
+ && now - timeLastNotification
+ > Constants.MIN_PROGRESS_TIME) {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.CURRENT_BYTES, bytesSoFar);
+ mContext.getContentResolver().update(
+ contentUri, values, null, null);
+ bytesNotified = bytesSoFar;
+ timeLastNotification = now;
+ }
+
+ if (Constants.LOGVV) {
+ Log.v(TAG, "downloaded " + bytesSoFar + " for " + mInfo.uri);
+ }
+ if (mInfo.status == Downloads.STATUS_CANCELED) {
+ if (Constants.LOGV) {
+ Log.d(TAG, "canceled " + mInfo.uri);
+ } else if (Config.LOGD) {
+ // Log.d(TAG, "canceled id " + mInfo.id);
+ }
+ finalStatus = Downloads.STATUS_CANCELED;
+ break http_request_loop;
+ }
+ }
+ if (Constants.LOGV) {
+ Log.v(TAG, "download completed for " + mInfo.uri);
+ }
+ finalStatus = Downloads.STATUS_SUCCESS;
+ }
+ break;
+ }
+ } catch (FileNotFoundException ex) {
+ if (Config.LOGD) {
+ Log.d(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);
+ } else if (Config.LOGD) {
+ Log.d(TAG, "Exception for id " + mInfo.id + " : " + ex);
+ }
+ finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
+ // falls through to the code that reports an error
+ } finally {
+ mInfo.hasActiveThread = false;
+ if (wakeLock != null) {
+ wakeLock.release();
+ wakeLock = null;
+ }
+ if (client != null) {
+ client.close();
+ client = null;
+ }
+ try {
+ // close the file
+ if (stream != null) {
+ stream.close();
+ }
+ } catch (IOException ex) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
+ }
+ // nothing can really be done if the file can't be closed
+ }
+ if (filename != null) {
+ // if the download wasn't successful, delete the file
+ if (Downloads.isStatusError(finalStatus)) {
+ new File(filename).delete();
+ filename = null;
+ } else if (Downloads.isStatusSuccess(finalStatus) &&
+ DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
+ .equalsIgnoreCase(mimeType)) {
+ // transfer the file to the DRM content provider
+ 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");
+ finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
+ } else {
+ filename = item.getDataString();
+ mimeType = item.getType();
+ }
+
+ file.delete();
+ } else if (Downloads.isStatusSuccess(finalStatus)) {
+ // make sure the file is readable
+ FileUtils.setPermissions(filename, 0644, -1, -1);
+ }
+ }
+ notifyDownloadCompleted(finalStatus, countRetry, gotData, filename, mimeType);
+ }
+ }
+
+ /**
+ * 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);
+ 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();
+ }
+ }
+
+ /**
+ * Notifies the initiating app if it requested it. That way, it can know that the
+ * download completed even if it's not actively watching the cursor.
+ */
+ private void notifyThroughIntent() {
+ Uri uri = Uri.parse(Downloads.CONTENT_URI + "/" + mInfo.id);
+ Intent intent = new Intent(Downloads.DOWNLOAD_COMPLETED_ACTION);
+ intent.setData(uri);
+ mContext.sendBroadcast(intent, "android.permission.ACCESS_DOWNLOAD_DATA");
+ mInfo.sendIntentIfRequested(uri, mContext);
+ }
+
+}
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
new file mode 100644
index 00000000..f966a7f5
--- /dev/null
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.drm.mobile1.DrmRawContent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.StatFs;
+import android.provider.Downloads;
+import android.util.Config;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.util.List;
+import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Some helper functions for the download manager
+ */
+public class Helpers {
+ /** Tag used for debugging/logging */
+ private static final String TAG = Constants.TAG;
+
+ /** Regex used to parse content-disposition headers */
+ private static final Pattern CONTENT_DISPOSITION_PATTERN =
+ Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+
+ private Helpers() {
+ }
+
+ /*
+ * Parse the Content-Disposition HTTP Header. The format of the header
+ * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
+ * This header provides a filename for content that is going to be
+ * downloaded to the file system. We only support the attachment type.
+ */
+ private static String parseContentDisposition(String contentDisposition) {
+ try {
+ Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
+ if (m.find()) {
+ return m.group(1);
+ }
+ } catch (IllegalStateException ex) {
+ // This function is defined as returning null when it can't parse the header
+ }
+ return null;
+ }
+
+ /**
+ * Creates a filename (where the file should be saved) from a uri.
+ */
+ public static DownloadFileInfo generateSaveFile(
+ Context context,
+ String url,
+ String hint,
+ String contentDisposition,
+ String contentLocation,
+ String mimeType,
+ int destination,
+ boolean otaUpdate,
+ boolean noSystem,
+ int contentLength) throws FileNotFoundException {
+
+ /*
+ * Don't download files that we won't be able to handle
+ */
+ if (destination == Downloads.DESTINATION_EXTERNAL
+ || 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");
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
+ }
+ if (!DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
+ // Check to see if we are allowed to download this file. Only files
+ // that can be handled by the platform can be downloaded.
+ // special case DRM files, which we should always allow downloading.
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+
+ // We can provide data as either content: or file: URIs,
+ // so allow both. (I think it would be nice if we just did
+ // everything as content: URIs)
+ // Actually, right now the download manager's UId restrictions
+ // prevent use from using content: so it's got to be file: or
+ // nothing
+
+ PackageManager pm = context.getPackageManager();
+ intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
+ List<ResolveInfo> list = pm.queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ //Log.i(TAG, "*** FILENAME QUERY " + intent + ": " + list);
+
+ if (list.size() == 0) {
+ if (Config.LOGD) {
+ Log.d(Constants.TAG, "no handler found for type " + mimeType);
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
+ }
+ }
+ }
+ String filename = chooseFilename(
+ url, hint, contentDisposition, contentLocation, destination, otaUpdate);
+
+ // Split filename between base and extension
+ // Add an extension if filename does not have one
+ String extension = null;
+ int dotIndex = filename.indexOf('.');
+ if (dotIndex < 0) {
+ extension = chooseExtensionFromMimeType(mimeType, true);
+ } else {
+ extension = chooseExtensionFromFilename(
+ mimeType, destination, otaUpdate, filename, dotIndex);
+ filename = filename.substring(0, dotIndex);
+ }
+
+ /*
+ * Locate the directory where the file will be saved
+ */
+
+ File base = null;
+ StatFs stat = null;
+ // DRM messages should be temporarily stored internally and then passed to
+ // the DRM content provider
+ if (destination == Downloads.DESTINATION_CACHE_PARTITION
+ || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE
+ || DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
+ base = Environment.getDownloadCacheDirectory();
+ stat = new StatFs(base.getPath());
+
+ /*
+ * Check whether there's enough space on the target filesystem to save the file.
+ * Put a bit of margin (in case creating the file grows the system by a few blocks).
+ */
+ int blockSize = stat.getBlockSize();
+ for (;;) {
+ int availableBlocks = stat.getAvailableBlocks();
+ if (blockSize * ((long) availableBlocks - 4) >= contentLength) {
+ break;
+ }
+ if (!discardPurgeableFiles(context,
+ contentLength - blockSize * ((long) availableBlocks - 4))) {
+ if (Config.LOGD) {
+ Log.d(TAG, "download aborted - not enough free space in internal storage");
+ }
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ }
+ stat.restat(base.getPath());
+ }
+
+ } else {
+ if (destination == Downloads.DESTINATION_DATA_CACHE) {
+ base = context.getCacheDir();
+ 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 (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);
+ }
+ }
+
+ /*
+ * Check whether there's enough space on the target filesystem to save the file.
+ * Put a bit of margin (in case creating the file grows the system by a few blocks).
+ */
+ if (stat.getBlockSize() * ((long) stat.getAvailableBlocks() - 4) < contentLength) {
+ if (Config.LOGD) {
+ Log.d(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;
+
+ /*
+ * Generate a unique filename, create the file, return it.
+ */
+ if (Constants.LOGVV) {
+ Log.v(TAG, "target file: " + filename + extension);
+ }
+
+ String fullFilename = chooseUniqueFilename(
+ destination, otaUpdate, filename, extension, otaFilename, recoveryDir);
+ if (fullFilename != null) {
+ return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
+ } else {
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ }
+ }
+
+ private static String chooseFilename(String url, String hint, String contentDisposition,
+ String contentLocation, int destination, boolean otaUpdate) {
+ 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");
+ }
+ int index = hint.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = hint.substring(index);
+ } else {
+ filename = hint;
+ }
+ }
+
+ // If we couldn't do anything with the hint, move toward the content disposition
+ if (filename == null && contentDisposition != null) {
+ filename = parseContentDisposition(contentDisposition);
+ if (filename != null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "getting filename from content-disposition");
+ }
+ int index = filename.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = filename.substring(index);
+ }
+ }
+ }
+
+ // If we still have nothing at this point, try the content location
+ if (filename == null && contentLocation != null) {
+ String decodedContentLocation = Uri.decode(contentLocation);
+ if (decodedContentLocation != null
+ && !decodedContentLocation.endsWith("/")
+ && decodedContentLocation.indexOf('?') < 0) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "getting filename from content-location");
+ }
+ int index = decodedContentLocation.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = decodedContentLocation.substring(index);
+ } else {
+ filename = decodedContentLocation;
+ }
+ }
+ }
+
+ // If all the other http-related approaches failed, use the plain uri
+ if (filename == null) {
+ String decodedUrl = Uri.decode(url);
+ if (decodedUrl != null
+ && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) {
+ int index = decodedUrl.lastIndexOf('/') + 1;
+ if (index > 0) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "getting filename from uri");
+ }
+ filename = decodedUrl.substring(index);
+ }
+ }
+ }
+
+ // Finally, if couldn't get filename from URI, get a generic filename
+ if (filename == null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "using default filename");
+ }
+ filename = Constants.DEFAULT_DL_FILENAME;
+ }
+ return filename;
+ }
+
+ private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) {
+ String extension = null;
+ if (mimeType != null) {
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ if (extension != null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "adding extension from type");
+ }
+ extension = "." + extension;
+ } else {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "couldn't find extension for " + mimeType);
+ }
+ }
+ }
+ if (extension == null) {
+ if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
+ if (mimeType.equalsIgnoreCase("text/html")) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "adding default html extension");
+ }
+ extension = Constants.DEFAULT_DL_HTML_EXTENSION;
+ } else if (useDefaults) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "adding default text extension");
+ }
+ extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
+ }
+ } else if (useDefaults) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "adding default binary extension");
+ }
+ extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
+ }
+ }
+ return extension;
+ }
+
+ private static String chooseExtensionFromFilename(String mimeType, int destination,
+ boolean otaUpdate, String filename, int dotIndex) {
+ String extension = null;
+ if (mimeType != null
+ && !(destination == Downloads.DESTINATION_CACHE_PARTITION && otaUpdate)) {
+ // Compare the last segment of the extension against the mime type.
+ // If there's a mismatch, discard the entire extension.
+ int lastDotIndex = filename.lastIndexOf('.');
+ String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+ filename.substring(lastDotIndex + 1));
+ if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) {
+ extension = chooseExtensionFromMimeType(mimeType, false);
+ if (extension != null) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "substituting extension from type");
+ }
+ } else {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "couldn't find extension for " + mimeType);
+ }
+ }
+ }
+ }
+ if (extension == null) {
+ if (Constants.LOGVV) {
+ Log.v(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) {
+ 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))) {
+ 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;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Deletes purgeable files from the cache partition. This also deletes
+ * the matching database entries. Files are deleted in LRU order until
+ * the total byte size is greater than targetBytes.
+ */
+ public static final boolean discardPurgeableFiles(Context context, long targetBytes) {
+ Cursor cursor = context.getContentResolver().query(
+ Downloads.CONTENT_URI,
+ null,
+ "( " +
+ Downloads.STATUS + " = " + Downloads.STATUS_SUCCESS + " AND " +
+ Downloads.DESTINATION + " = " + Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE
+ + " )",
+ null,
+ Downloads.LAST_MODIFICATION);
+ if (cursor == null) {
+ return false;
+ }
+ long totalFreed = 0;
+ try {
+ cursor.moveToFirst();
+ while (!cursor.isAfterLast() && totalFreed < targetBytes) {
+ File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.FILENAME)));
+ 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()
+ }
+ } finally {
+ cursor.close();
+ }
+ if (Constants.LOGV) {
+ if (totalFreed > 0) {
+ Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
+ targetBytes + " requested");
+ }
+ }
+ return totalFreed > 0;
+ }
+
+ /**
+ * Returns whether the network is available
+ */
+ public static boolean isNetworkAvailable(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) {
+ if (info.getState() == NetworkInfo.State.CONNECTED) {
+ if (Constants.LOGVV) {
+ Log.v(TAG, "network is available");
+ }
+ return true;
+ }
+ }
+ }
+ if (Constants.LOGVV) {
+ Log.v(TAG, "network is not available");
+ }
+ return false;
+ }
+
+}