1 /* 2 * Copyright (C) 2008 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.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.Fragment; 23 import android.app.FragmentBreadCrumbs; 24 import android.app.LoaderManager.LoaderCallbacks; 25 import android.content.ClipboardManager; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.CursorLoader; 29 import android.content.DialogInterface; 30 import android.content.Intent; 31 import android.content.Loader; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ResolveInfo; 34 import android.database.Cursor; 35 import android.database.DataSetObserver; 36 import android.graphics.BitmapFactory; 37 import android.graphics.drawable.Drawable; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.provider.Browser; 41 import android.provider.BrowserContract; 42 import android.provider.BrowserContract.Combined; 43 import android.view.ContextMenu; 44 import android.view.ContextMenu.ContextMenuInfo; 45 import android.view.LayoutInflater; 46 import android.view.Menu; 47 import android.view.MenuInflater; 48 import android.view.MenuItem; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.view.ViewStub; 52 import android.widget.AbsListView; 53 import android.widget.AdapterView; 54 import android.widget.AdapterView.AdapterContextMenuInfo; 55 import android.widget.AdapterView.OnItemClickListener; 56 import android.widget.BaseAdapter; 57 import android.widget.ExpandableListView; 58 import android.widget.ExpandableListView.ExpandableListContextMenuInfo; 59 import android.widget.ExpandableListView.OnChildClickListener; 60 import android.widget.ListView; 61 import android.widget.TextView; 62 import android.widget.Toast; 63 64 /** 65 * Activity for displaying the browser's history, divided into 66 * days of viewing. 67 */ 68 public class BrowserHistoryPage extends Fragment 69 implements LoaderCallbacks<Cursor>, OnChildClickListener { 70 71 static final int LOADER_HISTORY = 1; 72 static final int LOADER_MOST_VISITED = 2; 73 74 CombinedBookmarksCallbacks mCallback; 75 HistoryAdapter mAdapter; 76 HistoryChildWrapper mChildWrapper; 77 boolean mDisableNewWindow; 78 HistoryItem mContextHeader; 79 String mMostVisitsLimit; 80 ListView mGroupList, mChildList; 81 private ViewGroup mPrefsContainer; 82 private FragmentBreadCrumbs mFragmentBreadCrumbs; 83 private ExpandableListView mHistoryList; 84 85 private View mRoot; 86 87 static interface HistoryQuery { 88 static final String[] PROJECTION = new String[] { 89 Combined._ID, // 0 90 Combined.DATE_LAST_VISITED, // 1 91 Combined.TITLE, // 2 92 Combined.URL, // 3 93 Combined.FAVICON, // 4 94 Combined.VISITS, // 5 95 Combined.IS_BOOKMARK, // 6 96 }; 97 98 static final int INDEX_ID = 0; 99 static final int INDEX_DATE_LAST_VISITED = 1; 100 static final int INDEX_TITE = 2; 101 static final int INDEX_URL = 3; 102 static final int INDEX_FAVICON = 4; 103 static final int INDEX_VISITS = 5; 104 static final int INDEX_IS_BOOKMARK = 6; 105 } 106 107 private void copy(CharSequence text) { 108 ClipboardManager cm = (ClipboardManager) getActivity().getSystemService( 109 Context.CLIPBOARD_SERVICE); 110 cm.setText(text); 111 } 112 113 @Override 114 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 115 Uri.Builder combinedBuilder = Combined.CONTENT_URI.buildUpon(); 116 117 switch (id) { 118 case LOADER_HISTORY: { 119 String sort = Combined.DATE_LAST_VISITED + " DESC"; 120 String where = Combined.VISITS + " > 0"; 121 CursorLoader loader = new CursorLoader(getActivity(), combinedBuilder.build(), 122 HistoryQuery.PROJECTION, where, null, sort); 123 return loader; 124 } 125 126 case LOADER_MOST_VISITED: { 127 Uri uri = combinedBuilder 128 .appendQueryParameter(BrowserContract.PARAM_LIMIT, mMostVisitsLimit) 129 .build(); 130 String where = Combined.VISITS + " > 0"; 131 CursorLoader loader = new CursorLoader(getActivity(), uri, 132 HistoryQuery.PROJECTION, where, null, Combined.VISITS + " DESC"); 133 return loader; 134 } 135 136 default: { 137 throw new IllegalArgumentException(); 138 } 139 } 140 } 141 142 void selectGroup(int position) { 143 mGroupItemClickListener.onItemClick(null, 144 mAdapter.getGroupView(position, false, null, null), 145 position, position); 146 } 147 148 void checkIfEmpty() { 149 if (mAdapter.mMostVisited != null && mAdapter.mHistoryCursor != null) { 150 // Both cursors have loaded - check to see if we have data 151 if (mAdapter.isEmpty()) { 152 mRoot.findViewById(R.id.history).setVisibility(View.GONE); 153 mRoot.findViewById(android.R.id.empty).setVisibility(View.VISIBLE); 154 } else { 155 mRoot.findViewById(R.id.history).setVisibility(View.VISIBLE); 156 mRoot.findViewById(android.R.id.empty).setVisibility(View.GONE); 157 } 158 } 159 } 160 161 @Override 162 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 163 switch (loader.getId()) { 164 case LOADER_HISTORY: { 165 mAdapter.changeCursor(data); 166 if (!mAdapter.isEmpty() && mGroupList != null 167 && mGroupList.getCheckedItemPosition() == ListView.INVALID_POSITION) { 168 selectGroup(0); 169 } 170 171 checkIfEmpty(); 172 break; 173 } 174 175 case LOADER_MOST_VISITED: { 176 mAdapter.changeMostVisitedCursor(data); 177 178 checkIfEmpty(); 179 break; 180 } 181 182 default: { 183 throw new IllegalArgumentException(); 184 } 185 } 186 } 187 188 @Override 189 public void onLoaderReset(Loader<Cursor> loader) { 190 } 191 192 @Override 193 public void onCreate(Bundle icicle) { 194 super.onCreate(icicle); 195 196 setHasOptionsMenu(true); 197 198 Bundle args = getArguments(); 199 mDisableNewWindow = args.getBoolean(BrowserBookmarksPage.EXTRA_DISABLE_WINDOW, false); 200 int mvlimit = getResources().getInteger(R.integer.most_visits_limit); 201 mMostVisitsLimit = Integer.toString(mvlimit); 202 mCallback = (CombinedBookmarksCallbacks) getActivity(); 203 } 204 205 @Override 206 public View onCreateView(LayoutInflater inflater, ViewGroup container, 207 Bundle savedInstanceState) { 208 mRoot = inflater.inflate(R.layout.history, container, false); 209 mAdapter = new HistoryAdapter(getActivity()); 210 ViewStub stub = (ViewStub) mRoot.findViewById(R.id.pref_stub); 211 if (stub != null) { 212 inflateTwoPane(stub); 213 } else { 214 inflateSinglePane(); 215 } 216 217 // Start the loaders 218 getLoaderManager().restartLoader(LOADER_HISTORY, null, this); 219 getLoaderManager().restartLoader(LOADER_MOST_VISITED, null, this); 220 221 return mRoot; 222 } 223 224 private void inflateSinglePane() { 225 mHistoryList = (ExpandableListView) mRoot.findViewById(R.id.history); 226 mHistoryList.setAdapter(mAdapter); 227 mHistoryList.setOnChildClickListener(this); 228 registerForContextMenu(mHistoryList); 229 } 230 231 private void inflateTwoPane(ViewStub stub) { 232 stub.setLayoutResource(R.layout.preference_list_content); 233 stub.inflate(); 234 mGroupList = (ListView) mRoot.findViewById(android.R.id.list); 235 mPrefsContainer = (ViewGroup) mRoot.findViewById(R.id.prefs_frame); 236 mFragmentBreadCrumbs = (FragmentBreadCrumbs) mRoot.findViewById(android.R.id.title); 237 mFragmentBreadCrumbs.setMaxVisible(1); 238 mFragmentBreadCrumbs.setActivity(getActivity()); 239 mPrefsContainer.setVisibility(View.VISIBLE); 240 mGroupList.setAdapter(new HistoryGroupWrapper(mAdapter)); 241 mGroupList.setOnItemClickListener(mGroupItemClickListener); 242 mGroupList.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); 243 mChildWrapper = new HistoryChildWrapper(mAdapter); 244 mChildList = new ListView(getActivity()); 245 mChildList.setAdapter(mChildWrapper); 246 mChildList.setOnItemClickListener(mChildItemClickListener); 247 registerForContextMenu(mChildList); 248 ViewGroup prefs = (ViewGroup) mRoot.findViewById(R.id.prefs); 249 prefs.addView(mChildList); 250 } 251 252 private OnItemClickListener mGroupItemClickListener = new OnItemClickListener() { 253 @Override 254 public void onItemClick( 255 AdapterView<?> parent, View view, int position, long id) { 256 CharSequence title = ((TextView) view).getText(); 257 mFragmentBreadCrumbs.setTitle(title, title); 258 mChildWrapper.setSelectedGroup(position); 259 mGroupList.setItemChecked(position, true); 260 } 261 }; 262 263 private OnItemClickListener mChildItemClickListener = new OnItemClickListener() { 264 @Override 265 public void onItemClick( 266 AdapterView<?> parent, View view, int position, long id) { 267 mCallback.openUrl(((HistoryItem) view).getUrl()); 268 } 269 }; 270 271 @Override 272 public boolean onChildClick(ExpandableListView parent, View view, 273 int groupPosition, int childPosition, long id) { 274 mCallback.openUrl(((HistoryItem) view).getUrl()); 275 return true; 276 } 277 278 @Override 279 public void onDestroy() { 280 super.onDestroy(); 281 getLoaderManager().destroyLoader(LOADER_HISTORY); 282 getLoaderManager().destroyLoader(LOADER_MOST_VISITED); 283 } 284 285 @Override 286 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 287 super.onCreateOptionsMenu(menu, inflater); 288 inflater.inflate(R.menu.history, menu); 289 } 290 291 void promptToClearHistory() { 292 final ContentResolver resolver = getActivity().getContentResolver(); 293 final ClearHistoryTask clear = new ClearHistoryTask(resolver); 294 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) 295 .setMessage(R.string.pref_privacy_clear_history_dlg) 296 .setIconAttribute(android.R.attr.alertDialogIcon) 297 .setNegativeButton(R.string.cancel, null) 298 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { 299 @Override 300 public void onClick(DialogInterface dialog, int which) { 301 if (which == DialogInterface.BUTTON_POSITIVE) { 302 clear.start(); 303 } 304 } 305 }); 306 final Dialog dialog = builder.create(); 307 dialog.show(); 308 } 309 310 @Override 311 public boolean onOptionsItemSelected(MenuItem item) { 312 if (item.getItemId() == R.id.clear_history_menu_id) { 313 promptToClearHistory(); 314 return true; 315 } 316 return super.onOptionsItemSelected(item); 317 } 318 319 static class ClearHistoryTask extends Thread { 320 ContentResolver mResolver; 321 322 public ClearHistoryTask(ContentResolver resolver) { 323 mResolver = resolver; 324 } 325 326 @Override 327 public void run() { 328 Browser.clearHistory(mResolver); 329 } 330 } 331 332 View getTargetView(ContextMenuInfo menuInfo) { 333 if (menuInfo instanceof AdapterContextMenuInfo) { 334 return ((AdapterContextMenuInfo) menuInfo).targetView; 335 } 336 if (menuInfo instanceof ExpandableListContextMenuInfo) { 337 return ((ExpandableListContextMenuInfo) menuInfo).targetView; 338 } 339 return null; 340 } 341 342 @Override 343 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 344 345 View targetView = getTargetView(menuInfo); 346 if (!(targetView instanceof HistoryItem)) { 347 return; 348 } 349 HistoryItem historyItem = (HistoryItem) targetView; 350 351 // Inflate the menu 352 Activity parent = getActivity(); 353 MenuInflater inflater = parent.getMenuInflater(); 354 inflater.inflate(R.menu.historycontext, menu); 355 356 // Setup the header 357 if (mContextHeader == null) { 358 mContextHeader = new HistoryItem(parent, false); 359 mContextHeader.setEnableScrolling(true); 360 } else if (mContextHeader.getParent() != null) { 361 ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader); 362 } 363 historyItem.copyTo(mContextHeader); 364 menu.setHeaderView(mContextHeader); 365 366 // Only show open in new tab if it was not explicitly disabled 367 if (mDisableNewWindow) { 368 menu.findItem(R.id.new_window_context_menu_id).setVisible(false); 369 } 370 // For a bookmark, provide the option to remove it from bookmarks 371 if (historyItem.isBookmark()) { 372 MenuItem item = menu.findItem(R.id.save_to_bookmarks_menu_id); 373 item.setTitle(R.string.remove_from_bookmarks); 374 } 375 // decide whether to show the share link option 376 PackageManager pm = parent.getPackageManager(); 377 Intent send = new Intent(Intent.ACTION_SEND); 378 send.setType("text/plain"); 379 ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); 380 menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null); 381 382 super.onCreateContextMenu(menu, v, menuInfo); 383 } 384 385 @Override 386 public boolean onContextItemSelected(MenuItem item) { 387 ContextMenuInfo menuInfo = item.getMenuInfo(); 388 if (menuInfo == null) { 389 return false; 390 } 391 View targetView = getTargetView(menuInfo); 392 if (!(targetView instanceof HistoryItem)) { 393 return false; 394 } 395 HistoryItem historyItem = (HistoryItem) targetView; 396 String url = historyItem.getUrl(); 397 String title = historyItem.getName(); 398 Activity activity = getActivity(); 399 switch (item.getItemId()) { 400 case R.id.open_context_menu_id: 401 mCallback.openUrl(url); 402 return true; 403 case R.id.new_window_context_menu_id: 404 mCallback.openInNewTab(url); 405 return true; 406 case R.id.save_to_bookmarks_menu_id: 407 if (historyItem.isBookmark()) { 408 Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(), 409 url, title); 410 } else { 411 Browser.saveBookmark(activity, title, url); 412 } 413 return true; 414 case R.id.share_link_context_menu_id: 415 Browser.sendString(activity, url, 416 activity.getText(R.string.choosertitle_sharevia).toString()); 417 return true; 418 case R.id.copy_url_context_menu_id: 419 copy(url); 420 return true; 421 case R.id.delete_context_menu_id: 422 Browser.deleteFromHistory(activity.getContentResolver(), url); 423 return true; 424 case R.id.homepage_context_menu_id: 425 BrowserSettings.getInstance().setHomePage(url); 426 Toast.makeText(activity, R.string.homepage_set, Toast.LENGTH_LONG).show(); 427 return true; 428 default: 429 break; 430 } 431 return super.onContextItemSelected(item); 432 } 433 434 private static abstract class HistoryWrapper extends BaseAdapter { 435 436 protected HistoryAdapter mAdapter; 437 private DataSetObserver mObserver = new DataSetObserver() { 438 @Override 439 public void onChanged() { 440 super.onChanged(); 441 notifyDataSetChanged(); 442 } 443 444 @Override 445 public void onInvalidated() { 446 super.onInvalidated(); 447 notifyDataSetInvalidated(); 448 } 449 }; 450 451 public HistoryWrapper(HistoryAdapter adapter) { 452 mAdapter = adapter; 453 mAdapter.registerDataSetObserver(mObserver); 454 } 455 456 } 457 private static class HistoryGroupWrapper extends HistoryWrapper { 458 459 public HistoryGroupWrapper(HistoryAdapter adapter) { 460 super(adapter); 461 } 462 463 @Override 464 public int getCount() { 465 return mAdapter.getGroupCount(); 466 } 467 468 @Override 469 public Object getItem(int position) { 470 return null; 471 } 472 473 @Override 474 public long getItemId(int position) { 475 return position; 476 } 477 478 @Override 479 public View getView(int position, View convertView, ViewGroup parent) { 480 return mAdapter.getGroupView(position, false, convertView, parent); 481 } 482 483 } 484 485 private static class HistoryChildWrapper extends HistoryWrapper { 486 487 private int mSelectedGroup; 488 489 public HistoryChildWrapper(HistoryAdapter adapter) { 490 super(adapter); 491 } 492 493 void setSelectedGroup(int groupPosition) { 494 mSelectedGroup = groupPosition; 495 notifyDataSetChanged(); 496 } 497 498 @Override 499 public int getCount() { 500 return mAdapter.getChildrenCount(mSelectedGroup); 501 } 502 503 @Override 504 public Object getItem(int position) { 505 return null; 506 } 507 508 @Override 509 public long getItemId(int position) { 510 return position; 511 } 512 513 @Override 514 public View getView(int position, View convertView, ViewGroup parent) { 515 return mAdapter.getChildView(mSelectedGroup, position, 516 false, convertView, parent); 517 } 518 519 } 520 521 private class HistoryAdapter extends DateSortedExpandableListAdapter { 522 523 private Cursor mMostVisited, mHistoryCursor; 524 Drawable mFaviconBackground; 525 526 HistoryAdapter(Context context) { 527 super(context, HistoryQuery.INDEX_DATE_LAST_VISITED); 528 mFaviconBackground = BookmarkUtils.createListFaviconBackground(context); 529 } 530 531 @Override 532 public void changeCursor(Cursor cursor) { 533 mHistoryCursor = cursor; 534 super.changeCursor(cursor); 535 } 536 537 void changeMostVisitedCursor(Cursor cursor) { 538 if (mMostVisited == cursor) { 539 return; 540 } 541 if (mMostVisited != null) { 542 mMostVisited.unregisterDataSetObserver(mDataSetObserver); 543 mMostVisited.close(); 544 } 545 mMostVisited = cursor; 546 if (mMostVisited != null) { 547 mMostVisited.registerDataSetObserver(mDataSetObserver); 548 } 549 notifyDataSetChanged(); 550 } 551 552 @Override 553 public long getChildId(int groupPosition, int childPosition) { 554 if (moveCursorToChildPosition(groupPosition, childPosition)) { 555 Cursor cursor = getCursor(groupPosition); 556 return cursor.getLong(HistoryQuery.INDEX_ID); 557 } 558 return 0; 559 } 560 561 @Override 562 public int getGroupCount() { 563 return super.getGroupCount() + (!isMostVisitedEmpty() ? 1 : 0); 564 } 565 566 @Override 567 public int getChildrenCount(int groupPosition) { 568 if (groupPosition >= super.getGroupCount()) { 569 if (isMostVisitedEmpty()) { 570 return 0; 571 } 572 return mMostVisited.getCount(); 573 } 574 return super.getChildrenCount(groupPosition); 575 } 576 577 @Override 578 public boolean isEmpty() { 579 if (!super.isEmpty()) { 580 return false; 581 } 582 return isMostVisitedEmpty(); 583 } 584 585 private boolean isMostVisitedEmpty() { 586 return mMostVisited == null 587 || mMostVisited.isClosed() 588 || mMostVisited.getCount() == 0; 589 } 590 591 Cursor getCursor(int groupPosition) { 592 if (groupPosition >= super.getGroupCount()) { 593 return mMostVisited; 594 } 595 return mHistoryCursor; 596 } 597 598 @Override 599 public View getGroupView(int groupPosition, boolean isExpanded, 600 View convertView, ViewGroup parent) { 601 if (groupPosition >= super.getGroupCount()) { 602 if (mMostVisited == null || mMostVisited.isClosed()) { 603 throw new IllegalStateException("Data is not valid"); 604 } 605 TextView item; 606 if (null == convertView || !(convertView instanceof TextView)) { 607 LayoutInflater factory = LayoutInflater.from(getContext()); 608 item = (TextView) factory.inflate(R.layout.history_header, null); 609 } else { 610 item = (TextView) convertView; 611 } 612 item.setText(R.string.tab_most_visited); 613 return item; 614 } 615 return super.getGroupView(groupPosition, isExpanded, convertView, parent); 616 } 617 618 @Override 619 boolean moveCursorToChildPosition( 620 int groupPosition, int childPosition) { 621 if (groupPosition >= super.getGroupCount()) { 622 if (mMostVisited != null && !mMostVisited.isClosed()) { 623 mMostVisited.moveToPosition(childPosition); 624 return true; 625 } 626 return false; 627 } 628 return super.moveCursorToChildPosition(groupPosition, childPosition); 629 } 630 631 @Override 632 public View getChildView(int groupPosition, int childPosition, boolean isLastChild, 633 View convertView, ViewGroup parent) { 634 HistoryItem item; 635 if (null == convertView || !(convertView instanceof HistoryItem)) { 636 item = new HistoryItem(getContext()); 637 // Add padding on the left so it will be indented from the 638 // arrows on the group views. 639 item.setPadding(item.getPaddingLeft() + 10, 640 item.getPaddingTop(), 641 item.getPaddingRight(), 642 item.getPaddingBottom()); 643 item.setFaviconBackground(mFaviconBackground); 644 } else { 645 item = (HistoryItem) convertView; 646 } 647 648 // Bail early if the Cursor is closed. 649 if (!moveCursorToChildPosition(groupPosition, childPosition)) { 650 return item; 651 } 652 653 Cursor cursor = getCursor(groupPosition); 654 item.setName(cursor.getString(HistoryQuery.INDEX_TITE)); 655 String url = cursor.getString(HistoryQuery.INDEX_URL); 656 item.setUrl(url); 657 byte[] data = cursor.getBlob(HistoryQuery.INDEX_FAVICON); 658 if (data != null) { 659 item.setFavicon(BitmapFactory.decodeByteArray(data, 0, 660 data.length)); 661 } else { 662 item.setFavicon(null); 663 } 664 item.setIsBookmark(cursor.getInt(HistoryQuery.INDEX_IS_BOOKMARK) == 1); 665 return item; 666 } 667 } 668 } 669