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