BrowserHistoryPage.java revision 22807d1975984667829138d7d47d2020f8632f11
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 17package com.android.browser; 18 19import android.app.Activity; 20import android.app.ExpandableListActivity; 21import android.content.Intent; 22import android.content.pm.PackageManager; 23import android.content.pm.ResolveInfo; 24import android.database.ContentObserver; 25import android.database.Cursor; 26import android.database.DataSetObserver; 27import android.graphics.Bitmap; 28import android.graphics.BitmapFactory; 29import android.os.Bundle; 30import android.os.Handler; 31import android.os.ServiceManager; 32import android.provider.Browser; 33import android.text.IClipboard; 34import android.util.Log; 35import android.view.ContextMenu; 36import android.view.KeyEvent; 37import android.view.LayoutInflater; 38import android.view.Menu; 39import android.view.MenuInflater; 40import android.view.MenuItem; 41import android.view.View; 42import android.view.ViewGroup; 43import android.view.ViewGroup.LayoutParams; 44import android.view.ContextMenu.ContextMenuInfo; 45import android.view.ViewStub; 46import android.webkit.DateSorter; 47import android.webkit.WebIconDatabase.IconListener; 48import android.widget.AdapterView; 49import android.widget.ExpandableListAdapter; 50import android.widget.ExpandableListView; 51import android.widget.ExpandableListView.ExpandableListContextMenuInfo; 52import android.widget.TextView; 53import android.widget.Toast; 54 55import java.util.List; 56import java.util.Vector; 57 58/** 59 * Activity for displaying the browser's history, divided into 60 * days of viewing. 61 */ 62public class BrowserHistoryPage extends ExpandableListActivity { 63 private HistoryAdapter mAdapter; 64 private DateSorter mDateSorter; 65 private boolean mDisableNewWindow; 66 private HistoryItem mContextHeader; 67 68 private final static String LOGTAG = "browser"; 69 70 // Implementation of WebIconDatabase.IconListener 71 private class IconReceiver implements IconListener { 72 public void onReceivedIcon(String url, Bitmap icon) { 73 setListAdapter(mAdapter); 74 } 75 } 76 // Instance of IconReceiver 77 private final IconReceiver mIconReceiver = new IconReceiver(); 78 79 /** 80 * Report back to the calling activity to load a site. 81 * @param url Site to load. 82 * @param newWindow True if the URL should be loaded in a new window 83 */ 84 private void loadUrl(String url, boolean newWindow) { 85 Intent intent = new Intent().setAction(url); 86 if (newWindow) { 87 Bundle b = new Bundle(); 88 b.putBoolean("new_window", true); 89 intent.putExtras(b); 90 } 91 setResultToParent(RESULT_OK, intent); 92 finish(); 93 } 94 95 private void copy(CharSequence text) { 96 try { 97 IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); 98 if (clip != null) { 99 clip.setClipboardText(text); 100 } 101 } catch (android.os.RemoteException e) { 102 Log.e(LOGTAG, "Copy failed", e); 103 } 104 } 105 106 @Override 107 protected void onCreate(Bundle icicle) { 108 super.onCreate(icicle); 109 setTitle(R.string.browser_history); 110 111 mDateSorter = new DateSorter(this); 112 113 mAdapter = new HistoryAdapter(); 114 setListAdapter(mAdapter); 115 final ExpandableListView list = getExpandableListView(); 116 list.setOnCreateContextMenuListener(this); 117 View v = new ViewStub(this, R.layout.empty_history); 118 addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT, 119 LayoutParams.FILL_PARENT)); 120 list.setEmptyView(v); 121 // Do not post the runnable if there is nothing in the list. 122 if (list.getExpandableListAdapter().getGroupCount() > 0) { 123 list.post(new Runnable() { 124 public void run() { 125 // In case the history gets cleared before this event 126 // happens. 127 if (list.getExpandableListAdapter().getGroupCount() > 0) { 128 list.expandGroup(0); 129 } 130 } 131 }); 132 } 133 mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window", 134 false); 135 CombinedBookmarkHistoryActivity.getIconListenerSet() 136 .addListener(mIconReceiver); 137 138 // initialize the result to canceled, so that if the user just presses 139 // back then it will have the correct result 140 setResultToParent(RESULT_CANCELED, null); 141 } 142 143 @Override 144 protected void onDestroy() { 145 super.onDestroy(); 146 CombinedBookmarkHistoryActivity.getIconListenerSet() 147 .removeListener(mIconReceiver); 148 } 149 150 @Override 151 public boolean onCreateOptionsMenu(Menu menu) { 152 super.onCreateOptionsMenu(menu); 153 MenuInflater inflater = getMenuInflater(); 154 inflater.inflate(R.menu.history, menu); 155 return true; 156 } 157 158 @Override 159 public boolean onPrepareOptionsMenu(Menu menu) { 160 menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver())); 161 return true; 162 } 163 164 @Override 165 public boolean onOptionsItemSelected(MenuItem item) { 166 switch (item.getItemId()) { 167 case R.id.clear_history_menu_id: 168 // FIXME: Need to clear the tab control in browserActivity 169 // as well 170 Browser.clearHistory(getContentResolver()); 171 mAdapter.refreshData(); 172 return true; 173 174 default: 175 break; 176 } 177 return super.onOptionsItemSelected(item); 178 } 179 180 @Override 181 public void onCreateContextMenu(ContextMenu menu, View v, 182 ContextMenuInfo menuInfo) { 183 ExpandableListContextMenuInfo i = 184 (ExpandableListContextMenuInfo) menuInfo; 185 // Do not allow a context menu to come up from the group views. 186 if (!(i.targetView instanceof HistoryItem)) { 187 return; 188 } 189 190 // Inflate the menu 191 MenuInflater inflater = getMenuInflater(); 192 inflater.inflate(R.menu.historycontext, menu); 193 194 HistoryItem historyItem = (HistoryItem) i.targetView; 195 196 // Setup the header 197 if (mContextHeader == null) { 198 mContextHeader = new HistoryItem(this); 199 } else if (mContextHeader.getParent() != null) { 200 ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader); 201 } 202 historyItem.copyTo(mContextHeader); 203 menu.setHeaderView(mContextHeader); 204 205 // Only show open in new tab if it was not explicitly disabled 206 if (mDisableNewWindow) { 207 menu.findItem(R.id.new_window_context_menu_id).setVisible(false); 208 } 209 // For a bookmark, provide the option to remove it from bookmarks 210 if (historyItem.isBookmark()) { 211 MenuItem item = menu.findItem(R.id.save_to_bookmarks_menu_id); 212 item.setTitle(R.string.remove_from_bookmarks); 213 } 214 // decide whether to show the share link option 215 PackageManager pm = getPackageManager(); 216 Intent send = new Intent(Intent.ACTION_SEND); 217 send.setType("text/plain"); 218 ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); 219 menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null); 220 221 super.onCreateContextMenu(menu, v, menuInfo); 222 } 223 224 @Override 225 public boolean onContextItemSelected(MenuItem item) { 226 ExpandableListContextMenuInfo i = 227 (ExpandableListContextMenuInfo) item.getMenuInfo(); 228 HistoryItem historyItem = (HistoryItem) i.targetView; 229 String url = historyItem.getUrl(); 230 String title = historyItem.getName(); 231 switch (item.getItemId()) { 232 case R.id.open_context_menu_id: 233 loadUrl(url, false); 234 return true; 235 case R.id.new_window_context_menu_id: 236 loadUrl(url, true); 237 return true; 238 case R.id.save_to_bookmarks_menu_id: 239 if (historyItem.isBookmark()) { 240 Bookmarks.removeFromBookmarks(this, getContentResolver(), 241 url, title); 242 } else { 243 Browser.saveBookmark(this, title, url); 244 } 245 return true; 246 case R.id.share_link_context_menu_id: 247 Browser.sendString(this, url, 248 getText(R.string.choosertitle_sharevia).toString()); 249 return true; 250 case R.id.copy_url_context_menu_id: 251 copy(url); 252 return true; 253 case R.id.delete_context_menu_id: 254 Browser.deleteFromHistory(getContentResolver(), url); 255 mAdapter.refreshData(); 256 return true; 257 case R.id.homepage_context_menu_id: 258 BrowserSettings.getInstance().setHomePage(this, url); 259 Toast.makeText(this, R.string.homepage_set, 260 Toast.LENGTH_LONG).show(); 261 return true; 262 default: 263 break; 264 } 265 return super.onContextItemSelected(item); 266 } 267 268 @Override 269 public boolean onChildClick(ExpandableListView parent, View v, 270 int groupPosition, int childPosition, long id) { 271 if (v instanceof HistoryItem) { 272 loadUrl(((HistoryItem) v).getUrl(), false); 273 return true; 274 } 275 return false; 276 } 277 278 // This Activity is generally a sub-Activity of CombinedHistoryActivity. In 279 // that situation, we need to pass our result code up to our parent. 280 // However, if someone calls this Activity directly, then this has no 281 // parent, and it needs to set it on itself. 282 private void setResultToParent(int resultCode, Intent data) { 283 Activity a = getParent() == null ? this : getParent(); 284 a.setResult(resultCode, data); 285 } 286 287 private class ChangeObserver extends ContentObserver { 288 public ChangeObserver() { 289 super(new Handler()); 290 } 291 292 @Override 293 public boolean deliverSelfNotifications() { 294 return true; 295 } 296 297 @Override 298 public void onChange(boolean selfChange) { 299 mAdapter.refreshData(); 300 } 301 } 302 303 private class HistoryAdapter implements ExpandableListAdapter { 304 305 // Array for each of our bins. Each entry represents how many items are 306 // in that bin. 307 private int mItemMap[]; 308 // This is our GroupCount. We will have at most DateSorter.DAY_COUNT 309 // bins, less if the user has no items in one or more bins. 310 private int mNumberOfBins; 311 private Vector<DataSetObserver> mObservers; 312 private Cursor mCursor; 313 314 HistoryAdapter() { 315 mObservers = new Vector<DataSetObserver>(); 316 317 final String whereClause = Browser.BookmarkColumns.VISITS + " > 0" 318 // In AddBookmarkPage, where we save new bookmarks, we add 319 // three visits to newly created bookmarks, so that 320 // bookmarks that have not been visited will show up in the 321 // most visited, and higher in the goto search box. 322 // However, this puts the site in the history, unless we 323 // ignore sites with a DATE of 0, which the next line does. 324 + " AND " + Browser.BookmarkColumns.DATE + " > 0"; 325 final String orderBy = Browser.BookmarkColumns.DATE + " DESC"; 326 327 mCursor = managedQuery( 328 Browser.BOOKMARKS_URI, 329 Browser.HISTORY_PROJECTION, 330 whereClause, null, orderBy); 331 332 buildMap(); 333 mCursor.registerContentObserver(new ChangeObserver()); 334 } 335 336 void refreshData() { 337 if (mCursor.isClosed()) { 338 return; 339 } 340 mCursor.requery(); 341 buildMap(); 342 for (DataSetObserver o : mObservers) { 343 o.onChanged(); 344 } 345 } 346 347 private void buildMap() { 348 // The cursor is sorted by date 349 // The ItemMap will store the number of items in each bin. 350 int array[] = new int[DateSorter.DAY_COUNT]; 351 // Zero out the array. 352 for (int j = 0; j < DateSorter.DAY_COUNT; j++) { 353 array[j] = 0; 354 } 355 mNumberOfBins = 0; 356 int dateIndex = -1; 357 if (mCursor.moveToFirst() && mCursor.getCount() > 0) { 358 while (!mCursor.isAfterLast()) { 359 long date = mCursor.getLong(Browser.HISTORY_PROJECTION_DATE_INDEX); 360 int index = mDateSorter.getIndex(date); 361 if (index > dateIndex) { 362 mNumberOfBins++; 363 if (index == DateSorter.DAY_COUNT - 1) { 364 // We are already in the last bin, so it will 365 // include all the remaining items 366 array[index] = mCursor.getCount() 367 - mCursor.getPosition(); 368 break; 369 } 370 dateIndex = index; 371 } 372 array[dateIndex]++; 373 mCursor.moveToNext(); 374 } 375 } 376 mItemMap = array; 377 } 378 379 // This translates from a group position in the Adapter to a position in 380 // our array. This is necessary because some positions in the array 381 // have no history items, so we simply do not present those positions 382 // to the Adapter. 383 private int groupPositionToArrayPosition(int groupPosition) { 384 if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) { 385 throw new AssertionError("group position out of range"); 386 } 387 if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) { 388 // In the first case, we have exactly the same number of bins 389 // as our maximum possible, so there is no need to do a 390 // conversion 391 // The second statement is in case this method gets called when 392 // the array is empty, in which case the provided groupPosition 393 // will do fine. 394 return groupPosition; 395 } 396 int arrayPosition = -1; 397 while (groupPosition > -1) { 398 arrayPosition++; 399 if (mItemMap[arrayPosition] != 0) { 400 groupPosition--; 401 } 402 } 403 return arrayPosition; 404 } 405 406 public View getChildView(int groupPosition, int childPosition, boolean isLastChild, 407 View convertView, ViewGroup parent) { 408 groupPosition = groupPositionToArrayPosition(groupPosition); 409 HistoryItem item; 410 if (null == convertView || !(convertView instanceof HistoryItem)) { 411 item = new HistoryItem(BrowserHistoryPage.this); 412 // Add padding on the left so it will be indented from the 413 // arrows on the group views. 414 item.setPadding(item.getPaddingLeft() + 10, 415 item.getPaddingTop(), 416 item.getPaddingRight(), 417 item.getPaddingBottom()); 418 } else { 419 item = (HistoryItem) convertView; 420 } 421 // Bail early if the Cursor is closed. 422 if (mCursor.isClosed()) return item; 423 int index = childPosition; 424 for (int i = 0; i < groupPosition; i++) { 425 index += mItemMap[i]; 426 } 427 mCursor.moveToPosition(index); 428 item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); 429 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); 430 item.setUrl(url); 431 byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); 432 if (data != null) { 433 item.setFavicon(BitmapFactory.decodeByteArray(data, 0, 434 data.length)); 435 } else { 436 item.setFavicon(CombinedBookmarkHistoryActivity 437 .getIconListenerSet().getFavicon(url)); 438 } 439 item.setIsBookmark(1 == 440 mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX)); 441 return item; 442 } 443 444 public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { 445 groupPosition = groupPositionToArrayPosition(groupPosition); 446 TextView item; 447 if (null == convertView || !(convertView instanceof TextView)) { 448 LayoutInflater factory = 449 LayoutInflater.from(BrowserHistoryPage.this); 450 item = (TextView) 451 factory.inflate(R.layout.history_header, null); 452 } else { 453 item = (TextView) convertView; 454 } 455 item.setText(mDateSorter.getLabel(groupPosition)); 456 return item; 457 } 458 459 public boolean areAllItemsEnabled() { 460 return true; 461 } 462 463 public boolean isChildSelectable(int groupPosition, int childPosition) { 464 return true; 465 } 466 467 public int getGroupCount() { 468 return mNumberOfBins; 469 } 470 471 public int getChildrenCount(int groupPosition) { 472 return mItemMap[groupPositionToArrayPosition(groupPosition)]; 473 } 474 475 public Object getGroup(int groupPosition) { 476 return null; 477 } 478 479 public Object getChild(int groupPosition, int childPosition) { 480 return null; 481 } 482 483 public long getGroupId(int groupPosition) { 484 return groupPosition; 485 } 486 487 public long getChildId(int groupPosition, int childPosition) { 488 return (childPosition << 3) + groupPosition; 489 } 490 491 public boolean hasStableIds() { 492 return true; 493 } 494 495 public void registerDataSetObserver(DataSetObserver observer) { 496 mObservers.add(observer); 497 } 498 499 public void unregisterDataSetObserver(DataSetObserver observer) { 500 mObservers.remove(observer); 501 } 502 503 public void onGroupExpanded(int groupPosition) { 504 505 } 506 507 public void onGroupCollapsed(int groupPosition) { 508 509 } 510 511 public long getCombinedChildId(long groupId, long childId) { 512 return childId; 513 } 514 515 public long getCombinedGroupId(long groupId) { 516 return groupId; 517 } 518 519 public boolean isEmpty() { 520 return mCursor.isClosed() || mCursor.getCount() == 0; 521 } 522 } 523} 524