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.os.Bundle;
29 import android.os.Handler;
30 import android.os.ServiceManager;
31 import android.provider.Browser;
32 import android.text.IClipboard;
33 import android.util.Log;
34 import android.view.ContextMenu;
35 import android.view.KeyEvent;
36 import android.view.LayoutInflater;
37 import android.view.Menu;
38 import android.view.MenuInflater;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.ViewGroup.LayoutParams;
43 import android.view.ContextMenu.ContextMenuInfo;
44 import android.view.ViewStub;
45 import android.webkit.DateSorter;
46 import android.webkit.WebIconDatabase.IconListener;
47 import android.widget.AdapterView;
48 import android.widget.ExpandableListAdapter;
49 import android.widget.ExpandableListView;
50 import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
51 import android.widget.TextView;
53 import java.util.List;
54 import java.util.Vector;
57 * Activity for displaying the browser's history, divided into
60 public class BrowserHistoryPage extends ExpandableListActivity {
61 private HistoryAdapter mAdapter;
62 private DateSorter mDateSorter;
63 private boolean mMaxTabsOpen;
65 private final static String LOGTAG = "browser";
67 // Implementation of WebIconDatabase.IconListener
68 private class IconReceiver implements IconListener {
69 public void onReceivedIcon(String url, Bitmap icon) {
70 setListAdapter(mAdapter);
73 // Instance of IconReceiver
74 private final IconReceiver mIconReceiver = new IconReceiver();
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
81 private void loadUrl(String url, boolean newWindow) {
82 Intent intent = new Intent().setAction(url);
84 Bundle b = new Bundle();
85 b.putBoolean("new_window", true);
88 setResultToParent(RESULT_OK, intent);
92 private void copy(CharSequence text) {
94 IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
96 clip.setClipboardText(text);
98 } catch (android.os.RemoteException e) {
99 Log.e(LOGTAG, "Copy failed", e);
104 protected void onCreate(Bundle icicle) {
105 super.onCreate(icicle);
106 setTitle(R.string.browser_history);
108 mDateSorter = new DateSorter(this);
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() {
122 // In case the history gets cleared before this event
124 if (list.getExpandableListAdapter().getGroupCount() > 0) {
130 mMaxTabsOpen = getIntent().getBooleanExtra("maxTabsOpen", false);
131 CombinedBookmarkHistoryActivity.getIconListenerSet(getContentResolver())
132 .addListener(mIconReceiver);
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);
140 public boolean onCreateOptionsMenu(Menu menu) {
141 super.onCreateOptionsMenu(menu);
142 MenuInflater inflater = getMenuInflater();
143 inflater.inflate(R.menu.history, menu);
148 public boolean onPrepareOptionsMenu(Menu menu) {
149 menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver()));
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
159 Browser.clearHistory(getContentResolver());
160 mAdapter.refreshData();
166 return super.onOptionsItemSelected(item);
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)) {
180 MenuInflater inflater = getMenuInflater();
181 inflater.inflate(R.menu.historycontext, menu);
184 menu.setHeaderTitle(((HistoryItem)i.targetView).getUrl());
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);
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);
196 super.onCreateContextMenu(menu, v, menuInfo);
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:
209 case R.id.new_window_context_menu_id:
212 case R.id.save_to_bookmarks_menu_id:
213 Browser.saveBookmark(this, title, url);
215 case R.id.share_link_context_menu_id:
216 Browser.sendString(this, url);
218 case R.id.copy_context_menu_id:
221 case R.id.delete_context_menu_id:
222 Browser.deleteFromHistory(getContentResolver(), url);
223 mAdapter.refreshData();
228 return super.onContextItemSelected(item);
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);
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);
250 private class ChangeObserver extends ContentObserver {
251 public ChangeObserver() {
252 super(new Handler());
256 public boolean deliverSelfNotifications() {
261 public void onChange(boolean selfChange) {
262 mAdapter.refreshData();
266 private class HistoryAdapter implements ExpandableListAdapter {
268 // Array for each of our bins. Each entry represents how many items are
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.
274 Vector<DataSetObserver> mObservers;
278 mObservers = new Vector<DataSetObserver>();
280 String whereClause = Browser.BookmarkColumns.VISITS + " > 0 ";
281 String orderBy = Browser.BookmarkColumns.DATE + " DESC";
283 mCursor = managedQuery(
284 Browser.BOOKMARKS_URI,
285 Browser.HISTORY_PROJECTION,
286 whereClause, null, orderBy);
289 mCursor.registerContentObserver(new ChangeObserver());
295 for (DataSetObserver o : mObservers) {
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++) {
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) {
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();
326 mCursor.moveToNext();
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
336 private int groupPositionToArrayPosition(int groupPosition) {
337 if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
338 throw new AssertionError("group position out of range");
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
344 // The second statement is in case this method gets called when
345 // the array is empty, in which case the provided groupPosition
347 return groupPosition;
349 int arrayPosition = -1;
350 while (groupPosition > -1) {
352 if (mItemMap[arrayPosition] != 0) {
356 return arrayPosition;
359 public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
360 View convertView, ViewGroup parent) {
361 groupPosition = groupPositionToArrayPosition(groupPosition);
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());
372 item = (HistoryItem) convertView;
374 int index = childPosition;
375 for (int i = 0; i < groupPosition; i++) {
376 index += mItemMap[i];
378 mCursor.moveToPosition(index);
379 item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
380 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
382 item.setFavicon(CombinedBookmarkHistoryActivity.getIconListenerSet(
383 getContentResolver()).getFavicon(url));
384 item.setIsBookmark(1 ==
385 mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX));
389 public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
390 groupPosition = groupPositionToArrayPosition(groupPosition);
392 if (null == convertView || !(convertView instanceof TextView)) {
393 LayoutInflater factory =
394 LayoutInflater.from(BrowserHistoryPage.this);
396 factory.inflate(R.layout.history_header, null);
398 item = (TextView) convertView;
400 item.setText(mDateSorter.getLabel(groupPosition));
404 public boolean areAllItemsEnabled() {
408 public boolean isChildSelectable(int groupPosition, int childPosition) {
412 public int getGroupCount() {
413 return mNumberOfBins;
416 public int getChildrenCount(int groupPosition) {
417 return mItemMap[groupPositionToArrayPosition(groupPosition)];
420 public Object getGroup(int groupPosition) {
424 public Object getChild(int groupPosition, int childPosition) {
428 public long getGroupId(int groupPosition) {
429 return groupPosition;
432 public long getChildId(int groupPosition, int childPosition) {
433 return (childPosition << 3) + groupPosition;
436 public boolean hasStableIds() {
440 public void registerDataSetObserver(DataSetObserver observer) {
441 mObservers.add(observer);
444 public void unregisterDataSetObserver(DataSetObserver observer) {
445 mObservers.remove(observer);
448 public void onGroupExpanded(int groupPosition) {
452 public void onGroupCollapsed(int groupPosition) {
456 public long getCombinedChildId(long groupId, long childId) {
460 public long getCombinedGroupId(long groupId) {
464 public boolean isEmpty() {
465 return mCursor.getCount() == 0;