blob: 4a0f8c52e23b658da4a245c3ff63e97beb597251 [file] [log] [blame]
/*
* 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
protected void onDestroy() {
super.onDestroy();
CombinedBookmarkHistoryActivity.getIconListenerSet(getContentResolver())
.removeListener(mIconReceiver);
}
@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;
}
}
}