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