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