summaryrefslogtreecommitdiffstats
path: root/src/com/android/browser/BrowserHistoryPage.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/browser/BrowserHistoryPage.java')
-rw-r--r--src/com/android/browser/BrowserHistoryPage.java468
1 files changed, 468 insertions, 0 deletions
diff --git a/src/com/android/browser/BrowserHistoryPage.java b/src/com/android/browser/BrowserHistoryPage.java
new file mode 100644
index 0000000..42ca848
--- /dev/null
+++ b/src/com/android/browser/BrowserHistoryPage.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.app.ExpandableListActivity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ServiceManager;
+import android.provider.Browser;
+import android.text.IClipboard;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.webkit.DateSorter;
+import android.webkit.WebIconDatabase.IconListener;
+import android.widget.AdapterView;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.TextView;
+
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * Activity for displaying the browser's history, divided into
+ * days of viewing.
+ */
+public class BrowserHistoryPage extends ExpandableListActivity {
+ private HistoryAdapter mAdapter;
+ private DateSorter mDateSorter;
+ private boolean mMaxTabsOpen;
+
+ private final static String LOGTAG = "browser";
+
+ // Implementation of WebIconDatabase.IconListener
+ private class IconReceiver implements IconListener {
+ public void onReceivedIcon(String url, Bitmap icon) {
+ setListAdapter(mAdapter);
+ }
+ }
+ // Instance of IconReceiver
+ private final IconReceiver mIconReceiver = new IconReceiver();
+
+ /**
+ * Report back to the calling activity to load a site.
+ * @param url Site to load.
+ * @param newWindow True if the URL should be loaded in a new window
+ */
+ private void loadUrl(String url, boolean newWindow) {
+ Intent intent = new Intent().setAction(url);
+ if (newWindow) {
+ Bundle b = new Bundle();
+ b.putBoolean("new_window", true);
+ intent.putExtras(b);
+ }
+ setResultToParent(RESULT_OK, intent);
+ finish();
+ }
+
+ private void copy(CharSequence text) {
+ try {
+ IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
+ if (clip != null) {
+ clip.setClipboardText(text);
+ }
+ } catch (android.os.RemoteException e) {
+ Log.e(LOGTAG, "Copy failed", e);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setTitle(R.string.browser_history);
+
+ mDateSorter = new DateSorter(this);
+
+ mAdapter = new HistoryAdapter();
+ setListAdapter(mAdapter);
+ final ExpandableListView list = getExpandableListView();
+ list.setOnCreateContextMenuListener(this);
+ LayoutInflater factory = LayoutInflater.from(this);
+ View v = factory.inflate(R.layout.empty_history, null);
+ addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT,
+ LayoutParams.FILL_PARENT));
+ list.setEmptyView(v);
+ // Do not post the runnable if there is nothing in the list.
+ if (list.getExpandableListAdapter().getGroupCount() > 0) {
+ list.post(new Runnable() {
+ public void run() {
+ // In case the history gets cleared before this event
+ // happens.
+ if (list.getExpandableListAdapter().getGroupCount() > 0) {
+ list.expandGroup(0);
+ }
+ }
+ });
+ }
+ mMaxTabsOpen = getIntent().getBooleanExtra("maxTabsOpen", false);
+ CombinedBookmarkHistoryActivity.getIconListenerSet(getContentResolver())
+ .addListener(mIconReceiver);
+
+ // initialize the result to canceled, so that if the user just presses
+ // back then it will have the correct result
+ setResultToParent(RESULT_CANCELED, null);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.history, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver()));
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.clear_history_menu_id:
+ // FIXME: Need to clear the tab control in browserActivity
+ // as well
+ Browser.clearHistory(getContentResolver());
+ mAdapter.refreshData();
+ return true;
+
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ ExpandableListContextMenuInfo i =
+ (ExpandableListContextMenuInfo) menuInfo;
+ // Do not allow a context menu to come up from the group views.
+ if (!(i.targetView instanceof HistoryItem)) {
+ return;
+ }
+
+ // Inflate the menu
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.historycontext, menu);
+
+ // Setup the header
+ menu.setHeaderTitle(((HistoryItem)i.targetView).getUrl());
+
+ // Only show open in new tab if we have not maxed out available tabs
+ menu.findItem(R.id.new_window_context_menu_id).setVisible(!mMaxTabsOpen);
+
+ // decide whether to show the share link option
+ PackageManager pm = getPackageManager();
+ Intent send = new Intent(Intent.ACTION_SEND);
+ send.setType("text/plain");
+ ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
+ menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null);
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ ExpandableListContextMenuInfo i =
+ (ExpandableListContextMenuInfo) item.getMenuInfo();
+ String url = ((HistoryItem)i.targetView).getUrl();
+ String title = ((HistoryItem)i.targetView).getName();
+ switch (item.getItemId()) {
+ case R.id.open_context_menu_id:
+ loadUrl(url, false);
+ return true;
+ case R.id.new_window_context_menu_id:
+ loadUrl(url, true);
+ return true;
+ case R.id.save_to_bookmarks_menu_id:
+ Browser.saveBookmark(this, title, url);
+ return true;
+ case R.id.share_link_context_menu_id:
+ Browser.sendString(this, url);
+ return true;
+ case R.id.copy_context_menu_id:
+ copy(url);
+ return true;
+ case R.id.delete_context_menu_id:
+ Browser.deleteFromHistory(getContentResolver(), url);
+ mAdapter.refreshData();
+ return true;
+ default:
+ break;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ @Override
+ public boolean onChildClick(ExpandableListView parent, View v,
+ int groupPosition, int childPosition, long id) {
+ if (v instanceof HistoryItem) {
+ loadUrl(((HistoryItem) v).getUrl(), false);
+ return true;
+ }
+ return false;
+ }
+
+ // This Activity is generally a sub-Activity of CombinedHistoryActivity. In
+ // that situation, we need to pass our result code up to our parent.
+ // However, if someone calls this Activity directly, then this has no
+ // parent, and it needs to set it on itself.
+ private void setResultToParent(int resultCode, Intent data) {
+ Activity a = getParent() == null ? this : getParent();
+ a.setResult(resultCode, data);
+ }
+
+ private class ChangeObserver extends ContentObserver {
+ public ChangeObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mAdapter.refreshData();
+ }
+ }
+
+ private class HistoryAdapter implements ExpandableListAdapter {
+
+ // Array for each of our bins. Each entry represents how many items are
+ // in that bin.
+ int mItemMap[];
+ // This is our GroupCount. We will have at most DateSorter.DAY_COUNT
+ // bins, less if the user has no items in one or more bins.
+ int mNumberOfBins;
+ Vector<DataSetObserver> mObservers;
+ Cursor mCursor;
+
+ HistoryAdapter() {
+ mObservers = new Vector<DataSetObserver>();
+
+ String whereClause = Browser.BookmarkColumns.VISITS + " > 0 ";
+ String orderBy = Browser.BookmarkColumns.DATE + " DESC";
+
+ mCursor = managedQuery(
+ Browser.BOOKMARKS_URI,
+ Browser.HISTORY_PROJECTION,
+ whereClause, null, orderBy);
+
+ buildMap();
+ mCursor.registerContentObserver(new ChangeObserver());
+ }
+
+ void refreshData() {
+ mCursor.requery();
+ buildMap();
+ for (DataSetObserver o : mObservers) {
+ o.onChanged();
+ }
+ }
+
+ private void buildMap() {
+ // The cursor is sorted by date
+ // The ItemMap will store the number of items in each bin.
+ int array[] = new int[DateSorter.DAY_COUNT];
+ // Zero out the array.
+ for (int j = 0; j < DateSorter.DAY_COUNT; j++) {
+ array[j] = 0;
+ }
+ mNumberOfBins = 0;
+ int dateIndex = -1;
+ if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
+ while (!mCursor.isAfterLast()) {
+ long date = mCursor.getLong(Browser.HISTORY_PROJECTION_DATE_INDEX);
+ int index = mDateSorter.getIndex(date);
+ if (index > dateIndex) {
+ mNumberOfBins++;
+ if (index == DateSorter.DAY_COUNT - 1) {
+ // We are already in the last bin, so it will
+ // include all the remaining items
+ array[index] = mCursor.getCount()
+ - mCursor.getPosition();
+ break;
+ }
+ dateIndex = index;
+ }
+ array[dateIndex]++;
+ mCursor.moveToNext();
+ }
+ }
+ mItemMap = array;
+ }
+
+ // This translates from a group position in the Adapter to a position in
+ // our array. This is necessary because some positions in the array
+ // have no history items, so we simply do not present those positions
+ // to the Adapter.
+ private int groupPositionToArrayPosition(int groupPosition) {
+ if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
+ throw new AssertionError("group position out of range");
+ }
+ if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) {
+ // In the first case, we have exactly the same number of bins
+ // as our maximum possible, so there is no need to do a
+ // conversion
+ // The second statement is in case this method gets called when
+ // the array is empty, in which case the provided groupPosition
+ // will do fine.
+ return groupPosition;
+ }
+ int arrayPosition = -1;
+ while (groupPosition > -1) {
+ arrayPosition++;
+ if (mItemMap[arrayPosition] != 0) {
+ groupPosition--;
+ }
+ }
+ return arrayPosition;
+ }
+
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ groupPosition = groupPositionToArrayPosition(groupPosition);
+ HistoryItem item;
+ if (null == convertView || !(convertView instanceof HistoryItem)) {
+ item = new HistoryItem(BrowserHistoryPage.this);
+ // Add padding on the left so it will be indented from the
+ // arrows on the group views.
+ item.setPadding(item.getPaddingLeft() + 10,
+ item.getPaddingTop(),
+ item.getPaddingRight(),
+ item.getPaddingBottom());
+ } else {
+ item = (HistoryItem) convertView;
+ }
+ int index = childPosition;
+ for (int i = 0; i < groupPosition; i++) {
+ index += mItemMap[i];
+ }
+ mCursor.moveToPosition(index);
+ item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
+ String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
+ item.setUrl(url);
+ item.setFavicon(CombinedBookmarkHistoryActivity.getIconListenerSet(
+ getContentResolver()).getFavicon(url));
+ item.setIsBookmark(1 ==
+ mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX));
+ return item;
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
+ groupPosition = groupPositionToArrayPosition(groupPosition);
+ TextView item;
+ if (null == convertView || !(convertView instanceof TextView)) {
+ LayoutInflater factory =
+ LayoutInflater.from(BrowserHistoryPage.this);
+ item = (TextView)
+ factory.inflate(R.layout.history_header, null);
+ } else {
+ item = (TextView) convertView;
+ }
+ item.setText(mDateSorter.getLabel(groupPosition));
+ return item;
+ }
+
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public int getGroupCount() {
+ return mNumberOfBins;
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ return mItemMap[groupPositionToArrayPosition(groupPosition)];
+ }
+
+ public Object getGroup(int groupPosition) {
+ return null;
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ return null;
+ }
+
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ return (childPosition << 3) + groupPosition;
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mObservers.add(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mObservers.remove(observer);
+ }
+
+ public void onGroupExpanded(int groupPosition) {
+
+ }
+
+ public void onGroupCollapsed(int groupPosition) {
+
+ }
+
+ public long getCombinedChildId(long groupId, long childId) {
+ return childId;
+ }
+
+ public long getCombinedGroupId(long groupId) {
+ return groupId;
+ }
+
+ public boolean isEmpty() {
+ return mCursor.getCount() == 0;
+ }
+ }
+}