2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.browser;
19 import android.app.Activity;
20 import android.app.ExpandableListActivity;
21 import android.content.Intent;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ResolveInfo;
24 import android.database.ContentObserver;
25 import android.database.Cursor;
26 import android.database.DataSetObserver;
27 import android.graphics.Bitmap;
28 import android.graphics.BitmapFactory;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.ServiceManager;
32 import android.provider.Browser;
33 import android.text.IClipboard;
34 import android.util.Log;
35 import android.view.ContextMenu;
36 import android.view.KeyEvent;
37 import android.view.LayoutInflater;
38 import android.view.Menu;
39 import android.view.MenuInflater;
40 import android.view.MenuItem;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.ViewGroup.LayoutParams;
44 import android.view.ContextMenu.ContextMenuInfo;
45 import android.view.ViewStub;
46 import android.webkit.DateSorter;
47 import android.webkit.WebIconDatabase.IconListener;
48 import android.widget.AdapterView;
49 import android.widget.ExpandableListAdapter;
50 import android.widget.ExpandableListView;
51 import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
52 import android.widget.TextView;
53 import android.widget.Toast;
55 import java.util.List;
56 import java.util.Vector;
59 * Activity for displaying the browser's history, divided into
62 public class BrowserHistoryPage extends ExpandableListActivity {
63 private HistoryAdapter mAdapter;
64 private DateSorter mDateSorter;
65 private boolean mDisableNewWindow;
66 private HistoryItem mContextHeader;
68 private final static String LOGTAG = "browser";
70 // Implementation of WebIconDatabase.IconListener
71 private class IconReceiver implements IconListener {
72 public void onReceivedIcon(String url, Bitmap icon) {
73 setListAdapter(mAdapter);
76 // Instance of IconReceiver
77 private final IconReceiver mIconReceiver = new IconReceiver();
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
84 private void loadUrl(String url, boolean newWindow) {
85 Intent intent = new Intent().setAction(url);
87 Bundle b = new Bundle();
88 b.putBoolean("new_window", true);
91 setResultToParent(RESULT_OK, intent);
95 private void copy(CharSequence text) {
97 IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
99 clip.setClipboardText(text);
101 } catch (android.os.RemoteException e) {
102 Log.e(LOGTAG, "Copy failed", e);
107 protected void onCreate(Bundle icicle) {
108 super.onCreate(icicle);
109 setTitle(R.string.browser_history);
111 mDateSorter = new DateSorter(this);
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() {
125 // In case the history gets cleared before this event
127 if (list.getExpandableListAdapter().getGroupCount() > 0) {
133 mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window",
135 CombinedBookmarkHistoryActivity.getIconListenerSet()
136 .addListener(mIconReceiver);
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);
144 protected void onDestroy() {
146 CombinedBookmarkHistoryActivity.getIconListenerSet()
147 .removeListener(mIconReceiver);
151 public boolean onCreateOptionsMenu(Menu menu) {
152 super.onCreateOptionsMenu(menu);
153 MenuInflater inflater = getMenuInflater();
154 inflater.inflate(R.menu.history, menu);
159 public boolean onPrepareOptionsMenu(Menu menu) {
160 menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver()));
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
170 Browser.clearHistory(getContentResolver());
171 mAdapter.refreshData();
177 return super.onOptionsItemSelected(item);
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)) {
191 MenuInflater inflater = getMenuInflater();
192 inflater.inflate(R.menu.historycontext, menu);
194 HistoryItem historyItem = (HistoryItem) i.targetView;
197 if (mContextHeader == null) {
198 mContextHeader = new HistoryItem(this);
199 } else if (mContextHeader.getParent() != null) {
200 ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader);
202 historyItem.copyTo(mContextHeader);
203 menu.setHeaderView(mContextHeader);
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);
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);
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);
221 super.onCreateContextMenu(menu, v, menuInfo);
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:
235 case R.id.new_window_context_menu_id:
238 case R.id.save_to_bookmarks_menu_id:
239 if (historyItem.isBookmark()) {
240 Bookmarks.removeFromBookmarks(this, getContentResolver(),
243 Browser.saveBookmark(this, title, url);
246 case R.id.share_link_context_menu_id:
247 Browser.sendString(this, url,
248 getText(R.string.choosertitle_sharevia).toString());
250 case R.id.copy_url_context_menu_id:
253 case R.id.delete_context_menu_id:
254 Browser.deleteFromHistory(getContentResolver(), url);
255 mAdapter.refreshData();
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();
265 return super.onContextItemSelected(item);
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);
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);
287 private class ChangeObserver extends ContentObserver {
288 public ChangeObserver() {
289 super(new Handler());
293 public boolean deliverSelfNotifications() {
298 public void onChange(boolean selfChange) {
299 mAdapter.refreshData();
303 private class HistoryAdapter implements ExpandableListAdapter {
305 // Array for each of our bins. Each entry represents how many items are
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;
315 mObservers = new Vector<DataSetObserver>();
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";
327 mCursor = managedQuery(
328 Browser.BOOKMARKS_URI,
329 Browser.HISTORY_PROJECTION,
330 whereClause, null, orderBy);
333 mCursor.registerContentObserver(new ChangeObserver());
337 if (mCursor.isClosed()) {
342 for (DataSetObserver o : mObservers) {
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++) {
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) {
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();
373 mCursor.moveToNext();
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
383 private int groupPositionToArrayPosition(int groupPosition) {
384 if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
385 throw new AssertionError("group position out of range");
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
391 // The second statement is in case this method gets called when
392 // the array is empty, in which case the provided groupPosition
394 return groupPosition;
396 int arrayPosition = -1;
397 while (groupPosition > -1) {
399 if (mItemMap[arrayPosition] != 0) {
403 return arrayPosition;
406 public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
407 View convertView, ViewGroup parent) {
408 groupPosition = groupPositionToArrayPosition(groupPosition);
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());
419 item = (HistoryItem) convertView;
421 int index = childPosition;
422 for (int i = 0; i < groupPosition; i++) {
423 index += mItemMap[i];
425 mCursor.moveToPosition(index);
426 item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
427 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
429 byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
431 item.setFavicon(BitmapFactory.decodeByteArray(data, 0,
434 item.setFavicon(CombinedBookmarkHistoryActivity
435 .getIconListenerSet().getFavicon(url));
437 item.setIsBookmark(1 ==
438 mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX));
442 public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
443 groupPosition = groupPositionToArrayPosition(groupPosition);
445 if (null == convertView || !(convertView instanceof TextView)) {
446 LayoutInflater factory =
447 LayoutInflater.from(BrowserHistoryPage.this);
449 factory.inflate(R.layout.history_header, null);
451 item = (TextView) convertView;
453 item.setText(mDateSorter.getLabel(groupPosition));
457 public boolean areAllItemsEnabled() {
461 public boolean isChildSelectable(int groupPosition, int childPosition) {
465 public int getGroupCount() {
466 return mNumberOfBins;
469 public int getChildrenCount(int groupPosition) {
470 return mItemMap[groupPositionToArrayPosition(groupPosition)];
473 public Object getGroup(int groupPosition) {
477 public Object getChild(int groupPosition, int childPosition) {
481 public long getGroupId(int groupPosition) {
482 return groupPosition;
485 public long getChildId(int groupPosition, int childPosition) {
486 return (childPosition << 3) + groupPosition;
489 public boolean hasStableIds() {
493 public void registerDataSetObserver(DataSetObserver observer) {
494 mObservers.add(observer);
497 public void unregisterDataSetObserver(DataSetObserver observer) {
498 mObservers.remove(observer);
501 public void onGroupExpanded(int groupPosition) {
505 public void onGroupCollapsed(int groupPosition) {
509 public long getCombinedChildId(long groupId, long childId) {
513 public long getCombinedGroupId(long groupId) {
517 public boolean isEmpty() {
518 return mCursor.getCount() == 0;