Home | History | Annotate | Download | only in browser
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.browser;
     18 
     19 import android.app.AlertDialog;
     20 import android.app.ExpandableListActivity;
     21 import android.content.ActivityNotFoundException;
     22 import android.content.ContentValues;
     23 import android.content.DialogInterface;
     24 import android.content.Intent;
     25 import android.content.ContentUris;
     26 import android.content.pm.PackageManager;
     27 import android.content.pm.ResolveInfo;
     28 import android.database.ContentObserver;
     29 import android.database.Cursor;
     30 import android.net.Uri;
     31 import android.os.Bundle;
     32 import android.os.Handler;
     33 import android.provider.Downloads;
     34 import android.util.Log;
     35 import android.view.ContextMenu;
     36 import android.view.ContextMenu.ContextMenuInfo;
     37 import android.view.LayoutInflater;
     38 import android.view.Menu;
     39 import android.view.MenuItem;
     40 import android.view.MenuInflater;
     41 import android.view.View;
     42 import android.view.ViewGroup.LayoutParams;
     43 import android.widget.AdapterView;
     44 import android.widget.ExpandableListView;
     45 
     46 import java.io.File;
     47 import java.util.List;
     48 
     49 /**
     50  *  View showing the user's current browser downloads
     51  */
     52 public class BrowserDownloadPage extends ExpandableListActivity {
     53     private ExpandableListView      mListView;
     54     private Cursor                  mDownloadCursor;
     55     private BrowserDownloadAdapter  mDownloadAdapter;
     56     private int                     mStatusColumnId;
     57     private int                     mIdColumnId;
     58     private int                     mTitleColumnId;
     59     private long                    mContextMenuPosition;
     60     // Used to update the ContextMenu if an item is being downloaded and the
     61     // user opens the ContextMenu.
     62     private ContentObserver         mContentObserver;
     63     // Only meaningful while a ContentObserver is registered.  The ContextMenu
     64     // will be reopened on this View.
     65     private View                    mSelectedView;
     66 
     67     private final static String LOGTAG = "BrowserDownloadPage";
     68     @Override
     69     public void onCreate(Bundle icicle) {
     70         super.onCreate(icicle);
     71         setContentView(R.layout.browser_downloads_page);
     72 
     73         setTitle(getText(R.string.download_title));
     74 
     75         mListView = (ExpandableListView) findViewById(android.R.id.list);
     76         mListView.setEmptyView(findViewById(R.id.empty));
     77         mDownloadCursor = managedQuery(Downloads.Impl.CONTENT_URI,
     78                 new String [] {Downloads.Impl._ID, Downloads.Impl.COLUMN_TITLE,
     79                 Downloads.Impl.COLUMN_STATUS, Downloads.Impl.COLUMN_TOTAL_BYTES,
     80                 Downloads.Impl.COLUMN_CURRENT_BYTES,
     81                 Downloads.Impl.COLUMN_DESCRIPTION,
     82                 Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
     83                 Downloads.Impl.COLUMN_LAST_MODIFICATION,
     84                 Downloads.Impl.COLUMN_VISIBILITY,
     85                 Downloads.Impl._DATA,
     86                 Downloads.Impl.COLUMN_MIME_TYPE},
     87                 null, Downloads.Impl.COLUMN_LAST_MODIFICATION + " DESC");
     88 
     89         // only attach everything to the listbox if we can access
     90         // the download database. Otherwise, just show it empty
     91         if (mDownloadCursor != null) {
     92             mStatusColumnId =
     93                     mDownloadCursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
     94             mIdColumnId =
     95                     mDownloadCursor.getColumnIndexOrThrow(Downloads.Impl._ID);
     96             mTitleColumnId =
     97                     mDownloadCursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TITLE);
     98 
     99             // Create a list "controller" for the data
    100             mDownloadAdapter = new BrowserDownloadAdapter(this,
    101                     mDownloadCursor, mDownloadCursor.getColumnIndexOrThrow(
    102                     Downloads.Impl.COLUMN_LAST_MODIFICATION));
    103 
    104             setListAdapter(mDownloadAdapter);
    105             mListView.setOnCreateContextMenuListener(this);
    106 
    107             Intent intent = getIntent();
    108             final int groupToShow = intent == null || intent.getData() == null
    109                     ? 0 : checkStatus(ContentUris.parseId(intent.getData()));
    110             if (mDownloadAdapter.getGroupCount() > groupToShow) {
    111                 mListView.post(new Runnable() {
    112                     public void run() {
    113                         if (mDownloadAdapter.getGroupCount() > groupToShow) {
    114                             mListView.expandGroup(groupToShow);
    115                         }
    116                     }
    117                 });
    118             }
    119         }
    120     }
    121 
    122     @Override
    123     protected void onResume() {
    124         super.onResume();
    125         if (mDownloadCursor != null) {
    126             String where = null;
    127             for (mDownloadCursor.moveToFirst(); !mDownloadCursor.isAfterLast();
    128                     mDownloadCursor.moveToNext()) {
    129                 if (!Downloads.Impl.isStatusCompleted(
    130                         mDownloadCursor.getInt(mStatusColumnId))) {
    131                     // Only want to check files that have completed.
    132                     continue;
    133                 }
    134                 int filenameColumnId = mDownloadCursor.getColumnIndexOrThrow(
    135                         Downloads.Impl._DATA);
    136                 String filename = mDownloadCursor.getString(filenameColumnId);
    137                 if (filename != null) {
    138                     File file = new File(filename);
    139                     if (!file.exists()) {
    140                         long id = mDownloadCursor.getLong(mIdColumnId);
    141                         if (where == null) {
    142                             where = Downloads.Impl._ID + " = '" + id + "'";
    143                         } else {
    144                             where += " OR " + Downloads.Impl._ID + " = '" + id
    145                                     + "'";
    146                         }
    147                     }
    148                 }
    149             }
    150             if (where != null) {
    151                 getContentResolver().delete(Downloads.Impl.CONTENT_URI, where,
    152                         null);
    153             }
    154         }
    155     }
    156 
    157     @Override
    158     public boolean onCreateOptionsMenu(Menu menu) {
    159         if (mDownloadCursor != null) {
    160             MenuInflater inflater = getMenuInflater();
    161             inflater.inflate(R.menu.downloadhistory, menu);
    162         }
    163         return true;
    164     }
    165 
    166     @Override
    167     public boolean onPrepareOptionsMenu(Menu menu) {
    168         boolean showCancel = getCancelableCount() > 0;
    169         menu.findItem(R.id.download_menu_cancel_all).setEnabled(showCancel);
    170         return super.onPrepareOptionsMenu(menu);
    171     }
    172 
    173     @Override
    174     public boolean onOptionsItemSelected(MenuItem item) {
    175         switch (item.getItemId()) {
    176             case R.id.download_menu_cancel_all:
    177                 promptCancelAll();
    178                 return true;
    179         }
    180         return false;
    181     }
    182 
    183     /**
    184      * Remove the file from the list of downloads.
    185      * @param id Unique ID of the download to remove.
    186      */
    187     private void clearFromDownloads(long id) {
    188         getContentResolver().delete(ContentUris.withAppendedId(
    189                 Downloads.Impl.CONTENT_URI, id), null, null);
    190     }
    191 
    192     @Override
    193     public boolean onContextItemSelected(MenuItem item) {
    194         if (!mDownloadAdapter.moveCursorToPackedChildPosition(
    195                 mContextMenuPosition)) {
    196             return false;
    197         }
    198         switch (item.getItemId()) {
    199             case R.id.download_menu_open:
    200                 hideCompletedDownload();
    201                 openOrDeleteCurrentDownload(false);
    202                 return true;
    203 
    204             case R.id.download_menu_delete:
    205                 new AlertDialog.Builder(this)
    206                         .setTitle(R.string.download_delete_file)
    207                         .setIcon(android.R.drawable.ic_dialog_alert)
    208                         .setMessage(mDownloadCursor.getString(mTitleColumnId))
    209                         .setNegativeButton(R.string.cancel, null)
    210                         .setPositiveButton(R.string.ok,
    211                                 new DialogInterface.OnClickListener() {
    212                                     public void onClick(DialogInterface dialog,
    213                                             int whichButton) {
    214                                         openOrDeleteCurrentDownload(true);
    215                                     }
    216                                 })
    217                         .show();
    218                 break;
    219 
    220             case R.id.download_menu_clear:
    221             case R.id.download_menu_cancel:
    222                 clearFromDownloads(mDownloadCursor.getLong(mIdColumnId));
    223                 return true;
    224         }
    225         return false;
    226     }
    227 
    228     @Override
    229     protected void onPause() {
    230         super.onPause();
    231         if (mContentObserver != null) {
    232             getContentResolver().unregisterContentObserver(mContentObserver);
    233             // Note that we do not need to undo this in onResume, because the
    234             // ContextMenu does not get reinvoked when the Activity resumes.
    235         }
    236     }
    237 
    238     /*
    239      * ContentObserver to update the ContextMenu if it is open when the
    240      * corresponding download completes.
    241      */
    242     private class ChangeObserver extends ContentObserver {
    243         private final Uri mTrack;
    244         public ChangeObserver(Uri track) {
    245             super(new Handler());
    246             mTrack = track;
    247         }
    248 
    249         @Override
    250         public boolean deliverSelfNotifications() {
    251             return false;
    252         }
    253 
    254         @Override
    255         public void onChange(boolean selfChange) {
    256             Cursor cursor = null;
    257             try {
    258                 cursor = getContentResolver().query(mTrack,
    259                         new String[] { Downloads.Impl.COLUMN_STATUS }, null, null,
    260                         null);
    261                 if (cursor.moveToFirst() && Downloads.Impl.isStatusSuccess(
    262                         cursor.getInt(0))) {
    263                     // Do this right away, so we get no more updates.
    264                     getContentResolver().unregisterContentObserver(
    265                             mContentObserver);
    266                     // Post a runnable in case this ContentObserver gets notified
    267                     // before the one that updates the ListView.
    268                     mListView.post(new Runnable() {
    269                         public void run() {
    270                             // Close the context menu, reopen with up to date data.
    271                             closeContextMenu();
    272                             openContextMenu(mSelectedView);
    273                         }
    274                     });
    275                 }
    276             } catch (IllegalStateException e) {
    277                 Log.e(LOGTAG, "onChange", e);
    278             } finally {
    279                 if (cursor != null) cursor.close();
    280             }
    281         }
    282     }
    283 
    284     @Override
    285     public void onCreateContextMenu(ContextMenu menu, View v,
    286             ContextMenuInfo menuInfo) {
    287         if (mDownloadCursor != null) {
    288             ExpandableListView.ExpandableListContextMenuInfo info
    289                     = (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;
    290             long packedPosition = info.packedPosition;
    291             // Only show a context menu for the child views
    292             if (!mDownloadAdapter.moveCursorToPackedChildPosition(
    293                     packedPosition)) {
    294                 return;
    295             }
    296             mContextMenuPosition = packedPosition;
    297             menu.setHeaderTitle(mDownloadCursor.getString(mTitleColumnId));
    298 
    299             MenuInflater inflater = getMenuInflater();
    300             int status = mDownloadCursor.getInt(mStatusColumnId);
    301             if (Downloads.Impl.isStatusSuccess(status)) {
    302                 inflater.inflate(R.menu.downloadhistorycontextfinished, menu);
    303             } else if (Downloads.Impl.isStatusError(status)) {
    304                 inflater.inflate(R.menu.downloadhistorycontextfailed, menu);
    305             } else {
    306                 // In this case, the download is in progress.  Set a
    307                 // ContentObserver so that we can know when it completes,
    308                 // and if it does, we can then update the context menu
    309                 Uri track = ContentUris.withAppendedId(
    310                         Downloads.Impl.CONTENT_URI,
    311                         mDownloadCursor.getLong(mIdColumnId));
    312                 if (mContentObserver != null) {
    313                     getContentResolver().unregisterContentObserver(
    314                             mContentObserver);
    315                 }
    316                 mContentObserver = new ChangeObserver(track);
    317                 mSelectedView = v;
    318                 getContentResolver().registerContentObserver(track, false,
    319                         mContentObserver);
    320                 inflater.inflate(R.menu.downloadhistorycontextrunning, menu);
    321             }
    322         }
    323         super.onCreateContextMenu(menu, v, menuInfo);
    324     }
    325 
    326     /**
    327      * This function is called to check the status of the download and if it
    328      * has an error show an error dialog.
    329      * @param id Row id of the download to check
    330      * @return Group which contains the download
    331      */
    332     private int checkStatus(final long id) {
    333         int groupToShow = mDownloadAdapter.groupFromChildId(id);
    334         if (-1 == groupToShow) return 0;
    335         int status = mDownloadCursor.getInt(mStatusColumnId);
    336         if (!Downloads.Impl.isStatusError(status)) {
    337             return groupToShow;
    338         }
    339         if (status == Downloads.Impl.STATUS_FILE_ERROR) {
    340             String title = mDownloadCursor.getString(mTitleColumnId);
    341             if (title == null || title.length() == 0) {
    342                 title = getString(R.string.download_unknown_filename);
    343             }
    344             String msg = getString(R.string.download_file_error_dlg_msg, title);
    345             new AlertDialog.Builder(this)
    346                     .setTitle(R.string.download_file_error_dlg_title)
    347                     .setIcon(android.R.drawable.ic_popup_disk_full)
    348                     .setMessage(msg)
    349                     .setPositiveButton(R.string.ok, null)
    350                     .setNegativeButton(R.string.retry,
    351                             new DialogInterface.OnClickListener() {
    352                                 public void onClick(DialogInterface dialog,
    353                                         int whichButton) {
    354                                     resumeDownload(id);
    355                                 }
    356                             })
    357                     .show();
    358         } else {
    359             new AlertDialog.Builder(this)
    360                     .setTitle(R.string.download_failed_generic_dlg_title)
    361                     .setIcon(R.drawable.ssl_icon)
    362                     .setMessage(BrowserDownloadAdapter.getErrorText(status))
    363                     .setPositiveButton(R.string.ok, null)
    364                     .show();
    365         }
    366         return groupToShow;
    367     }
    368 
    369     /**
    370      * Resume a given download
    371      * @param id Row id of the download to resume
    372      */
    373     private void resumeDownload(final long id) {
    374         // the relevant functionality doesn't exist in the download manager
    375     }
    376 
    377     /**
    378      * Return the number of items in the list that can be canceled.
    379      * @return count
    380      */
    381     private int getCancelableCount() {
    382         // Count the number of items that will be canceled.
    383         int count = 0;
    384         if (mDownloadCursor != null) {
    385             for (mDownloadCursor.moveToFirst(); !mDownloadCursor.isAfterLast();
    386                     mDownloadCursor.moveToNext()) {
    387                 int status = mDownloadCursor.getInt(mStatusColumnId);
    388                 if (!Downloads.Impl.isStatusCompleted(status)) {
    389                     count++;
    390                 }
    391             }
    392         }
    393 
    394         return count;
    395     }
    396 
    397     /**
    398      * Prompt the user if they would like to clear the download history
    399      */
    400     private void promptCancelAll() {
    401         int count = getCancelableCount();
    402 
    403         // If there is nothing to do, just return
    404         if (count == 0) {
    405             return;
    406         }
    407 
    408         // Don't show the dialog if there is only one download
    409         if (count == 1) {
    410             cancelAllDownloads();
    411             return;
    412         }
    413         String msg =
    414             getString(R.string.download_cancel_dlg_msg, count);
    415         new AlertDialog.Builder(this)
    416                 .setTitle(R.string.download_cancel_dlg_title)
    417                 .setIcon(R.drawable.ssl_icon)
    418                 .setMessage(msg)
    419                 .setPositiveButton(R.string.ok,
    420                         new DialogInterface.OnClickListener() {
    421                             public void onClick(DialogInterface dialog,
    422                                     int whichButton) {
    423                                 cancelAllDownloads();
    424                             }
    425                         })
    426                  .setNegativeButton(R.string.cancel, null)
    427                  .show();
    428     }
    429 
    430     /**
    431      * Cancel all downloads. As canceled downloads are not
    432      * listed, we removed them from the db. Removing a download
    433      * record, cancels the download.
    434      */
    435     private void cancelAllDownloads() {
    436         if (mDownloadCursor.moveToFirst()) {
    437             StringBuilder where = new StringBuilder();
    438             boolean firstTime = true;
    439             while (!mDownloadCursor.isAfterLast()) {
    440                 int status = mDownloadCursor.getInt(mStatusColumnId);
    441                 if (!Downloads.Impl.isStatusCompleted(status)) {
    442                     if (firstTime) {
    443                         firstTime = false;
    444                     } else {
    445                         where.append(" OR ");
    446                     }
    447                     where.append("( ");
    448                     where.append(Downloads.Impl._ID);
    449                     where.append(" = '");
    450                     where.append(mDownloadCursor.getLong(mIdColumnId));
    451                     where.append("' )");
    452                 }
    453                 mDownloadCursor.moveToNext();
    454             }
    455             if (!firstTime) {
    456                 getContentResolver().delete(Downloads.Impl.CONTENT_URI,
    457                         where.toString(), null);
    458             }
    459         }
    460     }
    461 
    462     private int getClearableCount() {
    463         int count = 0;
    464         if (mDownloadCursor.moveToFirst()) {
    465             while (!mDownloadCursor.isAfterLast()) {
    466                 int status = mDownloadCursor.getInt(mStatusColumnId);
    467                 if (Downloads.Impl.isStatusCompleted(status)) {
    468                     count++;
    469                 }
    470                 mDownloadCursor.moveToNext();
    471             }
    472         }
    473         return count;
    474     }
    475 
    476     /**
    477      * Open or delete content where the download db cursor currently is.  Sends
    478      * an Intent to perform the action.
    479      * @param delete If true, delete the content.  Otherwise open it.
    480      */
    481     private void openOrDeleteCurrentDownload(boolean delete) {
    482         int packageColumnId = mDownloadCursor.getColumnIndexOrThrow(
    483                 Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
    484         String packageName = mDownloadCursor.getString(packageColumnId);
    485         Intent intent = new Intent(delete ? Intent.ACTION_DELETE
    486                 : Downloads.Impl.ACTION_NOTIFICATION_CLICKED);
    487         Uri contentUri = ContentUris.withAppendedId(
    488                 Downloads.Impl.CONTENT_URI,
    489                 mDownloadCursor.getLong(mIdColumnId));
    490         intent.setData(contentUri);
    491         intent.setPackage(packageName);
    492         sendBroadcast(intent);
    493     }
    494 
    495     @Override
    496     public boolean onChildClick(ExpandableListView parent, View v,
    497             int groupPosition, int childPosition, long id) {
    498         // Open the selected item
    499         mDownloadAdapter.moveCursorToChildPosition(groupPosition,
    500                 childPosition);
    501 
    502         hideCompletedDownload();
    503 
    504         int status = mDownloadCursor.getInt(mStatusColumnId);
    505         if (Downloads.Impl.isStatusSuccess(status)) {
    506             // Open it if it downloaded successfully
    507             openOrDeleteCurrentDownload(false);
    508         } else {
    509             // Check to see if there is an error.
    510             checkStatus(id);
    511         }
    512         return true;
    513     }
    514 
    515     /**
    516      * hides the notification for the download pointed by mDownloadCursor
    517      * if the download has completed.
    518      */
    519     private void hideCompletedDownload() {
    520         int status = mDownloadCursor.getInt(mStatusColumnId);
    521 
    522         int visibilityColumn = mDownloadCursor.getColumnIndexOrThrow(
    523                 Downloads.Impl.COLUMN_VISIBILITY);
    524         int visibility = mDownloadCursor.getInt(visibilityColumn);
    525 
    526         if (Downloads.Impl.isStatusCompleted(status) &&
    527                 visibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
    528             ContentValues values = new ContentValues();
    529             values.put(Downloads.Impl.COLUMN_VISIBILITY, Downloads.Impl.VISIBILITY_VISIBLE);
    530             getContentResolver().update(
    531                     ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI,
    532                     mDownloadCursor.getLong(mIdColumnId)), values, null, null);
    533         }
    534     }
    535 }
    536