BrowserHistoryPage.java revision 22807d1975984667829138d7d47d2020f8632f11
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.graphics.BitmapFactory;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.ServiceManager;
32import android.provider.Browser;
33import android.text.IClipboard;
34import android.util.Log;
35import android.view.ContextMenu;
36import android.view.KeyEvent;
37import android.view.LayoutInflater;
38import android.view.Menu;
39import android.view.MenuInflater;
40import android.view.MenuItem;
41import android.view.View;
42import android.view.ViewGroup;
43import android.view.ViewGroup.LayoutParams;
44import android.view.ContextMenu.ContextMenuInfo;
45import android.view.ViewStub;
46import android.webkit.DateSorter;
47import android.webkit.WebIconDatabase.IconListener;
48import android.widget.AdapterView;
49import android.widget.ExpandableListAdapter;
50import android.widget.ExpandableListView;
51import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
52import android.widget.TextView;
53import android.widget.Toast;
54
55import java.util.List;
56import java.util.Vector;
57
58/**
59 * Activity for displaying the browser's history, divided into
60 * days of viewing.
61 */
62public class BrowserHistoryPage extends ExpandableListActivity {
63    private HistoryAdapter          mAdapter;
64    private DateSorter              mDateSorter;
65    private boolean                 mDisableNewWindow;
66    private HistoryItem             mContextHeader;
67
68    private final static String LOGTAG = "browser";
69
70    // Implementation of WebIconDatabase.IconListener
71    private class IconReceiver implements IconListener {
72        public void onReceivedIcon(String url, Bitmap icon) {
73            setListAdapter(mAdapter);
74        }
75    }
76    // Instance of IconReceiver
77    private final IconReceiver mIconReceiver = new IconReceiver();
78
79    /**
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
83     */
84    private void loadUrl(String url, boolean newWindow) {
85        Intent intent = new Intent().setAction(url);
86        if (newWindow) {
87            Bundle b = new Bundle();
88            b.putBoolean("new_window", true);
89            intent.putExtras(b);
90        }
91        setResultToParent(RESULT_OK, intent);
92        finish();
93    }
94
95    private void copy(CharSequence text) {
96        try {
97            IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
98            if (clip != null) {
99                clip.setClipboardText(text);
100            }
101        } catch (android.os.RemoteException e) {
102            Log.e(LOGTAG, "Copy failed", e);
103        }
104    }
105
106    @Override
107    protected void onCreate(Bundle icicle) {
108        super.onCreate(icicle);
109        setTitle(R.string.browser_history);
110
111        mDateSorter = new DateSorter(this);
112
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() {
124                public void run() {
125                    // In case the history gets cleared before this event
126                    // happens.
127                    if (list.getExpandableListAdapter().getGroupCount() > 0) {
128                        list.expandGroup(0);
129                    }
130                }
131            });
132        }
133        mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window",
134                false);
135        CombinedBookmarkHistoryActivity.getIconListenerSet()
136                .addListener(mIconReceiver);
137
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);
141    }
142
143    @Override
144    protected void onDestroy() {
145        super.onDestroy();
146        CombinedBookmarkHistoryActivity.getIconListenerSet()
147                .removeListener(mIconReceiver);
148    }
149
150    @Override
151    public boolean onCreateOptionsMenu(Menu menu) {
152        super.onCreateOptionsMenu(menu);
153        MenuInflater inflater = getMenuInflater();
154        inflater.inflate(R.menu.history, menu);
155        return true;
156    }
157
158    @Override
159    public boolean onPrepareOptionsMenu(Menu menu) {
160        menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver()));
161        return true;
162    }
163
164    @Override
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
169                // as well
170                Browser.clearHistory(getContentResolver());
171                mAdapter.refreshData();
172                return true;
173
174            default:
175                break;
176        }
177        return super.onOptionsItemSelected(item);
178    }
179
180    @Override
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)) {
187            return;
188        }
189
190        // Inflate the menu
191        MenuInflater inflater = getMenuInflater();
192        inflater.inflate(R.menu.historycontext, menu);
193
194        HistoryItem historyItem = (HistoryItem) i.targetView;
195
196        // Setup the header
197        if (mContextHeader == null) {
198            mContextHeader = new HistoryItem(this);
199        } else if (mContextHeader.getParent() != null) {
200            ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader);
201        }
202        historyItem.copyTo(mContextHeader);
203        menu.setHeaderView(mContextHeader);
204
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);
208        }
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);
213        }
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);
220
221        super.onCreateContextMenu(menu, v, menuInfo);
222    }
223
224    @Override
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:
233                loadUrl(url, false);
234                return true;
235            case R.id.new_window_context_menu_id:
236                loadUrl(url, true);
237                return true;
238            case R.id.save_to_bookmarks_menu_id:
239                if (historyItem.isBookmark()) {
240                    Bookmarks.removeFromBookmarks(this, getContentResolver(),
241                            url, title);
242                } else {
243                    Browser.saveBookmark(this, title, url);
244                }
245                return true;
246            case R.id.share_link_context_menu_id:
247                Browser.sendString(this, url,
248                        getText(R.string.choosertitle_sharevia).toString());
249                return true;
250            case R.id.copy_url_context_menu_id:
251                copy(url);
252                return true;
253            case R.id.delete_context_menu_id:
254                Browser.deleteFromHistory(getContentResolver(), url);
255                mAdapter.refreshData();
256                return true;
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();
261                return true;
262            default:
263                break;
264        }
265        return super.onContextItemSelected(item);
266    }
267
268    @Override
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);
273            return true;
274        }
275        return false;
276    }
277
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);
285    }
286
287    private class ChangeObserver extends ContentObserver {
288        public ChangeObserver() {
289            super(new Handler());
290        }
291
292        @Override
293        public boolean deliverSelfNotifications() {
294            return true;
295        }
296
297        @Override
298        public void onChange(boolean selfChange) {
299            mAdapter.refreshData();
300        }
301    }
302
303    private class HistoryAdapter implements ExpandableListAdapter {
304
305        // Array for each of our bins.  Each entry represents how many items are
306        // in that bin.
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;
313
314        HistoryAdapter() {
315            mObservers = new Vector<DataSetObserver>();
316
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";
326
327            mCursor = managedQuery(
328                    Browser.BOOKMARKS_URI,
329                    Browser.HISTORY_PROJECTION,
330                    whereClause, null, orderBy);
331
332            buildMap();
333            mCursor.registerContentObserver(new ChangeObserver());
334        }
335
336        void refreshData() {
337            if (mCursor.isClosed()) {
338                return;
339            }
340            mCursor.requery();
341            buildMap();
342            for (DataSetObserver o : mObservers) {
343                o.onChanged();
344            }
345        }
346
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++) {
353                array[j] = 0;
354            }
355            mNumberOfBins = 0;
356            int dateIndex = -1;
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) {
362                        mNumberOfBins++;
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();
368                            break;
369                        }
370                        dateIndex = index;
371                    }
372                    array[dateIndex]++;
373                    mCursor.moveToNext();
374                }
375            }
376            mItemMap = array;
377        }
378
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
382        // to the Adapter.
383        private int groupPositionToArrayPosition(int groupPosition) {
384            if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
385                throw new AssertionError("group position out of range");
386            }
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
390                // conversion
391                // The second statement is in case this method gets called when
392                // the array is empty, in which case the provided groupPosition
393                // will do fine.
394                return groupPosition;
395            }
396            int arrayPosition = -1;
397            while (groupPosition > -1) {
398                arrayPosition++;
399                if (mItemMap[arrayPosition] != 0) {
400                    groupPosition--;
401                }
402            }
403            return arrayPosition;
404        }
405
406        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
407                View convertView, ViewGroup parent) {
408            groupPosition = groupPositionToArrayPosition(groupPosition);
409            HistoryItem item;
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());
418            } else {
419                item = (HistoryItem) convertView;
420            }
421            // Bail early if the Cursor is closed.
422            if (mCursor.isClosed()) return item;
423            int index = childPosition;
424            for (int i = 0; i < groupPosition; i++) {
425                index += mItemMap[i];
426            }
427            mCursor.moveToPosition(index);
428            item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
429            String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
430            item.setUrl(url);
431            byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
432            if (data != null) {
433                item.setFavicon(BitmapFactory.decodeByteArray(data, 0,
434                        data.length));
435            } else {
436                item.setFavicon(CombinedBookmarkHistoryActivity
437                        .getIconListenerSet().getFavicon(url));
438            }
439            item.setIsBookmark(1 ==
440                    mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX));
441            return item;
442        }
443
444        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
445            groupPosition = groupPositionToArrayPosition(groupPosition);
446            TextView item;
447            if (null == convertView || !(convertView instanceof TextView)) {
448                LayoutInflater factory =
449                        LayoutInflater.from(BrowserHistoryPage.this);
450                item = (TextView)
451                        factory.inflate(R.layout.history_header, null);
452            } else {
453                item = (TextView) convertView;
454            }
455            item.setText(mDateSorter.getLabel(groupPosition));
456            return item;
457        }
458
459        public boolean areAllItemsEnabled() {
460            return true;
461        }
462
463        public boolean isChildSelectable(int groupPosition, int childPosition) {
464            return true;
465        }
466
467        public int getGroupCount() {
468            return mNumberOfBins;
469        }
470
471        public int getChildrenCount(int groupPosition) {
472            return mItemMap[groupPositionToArrayPosition(groupPosition)];
473        }
474
475        public Object getGroup(int groupPosition) {
476            return null;
477        }
478
479        public Object getChild(int groupPosition, int childPosition) {
480            return null;
481        }
482
483        public long getGroupId(int groupPosition) {
484            return groupPosition;
485        }
486
487        public long getChildId(int groupPosition, int childPosition) {
488            return (childPosition << 3) + groupPosition;
489        }
490
491        public boolean hasStableIds() {
492            return true;
493        }
494
495        public void registerDataSetObserver(DataSetObserver observer) {
496            mObservers.add(observer);
497        }
498
499        public void unregisterDataSetObserver(DataSetObserver observer) {
500            mObservers.remove(observer);
501        }
502
503        public void onGroupExpanded(int groupPosition) {
504
505        }
506
507        public void onGroupCollapsed(int groupPosition) {
508
509        }
510
511        public long getCombinedChildId(long groupId, long childId) {
512            return childId;
513        }
514
515        public long getCombinedGroupId(long groupId) {
516            return groupId;
517        }
518
519        public boolean isEmpty() {
520            return mCursor.isClosed() || mCursor.getCount() == 0;
521        }
522    }
523}
524