BrowserHistoryPage.java revision 83817df9b28943c0587388ea30b440f4b0744313
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.os.Bundle;
29import android.os.Handler;
30import android.os.ServiceManager;
31import android.provider.Browser;
32import android.text.IClipboard;
33import android.util.Log;
34import android.view.ContextMenu;
35import android.view.KeyEvent;
36import android.view.LayoutInflater;
37import android.view.Menu;
38import android.view.MenuInflater;
39import android.view.MenuItem;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.ViewGroup.LayoutParams;
43import android.view.ContextMenu.ContextMenuInfo;
44import android.view.ViewStub;
45import android.webkit.DateSorter;
46import android.webkit.WebIconDatabase.IconListener;
47import android.widget.AdapterView;
48import android.widget.ExpandableListAdapter;
49import android.widget.ExpandableListView;
50import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
51import android.widget.TextView;
52
53import java.util.List;
54import java.util.Vector;
55
56/**
57 * Activity for displaying the browser's history, divided into
58 * days of viewing.
59 */
60public class BrowserHistoryPage extends ExpandableListActivity {
61    private HistoryAdapter          mAdapter;
62    private DateSorter              mDateSorter;
63    private boolean                 mMaxTabsOpen;
64
65    private final static String LOGTAG = "browser";
66
67    // Implementation of WebIconDatabase.IconListener
68    private class IconReceiver implements IconListener {
69        public void onReceivedIcon(String url, Bitmap icon) {
70            setListAdapter(mAdapter);
71        }
72    }
73    // Instance of IconReceiver
74    private final IconReceiver mIconReceiver = new IconReceiver();
75
76    /**
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
80     */
81    private void loadUrl(String url, boolean newWindow) {
82        Intent intent = new Intent().setAction(url);
83        if (newWindow) {
84            Bundle b = new Bundle();
85            b.putBoolean("new_window", true);
86            intent.putExtras(b);
87        }
88        setResultToParent(RESULT_OK, intent);
89        finish();
90    }
91
92    private void copy(CharSequence text) {
93        try {
94            IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
95            if (clip != null) {
96                clip.setClipboardText(text);
97            }
98        } catch (android.os.RemoteException e) {
99            Log.e(LOGTAG, "Copy failed", e);
100        }
101    }
102
103    @Override
104    protected void onCreate(Bundle icicle) {
105        super.onCreate(icicle);
106        setTitle(R.string.browser_history);
107
108        mDateSorter = new DateSorter(this);
109
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() {
121                public void run() {
122                    // In case the history gets cleared before this event
123                    // happens.
124                    if (list.getExpandableListAdapter().getGroupCount() > 0) {
125                        list.expandGroup(0);
126                    }
127                }
128            });
129        }
130        mMaxTabsOpen = getIntent().getBooleanExtra("maxTabsOpen", false);
131        CombinedBookmarkHistoryActivity.getIconListenerSet(getContentResolver())
132                .addListener(mIconReceiver);
133
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);
137    }
138
139    @Override
140    public boolean onCreateOptionsMenu(Menu menu) {
141        super.onCreateOptionsMenu(menu);
142        MenuInflater inflater = getMenuInflater();
143        inflater.inflate(R.menu.history, menu);
144        return true;
145    }
146
147    @Override
148    public boolean onPrepareOptionsMenu(Menu menu) {
149        menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver()));
150        return true;
151    }
152
153    @Override
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
158                // as well
159                Browser.clearHistory(getContentResolver());
160                mAdapter.refreshData();
161                return true;
162
163            default:
164                break;
165        }
166        return super.onOptionsItemSelected(item);
167    }
168
169    @Override
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)) {
176            return;
177        }
178
179        // Inflate the menu
180        MenuInflater inflater = getMenuInflater();
181        inflater.inflate(R.menu.historycontext, menu);
182
183        // Setup the header
184        menu.setHeaderTitle(((HistoryItem)i.targetView).getUrl());
185
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);
188
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);
195
196        super.onCreateContextMenu(menu, v, menuInfo);
197    }
198
199    @Override
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:
207                loadUrl(url, false);
208                return true;
209            case R.id.new_window_context_menu_id:
210                loadUrl(url, true);
211                return true;
212            case R.id.save_to_bookmarks_menu_id:
213                Browser.saveBookmark(this, title, url);
214                return true;
215            case R.id.share_link_context_menu_id:
216                Browser.sendString(this, url);
217                return true;
218            case R.id.copy_context_menu_id:
219                copy(url);
220                return true;
221            case R.id.delete_context_menu_id:
222                Browser.deleteFromHistory(getContentResolver(), url);
223                mAdapter.refreshData();
224                return true;
225            default:
226                break;
227        }
228        return super.onContextItemSelected(item);
229    }
230
231    @Override
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);
236            return true;
237        }
238        return false;
239    }
240
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);
248    }
249
250    private class ChangeObserver extends ContentObserver {
251        public ChangeObserver() {
252            super(new Handler());
253        }
254
255        @Override
256        public boolean deliverSelfNotifications() {
257            return true;
258        }
259
260        @Override
261        public void onChange(boolean selfChange) {
262            mAdapter.refreshData();
263        }
264    }
265
266    private class HistoryAdapter implements ExpandableListAdapter {
267
268        // Array for each of our bins.  Each entry represents how many items are
269        // in that bin.
270        int mItemMap[];
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.
273        int mNumberOfBins;
274        Vector<DataSetObserver> mObservers;
275        Cursor mCursor;
276
277        HistoryAdapter() {
278            mObservers = new Vector<DataSetObserver>();
279
280            String whereClause = Browser.BookmarkColumns.VISITS + " > 0 ";
281            String orderBy = Browser.BookmarkColumns.DATE + " DESC";
282
283            mCursor = managedQuery(
284                    Browser.BOOKMARKS_URI,
285                    Browser.HISTORY_PROJECTION,
286                    whereClause, null, orderBy);
287
288            buildMap();
289            mCursor.registerContentObserver(new ChangeObserver());
290        }
291
292        void refreshData() {
293            mCursor.requery();
294            buildMap();
295            for (DataSetObserver o : mObservers) {
296                o.onChanged();
297            }
298        }
299
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++) {
306                array[j] = 0;
307            }
308            mNumberOfBins = 0;
309            int dateIndex = -1;
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) {
315                        mNumberOfBins++;
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();
321                            break;
322                        }
323                        dateIndex = index;
324                    }
325                    array[dateIndex]++;
326                    mCursor.moveToNext();
327                }
328            }
329            mItemMap = array;
330        }
331
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
335        // to the Adapter.
336        private int groupPositionToArrayPosition(int groupPosition) {
337            if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
338                throw new AssertionError("group position out of range");
339            }
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
343                // conversion
344                // The second statement is in case this method gets called when
345                // the array is empty, in which case the provided groupPosition
346                // will do fine.
347                return groupPosition;
348            }
349            int arrayPosition = -1;
350            while (groupPosition > -1) {
351                arrayPosition++;
352                if (mItemMap[arrayPosition] != 0) {
353                    groupPosition--;
354                }
355            }
356            return arrayPosition;
357        }
358
359        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
360                View convertView, ViewGroup parent) {
361            groupPosition = groupPositionToArrayPosition(groupPosition);
362            HistoryItem item;
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());
371            } else {
372                item = (HistoryItem) convertView;
373            }
374            int index = childPosition;
375            for (int i = 0; i < groupPosition; i++) {
376                index += mItemMap[i];
377            }
378            mCursor.moveToPosition(index);
379            item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
380            String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
381            item.setUrl(url);
382            item.setFavicon(CombinedBookmarkHistoryActivity.getIconListenerSet(
383                    getContentResolver()).getFavicon(url));
384            item.setIsBookmark(1 ==
385                    mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX));
386            return item;
387        }
388
389        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
390            groupPosition = groupPositionToArrayPosition(groupPosition);
391            TextView item;
392            if (null == convertView || !(convertView instanceof TextView)) {
393                LayoutInflater factory =
394                        LayoutInflater.from(BrowserHistoryPage.this);
395                item = (TextView)
396                        factory.inflate(R.layout.history_header, null);
397            } else {
398                item = (TextView) convertView;
399            }
400            item.setText(mDateSorter.getLabel(groupPosition));
401            return item;
402        }
403
404        public boolean areAllItemsEnabled() {
405            return true;
406        }
407
408        public boolean isChildSelectable(int groupPosition, int childPosition) {
409            return true;
410        }
411
412        public int getGroupCount() {
413            return mNumberOfBins;
414        }
415
416        public int getChildrenCount(int groupPosition) {
417            return mItemMap[groupPositionToArrayPosition(groupPosition)];
418        }
419
420        public Object getGroup(int groupPosition) {
421            return null;
422        }
423
424        public Object getChild(int groupPosition, int childPosition) {
425            return null;
426        }
427
428        public long getGroupId(int groupPosition) {
429            return groupPosition;
430        }
431
432        public long getChildId(int groupPosition, int childPosition) {
433            return (childPosition << 3) + groupPosition;
434        }
435
436        public boolean hasStableIds() {
437            return true;
438        }
439
440        public void registerDataSetObserver(DataSetObserver observer) {
441            mObservers.add(observer);
442        }
443
444        public void unregisterDataSetObserver(DataSetObserver observer) {
445            mObservers.remove(observer);
446        }
447
448        public void onGroupExpanded(int groupPosition) {
449
450        }
451
452        public void onGroupCollapsed(int groupPosition) {
453
454        }
455
456        public long getCombinedChildId(long groupId, long childId) {
457            return childId;
458        }
459
460        public long getCombinedGroupId(long groupId) {
461            return groupId;
462        }
463
464        public boolean isEmpty() {
465            return mCursor.getCount() == 0;
466        }
467    }
468}
469