diff options
Diffstat (limited to 'ui/src/com')
5 files changed, 1116 insertions, 0 deletions
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); + } +} |