BrowserHistoryPage.java revision 8f0076b720c9ee1e9ef9d29910c261634fd5fb25
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); 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 return true; 249 case R.id.copy_url_context_menu_id: 250 copy(url); 251 return true; 252 case R.id.delete_context_menu_id: 253 Browser.deleteFromHistory(getContentResolver(), url); 254 mAdapter.refreshData(); 255 return true; 256 case R.id.homepage_context_menu_id: 257 BrowserSettings.getInstance().setHomePage(this, url); 258 Toast.makeText(this, R.string.homepage_set, 259 Toast.LENGTH_LONG).show(); 260 return true; 261 default: 262 break; 263 } 264 return super.onContextItemSelected(item); 265 } 266 267 @Override 268 public boolean onChildClick(ExpandableListView parent, View v, 269 int groupPosition, int childPosition, long id) { 270 if (v instanceof HistoryItem) { 271 loadUrl(((HistoryItem) v).getUrl(), false); 272 return true; 273 } 274 return false; 275 } 276 277 // This Activity is generally a sub-Activity of CombinedHistoryActivity. In 278 // that situation, we need to pass our result code up to our parent. 279 // However, if someone calls this Activity directly, then this has no 280 // parent, and it needs to set it on itself. 281 private void setResultToParent(int resultCode, Intent data) { 282 Activity a = getParent() == null ? this : getParent(); 283 a.setResult(resultCode, data); 284 } 285 286 private class ChangeObserver extends ContentObserver { 287 public ChangeObserver() { 288 super(new Handler()); 289 } 290 291 @Override 292 public boolean deliverSelfNotifications() { 293 return true; 294 } 295 296 @Override 297 public void onChange(boolean selfChange) { 298 mAdapter.refreshData(); 299 } 300 } 301 302 private class HistoryAdapter implements ExpandableListAdapter { 303 304 // Array for each of our bins. Each entry represents how many items are 305 // in that bin. 306 private int mItemMap[]; 307 // This is our GroupCount. We will have at most DateSorter.DAY_COUNT 308 // bins, less if the user has no items in one or more bins. 309 private int mNumberOfBins; 310 private Vector<DataSetObserver> mObservers; 311 private Cursor mCursor; 312 313 HistoryAdapter() { 314 mObservers = new Vector<DataSetObserver>(); 315 316 final String whereClause = Browser.BookmarkColumns.VISITS + " > 0" 317 // In AddBookmarkPage, where we save new bookmarks, we add 318 // three visits to newly created bookmarks, so that 319 // bookmarks that have not been visited will show up in the 320 // most visited, and higher in the goto search box. 321 // However, this puts the site in the history, unless we 322 // ignore sites with a DATE of 0, which the next line does. 323 + " AND " + Browser.BookmarkColumns.DATE + " > 0"; 324 final String orderBy = Browser.BookmarkColumns.DATE + " DESC"; 325 326 mCursor = managedQuery( 327 Browser.BOOKMARKS_URI, 328 Browser.HISTORY_PROJECTION, 329 whereClause, null, orderBy); 330 331 buildMap(); 332 mCursor.registerContentObserver(new ChangeObserver()); 333 } 334 335 void refreshData() { 336 if (mCursor.isClosed()) { 337 return; 338 } 339 mCursor.requery(); 340 buildMap(); 341 for (DataSetObserver o : mObservers) { 342 o.onChanged(); 343 } 344 } 345 346 private void buildMap() { 347 // The cursor is sorted by date 348 // The ItemMap will store the number of items in each bin. 349 int array[] = new int[DateSorter.DAY_COUNT]; 350 // Zero out the array. 351 for (int j = 0; j < DateSorter.DAY_COUNT; j++) { 352 array[j] = 0; 353 } 354 mNumberOfBins = 0; 355 int dateIndex = -1; 356 if (mCursor.moveToFirst() && mCursor.getCount() > 0) { 357 while (!mCursor.isAfterLast()) { 358 long date = mCursor.getLong(Browser.HISTORY_PROJECTION_DATE_INDEX); 359 int index = mDateSorter.getIndex(date); 360 if (index > dateIndex) { 361 mNumberOfBins++; 362 if (index == DateSorter.DAY_COUNT - 1) { 363 // We are already in the last bin, so it will 364 // include all the remaining items 365 array[index] = mCursor.getCount() 366 - mCursor.getPosition(); 367 break; 368 } 369 dateIndex = index; 370 } 371 array[dateIndex]++; 372 mCursor.moveToNext(); 373 } 374 } 375 mItemMap = array; 376 } 377 378 // This translates from a group position in the Adapter to a position in 379 // our array. This is necessary because some positions in the array 380 // have no history items, so we simply do not present those positions 381 // to the Adapter. 382 private int groupPositionToArrayPosition(int groupPosition) { 383 if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) { 384 throw new AssertionError("group position out of range"); 385 } 386 if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) { 387 // In the first case, we have exactly the same number of bins 388 // as our maximum possible, so there is no need to do a 389 // conversion 390 // The second statement is in case this method gets called when 391 // the array is empty, in which case the provided groupPosition 392 // will do fine. 393 return groupPosition; 394 } 395 int arrayPosition = -1; 396 while (groupPosition > -1) { 397 arrayPosition++; 398 if (mItemMap[arrayPosition] != 0) { 399 groupPosition--; 400 } 401 } 402 return arrayPosition; 403 } 404 405 public View getChildView(int groupPosition, int childPosition, boolean isLastChild, 406 View convertView, ViewGroup parent) { 407 groupPosition = groupPositionToArrayPosition(groupPosition); 408 HistoryItem item; 409 if (null == convertView || !(convertView instanceof HistoryItem)) { 410 item = new HistoryItem(BrowserHistoryPage.this); 411 // Add padding on the left so it will be indented from the 412 // arrows on the group views. 413 item.setPadding(item.getPaddingLeft() + 10, 414 item.getPaddingTop(), 415 item.getPaddingRight(), 416 item.getPaddingBottom()); 417 } else { 418 item = (HistoryItem) convertView; 419 } 420 int index = childPosition; 421 for (int i = 0; i < groupPosition; i++) { 422 index += mItemMap[i]; 423 } 424 mCursor.moveToPosition(index); 425 item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); 426 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); 427 item.setUrl(url); 428 byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); 429 if (data != null) { 430 item.setFavicon(BitmapFactory.decodeByteArray(data, 0, 431 data.length)); 432 } else { 433 item.setFavicon(CombinedBookmarkHistoryActivity 434 .getIconListenerSet().getFavicon(url)); 435 } 436 item.setIsBookmark(1 == 437 mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX)); 438 return item; 439 } 440 441 public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { 442 groupPosition = groupPositionToArrayPosition(groupPosition); 443 TextView item; 444 if (null == convertView || !(convertView instanceof TextView)) { 445 LayoutInflater factory = 446 LayoutInflater.from(BrowserHistoryPage.this); 447 item = (TextView) 448 factory.inflate(R.layout.history_header, null); 449 } else { 450 item = (TextView) convertView; 451 } 452 item.setText(mDateSorter.getLabel(groupPosition)); 453 return item; 454 } 455 456 public boolean areAllItemsEnabled() { 457 return true; 458 } 459 460 public boolean isChildSelectable(int groupPosition, int childPosition) { 461 return true; 462 } 463 464 public int getGroupCount() { 465 return mNumberOfBins; 466 } 467 468 public int getChildrenCount(int groupPosition) { 469 return mItemMap[groupPositionToArrayPosition(groupPosition)]; 470 } 471 472 public Object getGroup(int groupPosition) { 473 return null; 474 } 475 476 public Object getChild(int groupPosition, int childPosition) { 477 return null; 478 } 479 480 public long getGroupId(int groupPosition) { 481 return groupPosition; 482 } 483 484 public long getChildId(int groupPosition, int childPosition) { 485 return (childPosition << 3) + groupPosition; 486 } 487 488 public boolean hasStableIds() { 489 return true; 490 } 491 492 public void registerDataSetObserver(DataSetObserver observer) { 493 mObservers.add(observer); 494 } 495 496 public void unregisterDataSetObserver(DataSetObserver observer) { 497 mObservers.remove(observer); 498 } 499 500 public void onGroupExpanded(int groupPosition) { 501 502 } 503 504 public void onGroupCollapsed(int groupPosition) { 505 506 } 507 508 public long getCombinedChildId(long groupId, long childId) { 509 return childId; 510 } 511 512 public long getCombinedGroupId(long groupId) { 513 return groupId; 514 } 515 516 public boolean isEmpty() { 517 return mCursor.getCount() == 0; 518 } 519 } 520} 521