summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk7
-rw-r--r--ui/Android.mk11
-rw-r--r--ui/AndroidManifest.xml18
-rw-r--r--ui/res/anim/footer_appear.xml25
-rw-r--r--ui/res/anim/footer_disappear.xml25
-rw-r--r--ui/res/drawable-hdpi/ic_launcher_drm_file.pngbin0 -> 4501 bytes
-rw-r--r--ui/res/drawable-mdpi/ic_launcher_drm_file.pngbin0 -> 2738 bytes
-rw-r--r--ui/res/layout/download_list.xml61
-rw-r--r--ui/res/layout/download_list_item.xml81
-rw-r--r--ui/res/layout/list_group_header.xml23
-rw-r--r--ui/res/menu/download_menu.xml24
-rw-r--r--ui/res/values/dimen.xml19
-rw-r--r--ui/res/values/strings.xml78
-rw-r--r--ui/src/com/android/providers/downloads/ui/DateSortedDownloadAdapter.java59
-rw-r--r--ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java347
-rw-r--r--ui/src/com/android/providers/downloads/ui/DownloadAdapter.java185
-rw-r--r--ui/src/com/android/providers/downloads/ui/DownloadItem.java116
-rw-r--r--ui/src/com/android/providers/downloads/ui/DownloadList.java409
18 files changed, 1486 insertions, 2 deletions
diff --git a/Android.mk b/Android.mk
index c7225375..63def400 100644
--- a/Android.mk
+++ b/Android.mk
@@ -12,5 +12,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := guava
include $(BUILD_PACKAGE)
-# additionally, call tests makefiles
-include $(call all-makefiles-under,$(LOCAL_PATH))
+# build UI
+include $(call all-makefiles-under,$(LOCAL_PATH)/ui)
+
+# call tests makefiles
+include $(call all-makefiles-under,$(LOCAL_PATH)/tests)
diff --git a/ui/Android.mk b/ui/Android.mk
new file mode 100644
index 00000000..8c925f64
--- /dev/null
+++ b/ui/Android.mk
@@ -0,0 +1,11 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := DownloadProviderUi
+LOCAL_CERTIFICATE := media
+
+include $(BUILD_PACKAGE)
diff --git a/ui/AndroidManifest.xml b/ui/AndroidManifest.xml
new file mode 100644
index 00000000..71fad406
--- /dev/null
+++ b/ui/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.downloads.ui"
+ android:sharedUserId="android.media">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS" />
+
+ <application android:process="android.process.media"
+ android:label="@string/app_label">
+ <activity android:name=".DownloadList">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/ui/res/anim/footer_appear.xml b/ui/res/anim/footer_appear.xml
new file mode 100644
index 00000000..aacfd035
--- /dev/null
+++ b/ui/res/anim/footer_appear.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+** Copyright 2010, Google Inc.
+**
+** 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android" >
+ <translate
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromYDelta="+12%p"
+ android:toYDelta="0"
+ android:duration="300" />
+</set>
diff --git a/ui/res/anim/footer_disappear.xml b/ui/res/anim/footer_disappear.xml
new file mode 100644
index 00000000..d87be6ab
--- /dev/null
+++ b/ui/res/anim/footer_disappear.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+** Copyright 2010, Google Inc.
+**
+** 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android" >
+ <translate
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromYDelta="0"
+ android:toYDelta="+12%p"
+ android:duration="300" />
+</set>
diff --git a/ui/res/drawable-hdpi/ic_launcher_drm_file.png b/ui/res/drawable-hdpi/ic_launcher_drm_file.png
new file mode 100644
index 00000000..9df1c556
--- /dev/null
+++ b/ui/res/drawable-hdpi/ic_launcher_drm_file.png
Binary files differ
diff --git a/ui/res/drawable-mdpi/ic_launcher_drm_file.png b/ui/res/drawable-mdpi/ic_launcher_drm_file.png
new file mode 100644
index 00000000..57378b23
--- /dev/null
+++ b/ui/res/drawable-mdpi/ic_launcher_drm_file.png
Binary files differ
diff --git a/ui/res/layout/download_list.xml b/ui/res/layout/download_list.xml
new file mode 100644
index 00000000..241bb3d3
--- /dev/null
+++ b/ui/res/layout/download_list.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2010, 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="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <!-- The main area showing the list of downloads -->
+ <FrameLayout android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1">
+ <ExpandableListView android:id="@+id/date_ordered_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ <ListView android:id="@+id/size_ordered_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/no_downloads"
+ android:gravity="center"
+ android:textStyle="bold"/>
+ </FrameLayout>
+
+ <!-- The selection menu that pops up from the bottom of the screen -->
+ <LinearLayout android:id="@+id/selection_menu"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="5dip"
+ android:paddingLeft="4dip"
+ android:paddingRight="4dip"
+ android:paddingBottom="1dip"
+ android:gravity="center"
+ android:background="@android:drawable/bottom_bar">
+ <Button android:id="@+id/selection_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:paddingLeft="30dip"
+ android:paddingRight="30dip"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/ui/res/layout/download_list_item.xml b/ui/res/layout/download_list_item.xml
new file mode 100644
index 00000000..9c3b3dae
--- /dev/null
+++ b/ui/res/layout/download_list_item.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2010, 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.
+*/
+-->
+
+<com.android.providers.downloads.ui.DownloadItem
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingRight="?android:attr/scrollbarSize"
+ android:descendantFocusability="blocksDescendants">
+
+ <!-- Clicks are handled directly by DownloadItem -->
+ <CheckBox android:id="@+id/download_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:scaleType="fitCenter"
+ android:clickable="false"/>
+
+ <ImageView android:id="@+id/download_icon"
+ android:layout_width="@android:dimen/app_icon_size"
+ android:layout_height="@android:dimen/app_icon_size"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/download_checkbox"
+ android:scaleType="fitCenter" />
+
+ <TextView android:id="@+id/download_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/download_icon"
+ android:maxLines="1"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+ <TextView android:id="@+id/domain"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/download_title"
+ android:layout_toRightOf="@id/download_icon"
+ android:maxLines="1"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+ <TextView android:id="@+id/last_modified_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/domain"
+ android:layout_alignParentRight="true"
+ android:maxLines="1"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+ <TextView android:id="@+id/status_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/domain"
+ android:layout_toRightOf="@id/download_icon"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+ <TextView android:id="@+id/size_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/domain"
+ android:layout_toRightOf="@id/status_text"
+ android:maxLines="1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingLeft="15dp"/>
+</com.android.providers.downloads.ui.DownloadItem>
+
diff --git a/ui/res/layout/list_group_header.xml b/ui/res/layout/list_group_header.xml
new file mode 100644
index 00000000..984b4142
--- /dev/null
+++ b/ui/res/layout/list_group_header.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:paddingLeft="35dip"
+ android:gravity="center_vertical"/>
diff --git a/ui/res/menu/download_menu.xml b/ui/res/menu/download_menu.xml
new file mode 100644
index 00000000..f09f5c70
--- /dev/null
+++ b/ui/res/menu/download_menu.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/download_menu_sort_by_size"
+ android:title="@string/download_menu_sort_by_size"
+ android:icon="@android:drawable/ic_menu_sort_by_size" />
+ <item android:id="@+id/download_menu_sort_by_date"
+ android:title="@string/download_menu_sort_by_date"
+ android:icon="@android:drawable/ic_menu_day" />
+</menu>
diff --git a/ui/res/values/dimen.xml b/ui/res/values/dimen.xml
new file mode 100644
index 00000000..6e48f132
--- /dev/null
+++ b/ui/res/values/dimen.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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>
+ <dimen name="checkmark_area">40dip</dimen>
+</resources>
diff --git a/ui/res/values/strings.xml b/ui/res/values/strings.xml
new file mode 100644
index 00000000..5bebb3ca
--- /dev/null
+++ b/ui/res/values/strings.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- The name of the application that appears in the launcher [CHAR LIMIT=15] -->
+ <string name="app_label">Downloads</string>
+ <!-- The title that appears at the top of the activity listing downloads [CHAR LIMIT=25] -->
+ <string name="download_title">Downloads</string>
+
+ <!-- Appears in lieu of the list of downloads if there are no downloads to view
+ [CHAR LIMIT=200] -->
+ <string name="no_downloads">No downloads.</string>
+
+ <!-- Menu items -->
+
+ <!-- Menu option to sort the list of downloads by the size of the downloaded file
+ [CHAR LIMIT=25] -->
+ <string name="download_menu_sort_by_size">Sort by size</string>
+ <!-- Menu option to sort the list of downloads by the date/time of the last activity related to
+ the download [CHAR LIMIT=25] -->
+ <string name="download_menu_sort_by_date">Sort by date</string>
+
+ <!-- Status messages -->
+
+ <!-- Status indicating that the download has not yet begun. Appears for an individual item in
+ the download list. [CHAR LIMIT=11] -->
+ <string name="download_pending">Queued</string>
+ <!-- Status indicating that the system is currently downloading the file. Appears for an
+ individual item in the download list. [CHAR LIMIT=11] -->
+ <string name="download_running">In progress</string>
+ <!-- Status indicating that the download has completed successfully. Appears for an individual
+ item in the download list. [CHAR LIMIT=11] -->
+ <string name="download_success">Complete</string>
+ <!-- Status indicating that the download has ended without completing successfully. Appears for
+ an individual item in the download list. [CHAR LIMIT=11] -->
+ <string name="download_error">Failed</string>
+
+ <!-- Dialog/toast messages -->
+
+ <!-- Title of dialog that is shown when the user clicks a download for which no file is
+ available, either because the download hasn't started or because the download failed
+ [CHAR LIMIT=25] -->
+ <string name="dialog_title_not_available">File not available</string>
+ <!-- Text for dialog when user clicks on a download that failed [CHAR LIMIT=200] -->
+ <string name="dialog_failed_body">This download was unsuccessful. </string>
+ <!-- Text for dialog when user clicks on a download that has not yet begun, but will be started
+ in the future. [CHAR LIMIT=200] -->
+ <string name="dialog_queued_body">This file is queued for future download.</string>
+ <!-- Text for a toast appearing when a user clicks on a completed download, informing the user
+ that there is no application on the device that can open the file that was downloaded
+ [CHAR LIMIT=200] -->
+ <string name="download_no_application_title">Cannot open file</string>
+
+ <!-- Buttons -->
+
+ <!-- Text for button to remove the entry for a download that has not yet begun or that has
+ failed [CHAR LIMIT=25] -->
+ <string name="remove_download">Remove</string>
+ <!-- Text for button to delete a download that has completed, or to delete multiple download
+ entries [CHAR LIMIT=25] -->
+ <string name="delete_download">Delete</string>
+ <!-- Text for button to keep a download that has not yet begun [CHAR LIMIT=25] -->
+ <string name="keep_queued_download">Keep</string>
+ <!-- Text for button to cancel a download that is currently in progress [CHAR LIMIT=25] -->
+ <string name="cancel_running_download">Cancel</string>
+</resources>
diff --git a/ui/src/com/android/providers/downloads/ui/DateSortedDownloadAdapter.java b/ui/src/com/android/providers/downloads/ui/DateSortedDownloadAdapter.java
new file mode 100644
index 00000000..3e234008
--- /dev/null
+++ b/ui/src/com/android/providers/downloads/ui/DateSortedDownloadAdapter.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.DownloadManager;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
+
+/**
+ * Adapter for a date-sorted list of downloads. Delegates all the real work to
+ * {@link DownloadAdapter}.
+ */
+public class DateSortedDownloadAdapter extends DateSortedExpandableListAdapter {
+ private DownloadAdapter mDelegate;
+
+ public DateSortedDownloadAdapter(Context context, Cursor cursor,
+ DownloadSelectListener selectionListener) {
+ super(context, cursor,
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
+ mDelegate = new DownloadAdapter(context, cursor, selectionListener);
+ }
+
+ @Override
+ public View getChildView(int groupPosition, int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+ // The layout file uses a RelativeLayout, whereas the GroupViews use TextView.
+ if (null == convertView || !(convertView instanceof RelativeLayout)) {
+ convertView = mDelegate.newView();
+ }
+
+ // Bail early if the Cursor is closed.
+ if (!moveCursorToChildPosition(groupPosition, childPosition)) {
+ return convertView;
+ }
+
+ mDelegate.bindView(convertView);
+ return convertView;
+ }
+}
diff --git a/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java b/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
new file mode 100644
index 00000000..88ffdee3
--- /dev/null
+++ b/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.net.DownloadManager;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.DateSorter;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.TextView;
+
+import java.util.Vector;
+
+/**
+ * ExpandableListAdapter which separates data into categories based on date. Copied from
+ * packages/apps/Browser.
+ */
+public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
+ // Array for each of our bins. Each entry represents how many items are
+ // in that bin.
+ private int mItemMap[];
+ // This is our GroupCount. We will have at most DateSorter.DAY_COUNT
+ // bins, less if the user has no items in one or more bins.
+ private int mNumberOfBins;
+ private Vector<DataSetObserver> mObservers;
+ private Cursor mCursor;
+ private DateSorter mDateSorter;
+ private int mDateIndex;
+ private int mIdIndex;
+ private Context mContext;
+
+ private class ChangeObserver extends ContentObserver {
+ public ChangeObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ refreshData();
+ }
+ }
+
+ public DateSortedExpandableListAdapter(Context context, Cursor cursor,
+ int dateIndex) {
+ mContext = context;
+ mDateSorter = new DateSorter(context);
+ mObservers = new Vector<DataSetObserver>();
+ mCursor = cursor;
+ mIdIndex = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
+ cursor.registerContentObserver(new ChangeObserver());
+ mDateIndex = dateIndex;
+ buildMap();
+ }
+
+ /**
+ * Set up the bins for determining which items belong to which groups.
+ */
+ private void buildMap() {
+ // The cursor is sorted by date
+ // The ItemMap will store the number of items in each bin.
+ int array[] = new int[DateSorter.DAY_COUNT];
+ // Zero out the array.
+ for (int j = 0; j < DateSorter.DAY_COUNT; j++) {
+ array[j] = 0;
+ }
+ mNumberOfBins = 0;
+ int dateIndex = -1;
+ if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
+ while (!mCursor.isAfterLast()) {
+ long date = getLong(mDateIndex);
+ int index = mDateSorter.getIndex(date);
+ if (index > dateIndex) {
+ mNumberOfBins++;
+ if (index == DateSorter.DAY_COUNT - 1) {
+ // We are already in the last bin, so it will
+ // include all the remaining items
+ array[index] = mCursor.getCount()
+ - mCursor.getPosition();
+ break;
+ }
+ dateIndex = index;
+ }
+ array[dateIndex]++;
+ mCursor.moveToNext();
+ }
+ }
+ mItemMap = array;
+ }
+
+ /**
+ * Get the byte array at cursorIndex from the Cursor. Assumes the Cursor
+ * has already been moved to the correct position. Along with
+ * {@link #getInt} and {@link #getString}, these are provided so the client
+ * does not need to access the Cursor directly
+ * @param cursorIndex Index to query the Cursor.
+ * @return corresponding byte array from the Cursor.
+ */
+ /* package */ byte[] getBlob(int cursorIndex) {
+ return mCursor.getBlob(cursorIndex);
+ }
+
+ /* package */ Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Get the integer at cursorIndex from the Cursor. Assumes the Cursor has
+ * already been moved to the correct position. Along with
+ * {@link #getBlob} and {@link #getString}, these are provided so the client
+ * does not need to access the Cursor directly
+ * @param cursorIndex Index to query the Cursor.
+ * @return corresponding integer from the Cursor.
+ */
+ /* package */ int getInt(int cursorIndex) {
+ return mCursor.getInt(cursorIndex);
+ }
+
+ /**
+ * Get the long at cursorIndex from the Cursor. Assumes the Cursor has
+ * already been moved to the correct position.
+ */
+ /* package */ long getLong(int cursorIndex) {
+ return mCursor.getLong(cursorIndex);
+ }
+
+ /**
+ * Get the String at cursorIndex from the Cursor. Assumes the Cursor has
+ * already been moved to the correct position. Along with
+ * {@link #getInt} and {@link #getInt}, these are provided so the client
+ * does not need to access the Cursor directly
+ * @param cursorIndex Index to query the Cursor.
+ * @return corresponding String from the Cursor.
+ */
+ /* package */ String getString(int cursorIndex) {
+ return mCursor.getString(cursorIndex);
+ }
+
+ /**
+ * Determine which group an item belongs to.
+ * @param childId ID of the child view in question.
+ * @return int Group position of the containing group.
+ /* package */ int groupFromChildId(long childId) {
+ int group = -1;
+ for (mCursor.moveToFirst(); !mCursor.isAfterLast();
+ mCursor.moveToNext()) {
+ if (getLong(mIdIndex) == childId) {
+ int bin = mDateSorter.getIndex(getLong(mDateIndex));
+ // bin is the same as the group if the number of bins is the
+ // same as DateSorter
+ if (DateSorter.DAY_COUNT == mNumberOfBins) return bin;
+ // There are some empty bins. Find the corresponding group.
+ group = 0;
+ for (int i = 0; i < bin; i++) {
+ if (mItemMap[i] != 0) group++;
+ }
+ break;
+ }
+ }
+ return group;
+ }
+
+ /**
+ * Translates from a group position in the ExpandableList to a bin. This is
+ * necessary because some groups have no history items, so we do not include
+ * those in the ExpandableList.
+ * @param groupPosition Position in the ExpandableList's set of groups
+ * @return The corresponding bin that holds that group.
+ */
+ private int groupPositionToBin(int groupPosition) {
+ if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
+ throw new AssertionError("group position out of range");
+ }
+ if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) {
+ // In the first case, we have exactly the same number of bins
+ // as our maximum possible, so there is no need to do a
+ // conversion
+ // The second statement is in case this method gets called when
+ // the array is empty, in which case the provided groupPosition
+ // will do fine.
+ return groupPosition;
+ }
+ int arrayPosition = -1;
+ while (groupPosition > -1) {
+ arrayPosition++;
+ if (mItemMap[arrayPosition] != 0) {
+ groupPosition--;
+ }
+ }
+ return arrayPosition;
+ }
+
+ /**
+ * Move the cursor to the position indicated.
+ * @param packedPosition Position in packed position representation.
+ * @return True on success, false otherwise.
+ */
+ boolean moveCursorToPackedChildPosition(long packedPosition) {
+ if (ExpandableListView.getPackedPositionType(packedPosition) !=
+ ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+ return false;
+ }
+ int groupPosition = ExpandableListView.getPackedPositionGroup(
+ packedPosition);
+ int childPosition = ExpandableListView.getPackedPositionChild(
+ packedPosition);
+ return moveCursorToChildPosition(groupPosition, childPosition);
+ }
+
+ /**
+ * Move the cursor the the position indicated.
+ * @param groupPosition Index of the group containing the desired item.
+ * @param childPosition Index of the item within the specified group.
+ * @return boolean False if the cursor is closed, so the Cursor was not
+ * moved. True on success.
+ */
+ /* package */ boolean moveCursorToChildPosition(int groupPosition,
+ int childPosition) {
+ if (mCursor.isClosed()) return false;
+ groupPosition = groupPositionToBin(groupPosition);
+ int index = childPosition;
+ for (int i = 0; i < groupPosition; i++) {
+ index += mItemMap[i];
+ }
+ return mCursor.moveToPosition(index);
+ }
+
+ /* package */ void refreshData() {
+ if (mCursor.isClosed()) {
+ return;
+ }
+ mCursor.requery();
+ buildMap();
+ for (DataSetObserver o : mObservers) {
+ o.onChanged();
+ }
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+ TextView item;
+ if (null == convertView || !(convertView instanceof TextView)) {
+ LayoutInflater factory = LayoutInflater.from(mContext);
+ item = (TextView) factory.inflate(R.layout.list_group_header, null);
+ } else {
+ item = (TextView) convertView;
+ }
+ String label = mDateSorter.getLabel(groupPositionToBin(groupPosition));
+ item.setText(label);
+ return item;
+ }
+
+ public View getChildView(int groupPosition, int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+ return null;
+ }
+
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public int getGroupCount() {
+ return mNumberOfBins;
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ return mItemMap[groupPositionToBin(groupPosition)];
+ }
+
+ public Object getGroup(int groupPosition) {
+ return null;
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ return null;
+ }
+
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ if (moveCursorToChildPosition(groupPosition, childPosition)) {
+ return getLong(mIdIndex);
+ }
+ return 0;
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mObservers.add(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mObservers.remove(observer);
+ }
+
+ public void onGroupExpanded(int groupPosition) {
+ }
+
+ public void onGroupCollapsed(int groupPosition) {
+ }
+
+ public long getCombinedChildId(long groupId, long childId) {
+ return childId;
+ }
+
+ public long getCombinedGroupId(long groupId) {
+ return groupId;
+ }
+
+ public boolean isEmpty() {
+ return mCursor.isClosed() || mCursor.getCount() == 0;
+ }
+}
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadAdapter.java b/ui/src/com/android/providers/downloads/ui/DownloadAdapter.java
new file mode 100644
index 00000000..a79122a4
--- /dev/null
+++ b/ui/src/com/android/providers/downloads/ui/DownloadAdapter.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.drm.mobile1.DrmRawContent;
+import android.graphics.drawable.Drawable;
+import android.net.DownloadManager;
+import android.net.Uri;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener;
+
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * List adapter for Cursors returned by {@link DownloadManager}.
+ */
+public class DownloadAdapter extends CursorAdapter {
+ private Context mContext;
+ private Cursor mCursor;
+ private DownloadSelectListener mDownloadSelectionListener;
+ private Resources mResources;
+ private DateFormat mDateFormat;
+
+ private int mTitleColumnId;
+ private int mDescriptionColumnId;
+ private int mStatusColumnId;
+ private int mTotalBytesColumnId;
+ private int mMediaTypeColumnId;
+ private int mDateColumnId;
+ private int mIdColumnId;
+
+ public DownloadAdapter(Context context, Cursor cursor,
+ DownloadSelectListener selectionListener) {
+ super(context, cursor);
+ mContext = context;
+ mCursor = cursor;
+ mResources = mContext.getResources();
+ mDownloadSelectionListener = selectionListener;
+ mDateFormat = DateFormat.getDateInstance(DateFormat.SHORT);
+
+ mIdColumnId = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
+ mTitleColumnId = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE);
+ mDescriptionColumnId = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION);
+ mStatusColumnId = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
+ mTotalBytesColumnId = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
+ mMediaTypeColumnId = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE);
+ mDateColumnId =
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
+ }
+
+ public View newView() {
+ DownloadItem view = (DownloadItem) LayoutInflater.from(mContext)
+ .inflate(R.layout.download_list_item, null);
+ view.setSelectListener(mDownloadSelectionListener);
+ return view;
+ }
+
+ public void bindView(View convertView) {
+ if (!(convertView instanceof DownloadItem)) {
+ return;
+ }
+
+ long downloadId = mCursor.getLong(mIdColumnId);
+ ((DownloadItem) convertView).setDownloadId(downloadId);
+
+ // Retrieve the icon for this download
+ retrieveAndSetIcon(convertView);
+
+ // TODO: default text for null title?
+ setTextForView(convertView, R.id.download_title, mCursor.getString(mTitleColumnId));
+ setTextForView(convertView, R.id.domain, mCursor.getString(mDescriptionColumnId));
+ setTextForView(convertView, R.id.size_text, getSizeText());
+ setTextForView(convertView, R.id.status_text, mResources.getString(getStatusStringId()));
+ setTextForView(convertView, R.id.last_modified_date, getDateString());
+
+ CheckBox checkBox = (CheckBox) convertView.findViewById(R.id.download_checkbox);
+ checkBox.setChecked(mDownloadSelectionListener.isDownloadSelected(downloadId));
+ }
+
+ private String getDateString() {
+ Date date = new Date(mCursor.getLong(mDateColumnId));
+ return mDateFormat.format(date);
+ }
+
+ private String getSizeText() {
+ long totalBytes = mCursor.getLong(mTotalBytesColumnId);
+ String sizeText = "";
+ if (totalBytes >= 0) {
+ sizeText = Formatter.formatFileSize(mContext, totalBytes);
+ }
+ return sizeText;
+ }
+
+ private int getStatusStringId() {
+ switch (mCursor.getInt(mStatusColumnId)) {
+ case DownloadManager.STATUS_FAILED:
+ return R.string.download_error;
+
+ case DownloadManager.STATUS_SUCCESSFUL:
+ return R.string.download_success;
+
+ case DownloadManager.STATUS_PENDING:
+ return R.string.download_pending;
+
+ case DownloadManager.STATUS_RUNNING:
+ case DownloadManager.STATUS_PAUSED:
+ return R.string.download_running;
+ }
+ throw new IllegalStateException("Unknown status: " + mCursor.getInt(mStatusColumnId));
+ }
+
+ private void retrieveAndSetIcon(View convertView) {
+ String mediaType = mCursor.getString(mMediaTypeColumnId);
+ ImageView iconView = (ImageView) convertView.findViewById(R.id.download_icon);
+ iconView.setVisibility(View.INVISIBLE);
+
+ if (mediaType == null) {
+ return;
+ }
+
+ if (DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mediaType)) {
+ iconView.setImageResource(R.drawable.ic_launcher_drm_file);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.fromParts("file", "", null), mediaType);
+ PackageManager pm = mContext.getPackageManager();
+ List<ResolveInfo> list = pm.queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ if (list.size() == 0) {
+ return;
+ }
+ Drawable icon = list.get(0).activityInfo.loadIcon(pm);
+ iconView.setImageDrawable(icon);
+ }
+
+ iconView.setVisibility(View.VISIBLE);
+ }
+
+ private void setTextForView(View parent, int textViewId, String text) {
+ TextView view = (TextView) parent.findViewById(textViewId);
+ view.setText(text);
+ }
+
+ // CursorAdapter overrides
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return newView();
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ bindView(view);
+ }
+}
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadItem.java b/ui/src/com/android/providers/downloads/ui/DownloadItem.java
new file mode 100644
index 00000000..c462d596
--- /dev/null
+++ b/ui/src/com/android/providers/downloads/ui/DownloadItem.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.CheckBox;
+import android.widget.RelativeLayout;
+
+/**
+ * This class customizes RelativeLayout to directly handle clicks on the left part of the view and
+ * treat them at clicks on the checkbox. This makes rapid selection of many items easier. This class
+ * also keeps an ID associated with the currently displayed download and notifies a listener upon
+ * selection changes with that ID.
+ */
+public class DownloadItem extends RelativeLayout {
+ private static float CHECKMARK_AREA = -1;
+
+ private boolean mIsInDownEvent = false;
+ private CheckBox mCheckBox;
+ private long mDownloadId;
+ private DownloadSelectListener mListener;
+
+ static interface DownloadSelectListener {
+ public void onDownloadSelectionChanged(long downloadId, boolean isSelected);
+ public boolean isDownloadSelected(long id);
+ }
+
+ public DownloadItem(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initialize();
+ }
+
+ public DownloadItem(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ public DownloadItem(Context context) {
+ super(context);
+ initialize();
+ }
+
+ private void initialize() {
+ if (CHECKMARK_AREA == -1) {
+ CHECKMARK_AREA = getResources().getDimensionPixelSize(R.dimen.checkmark_area);
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mCheckBox = (CheckBox) findViewById(R.id.download_checkbox);
+ }
+
+ public void setDownloadId(long downloadId) {
+ mDownloadId = downloadId;
+ }
+
+ public void setSelectListener(DownloadSelectListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean handled = false;
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (event.getX() < CHECKMARK_AREA) {
+ mIsInDownEvent = true;
+ handled = true;
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mIsInDownEvent = false;
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (mIsInDownEvent && event.getX() < CHECKMARK_AREA) {
+ toggleCheckMark();
+ handled = true;
+ }
+ mIsInDownEvent = false;
+ break;
+ }
+
+ if (handled) {
+ postInvalidate();
+ } else {
+ handled = super.onTouchEvent(event);
+ }
+
+ return handled;
+ }
+
+ private void toggleCheckMark() {
+ mCheckBox.toggle();
+ mListener.onDownloadSelectionChanged(mDownloadId, mCheckBox.isChecked());
+ }
+}
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadList.java b/ui/src/com/android/providers/downloads/ui/DownloadList.java
new file mode 100644
index 00000000..1b7c727e
--- /dev/null
+++ b/ui/src/com/android/providers/downloads/ui/DownloadList.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2010 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.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.DownloadManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Downloads;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.OnChildClickListener;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * View showing a list of all downloads the Download Manager knows about.
+ */
+public class DownloadList extends Activity
+ implements OnChildClickListener, OnItemClickListener, DownloadSelectListener,
+ OnClickListener {
+ private ExpandableListView mDateOrderedListView;
+ private ListView mSizeOrderedListView;
+ private View mEmptyView;
+ private ViewGroup mSelectionMenuView;
+ private Button mSelectionDeleteButton;
+
+ private DownloadManager mDownloadManager;
+ private Cursor mDateSortedCursor;
+ private DateSortedDownloadAdapter mDateSortedAdapter;
+ private Cursor mSizeSortedCursor;
+ private DownloadAdapter mSizeSortedAdapter;
+
+ private int mStatusColumnId;
+ private int mIdColumnId;
+ private int mLocalUriColumnId;
+ private int mMediaTypeColumnId;
+
+ private boolean mIsSortedBySize = false;
+ private Set<Long> mSelectedIds = new HashSet<Long>();
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setupViews();
+
+ mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
+ mDateSortedCursor = mDownloadManager.query(new DownloadManager.Query());
+ mSizeSortedCursor = mDownloadManager.query(new DownloadManager.Query()
+ .orderBy(DownloadManager.COLUMN_TOTAL_SIZE_BYTES,
+ DownloadManager.Query.ORDER_DESCENDING));
+
+ // only attach everything to the listbox if we can access the download database. Otherwise,
+ // just show it empty
+ if (mDateSortedCursor != null && mSizeSortedCursor != null) {
+ startManagingCursor(mDateSortedCursor);
+ startManagingCursor(mSizeSortedCursor);
+
+ mStatusColumnId =
+ mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
+ mIdColumnId =
+ mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
+ mLocalUriColumnId =
+ mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
+ mMediaTypeColumnId =
+ mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE);
+
+ mDateSortedAdapter = new DateSortedDownloadAdapter(this, mDateSortedCursor, this);
+ mDateOrderedListView.setAdapter(mDateSortedAdapter);
+ mSizeSortedAdapter = new DownloadAdapter(this, mSizeSortedCursor, this);
+ mSizeOrderedListView.setAdapter(mSizeSortedAdapter);
+
+ // have the first group be open by default
+ mDateOrderedListView.post(new Runnable() {
+ public void run() {
+ if (mDateSortedAdapter.getGroupCount() > 0) {
+ mDateOrderedListView.expandGroup(0);
+ }
+ }
+ });
+ }
+
+ chooseListToShow();
+ }
+
+ private void setupViews() {
+ setContentView(R.layout.download_list);
+ setTitle(getText(R.string.download_title));
+
+ mDateOrderedListView = (ExpandableListView) findViewById(R.id.date_ordered_list);
+ mDateOrderedListView.setOnChildClickListener(this);
+ mSizeOrderedListView = (ListView) findViewById(R.id.size_ordered_list);
+ mSizeOrderedListView.setOnItemClickListener(this);
+ mEmptyView = findViewById(R.id.empty);
+
+ mSelectionMenuView = (ViewGroup) findViewById(R.id.selection_menu);
+ mSelectionDeleteButton = (Button) findViewById(R.id.selection_delete);
+ mSelectionDeleteButton.setOnClickListener(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ refresh();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean("isSortedBySize", mIsSortedBySize);
+ outState.putLongArray("selection", getSelectionAsArray());
+ }
+
+ private long[] getSelectionAsArray() {
+ long[] selectedIds = new long[mSelectedIds.size()];
+ Iterator<Long> iterator = mSelectedIds.iterator();
+ for (int i = 0; i < selectedIds.length; i++) {
+ selectedIds[i] = iterator.next();
+ }
+ return selectedIds;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mIsSortedBySize = savedInstanceState.getBoolean("isSortedBySize");
+ mSelectedIds.clear();
+ for (long selectedId : savedInstanceState.getLongArray("selection")) {
+ mSelectedIds.add(selectedId);
+ }
+ chooseListToShow();
+ showOrHideSelectionMenu();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (mDateSortedCursor != null) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.download_menu, menu);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(R.id.download_menu_sort_by_size).setVisible(!mIsSortedBySize);
+ menu.findItem(R.id.download_menu_sort_by_date).setVisible(mIsSortedBySize);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.download_menu_sort_by_size:
+ mIsSortedBySize = true;
+ chooseListToShow();
+ return true;
+ case R.id.download_menu_sort_by_date:
+ mIsSortedBySize = false;
+ chooseListToShow();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Show the correct ListView and hide the other, or hide both and show the empty view.
+ */
+ private void chooseListToShow() {
+ mDateOrderedListView.setVisibility(View.GONE);
+ mSizeOrderedListView.setVisibility(View.GONE);
+
+ if (mDateSortedCursor.getCount() == 0) {
+ mEmptyView.setVisibility(View.VISIBLE);
+ } else {
+ mEmptyView.setVisibility(View.GONE);
+ activeListView().setVisibility(View.VISIBLE);
+ activeListView().invalidateViews(); // ensure checkboxes get updated
+ }
+ }
+
+ /**
+ * @return the ListView that should currently be visible.
+ */
+ private ListView activeListView() {
+ if (mIsSortedBySize) {
+ return mSizeOrderedListView;
+ }
+ return mDateOrderedListView;
+ }
+
+ /**
+ * @return an OnClickListener to delete the given downloadId from the Download Manager
+ */
+ private DialogInterface.OnClickListener getDeleteClickHandler(final long downloadId) {
+ return new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ deleteDownload(downloadId);
+ }
+ };
+ }
+
+ /**
+ * Send an Intent to open the download currently pointed to by the given cursor.
+ */
+ private void openCurrentDownload(Cursor cursor) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ Uri fileUri = Uri.parse(cursor.getString(mLocalUriColumnId));
+ intent.setDataAndType(fileUri, cursor.getString(mMediaTypeColumnId));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void handleItemClick(Cursor cursor) {
+ long id = cursor.getInt(mIdColumnId);
+ switch (cursor.getInt(mStatusColumnId)) {
+ case DownloadManager.STATUS_PENDING:
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.dialog_title_not_available)
+ .setMessage("This file is queued for future download.")
+ .setPositiveButton(R.string.keep_queued_download, null)
+ .setNegativeButton(R.string.remove_download, getDeleteClickHandler(id))
+ .show();
+ break;
+
+ case DownloadManager.STATUS_RUNNING:
+ case DownloadManager.STATUS_PAUSED:
+ sendRunningDownloadClickedBroadcast(id);
+ break;
+
+ case DownloadManager.STATUS_SUCCESSFUL:
+ openCurrentDownload(cursor);
+ break;
+
+ case DownloadManager.STATUS_FAILED:
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.dialog_title_not_available)
+ .setMessage(getResources().getString(R.string.dialog_failed_body))
+ .setPositiveButton(R.string.remove_download, getDeleteClickHandler(id))
+ // TODO button to retry download
+ .show();
+ break;
+ }
+ }
+
+ /**
+ * TODO use constants/shared code?
+ */
+ private void sendRunningDownloadClickedBroadcast(long id) {
+ Intent intent = new Intent("android.intent.action.DOWNLOAD_LIST");
+ intent.setClassName("com.android.providers.downloads",
+ "com.android.providers.downloads.DownloadReceiver");
+ intent.setData(Uri.parse(Downloads.Impl.CONTENT_URI + "/" + id));
+ intent.putExtra("multiple", false);
+ sendBroadcast(intent);
+ }
+
+ // handle a click from the date-sorted list
+ @Override
+ public boolean onChildClick(ExpandableListView parent, View v,
+ int groupPosition, int childPosition, long id) {
+ mDateSortedAdapter.moveCursorToChildPosition(groupPosition, childPosition);
+ handleItemClick(mDateSortedCursor);
+ return true;
+ }
+
+ // handle a click from the size-sorted list
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ mSizeSortedCursor.moveToPosition(position);
+ handleItemClick(mSizeSortedCursor);
+ }
+
+ // handle a click on one of the download item checkboxes
+ @Override
+ public void onDownloadSelectionChanged(long downloadId, boolean isSelected) {
+ if (isSelected) {
+ mSelectedIds.add(downloadId);
+ } else {
+ mSelectedIds.remove(downloadId);
+ }
+ showOrHideSelectionMenu();
+ }
+
+ private void showOrHideSelectionMenu() {
+ boolean shouldBeVisible = !mSelectedIds.isEmpty();
+ boolean isVisible = mSelectionMenuView.getVisibility() == View.VISIBLE;
+ if (shouldBeVisible) {
+ updateSelectionMenu();
+ if (!isVisible) {
+ // show menu
+ mSelectionMenuView.setVisibility(View.VISIBLE);
+ mSelectionMenuView.startAnimation(
+ AnimationUtils.loadAnimation(this, R.anim.footer_appear));
+ }
+ } else if (!shouldBeVisible && isVisible) {
+ // hide menu
+ mSelectionMenuView.setVisibility(View.GONE);
+ mSelectionMenuView.startAnimation(
+ AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
+ }
+ }
+
+ /**
+ * Set up the contents of the selection menu based on the current selection.
+ */
+ private void updateSelectionMenu() {
+ int deleteButtonStringId = R.string.delete_download;
+ if (mSelectedIds.size() == 1) {
+ Cursor cursor = mDownloadManager.query(new DownloadManager.Query()
+ .setFilterById(mSelectedIds.iterator().next()));
+ try {
+ cursor.moveToFirst();
+ switch (cursor.getInt(mStatusColumnId)) {
+ case DownloadManager.STATUS_FAILED:
+ case DownloadManager.STATUS_PENDING:
+ deleteButtonStringId = R.string.remove_download;
+ break;
+
+ case DownloadManager.STATUS_PAUSED:
+ case DownloadManager.STATUS_RUNNING:
+ deleteButtonStringId = R.string.cancel_running_download;
+ break;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ mSelectionDeleteButton.setText(deleteButtonStringId);
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.selection_delete:
+ for (Long downloadId : mSelectedIds) {
+ deleteDownload(downloadId);
+ }
+ clearSelection();
+ return;
+ }
+ }
+
+ /**
+ * Requery the database and update the UI.
+ */
+ private void refresh() {
+ mDateSortedCursor.requery();
+ mSizeSortedCursor.requery();
+ // Adapters get notification of changes and update automatically
+ }
+
+ private void clearSelection() {
+ mSelectedIds.clear();
+ showOrHideSelectionMenu();
+ }
+
+ /**
+ * Delete a download from the Download Manager.
+ */
+ private void deleteDownload(Long downloadId) {
+ mDownloadManager.remove(downloadId);
+ }
+
+ @Override
+ public boolean isDownloadSelected(long id) {
+ return mSelectedIds.contains(id);
+ }
+}