OSDN Git Service

Initial Contribution
authorThe Android Open Source Project <initial-contribution@android.com>
Tue, 21 Oct 2008 14:00:00 +0000 (07:00 -0700)
committerThe Android Open Source Project <initial-contribution@android.com>
Tue, 21 Oct 2008 14:00:00 +0000 (07:00 -0700)
21 files changed:
Android.mk [new file with mode: 0644]
AndroidManifest.xml [new file with mode: 0644]
MODULE_LICENSE_APACHE2 [new file with mode: 0644]
NOTICE [new file with mode: 0644]
res/layout/status_bar_ongoing_event_progress_bar.xml [new file with mode: 0644]
res/values-de-rDE/strings.xml [new file with mode: 0644]
res/values-en-rGB/strings.xml [new file with mode: 0644]
res/values-es-rUS/strings.xml [new file with mode: 0644]
res/values-fr-rFR/strings.xml [new file with mode: 0644]
res/values-it-rIT/strings.xml [new file with mode: 0644]
res/values-zh-rTW/strings.xml [new file with mode: 0644]
res/values/strings.xml [new file with mode: 0644]
src/com/android/providers/downloads/Constants.java [new file with mode: 0644]
src/com/android/providers/downloads/DownloadFileInfo.java [new file with mode: 0644]
src/com/android/providers/downloads/DownloadInfo.java [new file with mode: 0644]
src/com/android/providers/downloads/DownloadNotification.java [new file with mode: 0644]
src/com/android/providers/downloads/DownloadProvider.java [new file with mode: 0644]
src/com/android/providers/downloads/DownloadReceiver.java [new file with mode: 0644]
src/com/android/providers/downloads/DownloadService.java [new file with mode: 0644]
src/com/android/providers/downloads/DownloadThread.java [new file with mode: 0644]
src/com/android/providers/downloads/Helpers.java [new file with mode: 0644]

diff --git a/Android.mk b/Android.mk
new file mode 100644 (file)
index 0000000..82f0d58
--- /dev/null
@@ -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 (file)
index 0000000..d9873e6
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/NOTICE b/NOTICE
new file mode 100644 (file)
index 0000000..c5b1efa
--- /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 (file)
index 0000000..c0cdcbf
--- /dev/null
@@ -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 (file)
index 0000000..e4954d8
--- /dev/null
@@ -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 (file)
index 0000000..ffdaf04
--- /dev/null
@@ -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 (file)
index 0000000..7453e8a
--- /dev/null
@@ -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 (file)
index 0000000..30c4e62
--- /dev/null
@@ -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 (file)
index 0000000..c19c0bb
--- /dev/null
@@ -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 (file)
index 0000000..c34475b
--- /dev/null
@@ -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 (file)
index 0000000..657c92e
--- /dev/null
@@ -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 (file)
index 0000000..f3dd08c
--- /dev/null
@@ -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 (file)
index 0000000..29cbd94
--- /dev/null
@@ -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 (file)
index 0000000..b8cead6
--- /dev/null
@@ -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 (file)
index 0000000..38cd84f
--- /dev/null
@@ -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 (file)
index 0000000..c85c94a
--- /dev/null
@@ -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 (file)
index 0000000..e5bc4e1
--- /dev/null
@@ -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 (file)
index 0000000..0d3650c
--- /dev/null
@@ -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 (file)
index 0000000..66417b3
--- /dev/null
@@ -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 (file)
index 0000000..f966a7f
--- /dev/null
@@ -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;
+    }
+
+}