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.AlertDialog;
21import android.app.Dialog;
22import android.app.Fragment;
23import android.app.FragmentBreadCrumbs;
24import android.app.LoaderManager.LoaderCallbacks;
25import android.content.ClipboardManager;
26import android.content.ContentResolver;
27import android.content.Context;
28import android.content.CursorLoader;
29import android.content.DialogInterface;
30import android.content.Intent;
31import android.content.Loader;
32import android.content.pm.PackageManager;
33import android.content.pm.ResolveInfo;
34import android.database.Cursor;
35import android.database.DataSetObserver;
36import android.graphics.BitmapFactory;
37import android.graphics.drawable.Drawable;
38import android.net.Uri;
39import android.os.Bundle;
40import android.provider.Browser;
41import android.provider.BrowserContract;
42import android.provider.BrowserContract.Combined;
43import android.view.ContextMenu;
44import android.view.ContextMenu.ContextMenuInfo;
45import android.view.LayoutInflater;
46import android.view.Menu;
47import android.view.MenuInflater;
48import android.view.MenuItem;
49import android.view.View;
50import android.view.ViewGroup;
51import android.view.ViewStub;
52import android.widget.AbsListView;
53import android.widget.AdapterView;
54import android.widget.AdapterView.AdapterContextMenuInfo;
55import android.widget.AdapterView.OnItemClickListener;
56import android.widget.BaseAdapter;
57import android.widget.ExpandableListView;
58import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
59import android.widget.ExpandableListView.OnChildClickListener;
60import android.widget.ListView;
61import android.widget.TextView;
62import android.widget.Toast;
63
64/**
65 * Activity for displaying the browser's history, divided into
66 * days of viewing.
67 */
68public class BrowserHistoryPage extends Fragment
69        implements LoaderCallbacks<Cursor>, OnChildClickListener {
70
71    static final int LOADER_HISTORY = 1;
72    static final int LOADER_MOST_VISITED = 2;
73
74    CombinedBookmarksCallbacks mCallback;
75    HistoryAdapter mAdapter;
76    HistoryChildWrapper mChildWrapper;
77    boolean mDisableNewWindow;
78    HistoryItem mContextHeader;
79    String mMostVisitsLimit;
80    ListView mGroupList, mChildList;
81    private ViewGroup mPrefsContainer;
82    private FragmentBreadCrumbs mFragmentBreadCrumbs;
83    private ExpandableListView mHistoryList;
84
85    private View mRoot;
86
87    static interface HistoryQuery {
88        static final String[] PROJECTION = new String[] {
89                Combined._ID, // 0
90                Combined.DATE_LAST_VISITED, // 1
91                Combined.TITLE, // 2
92                Combined.URL, // 3
93                Combined.FAVICON, // 4
94                Combined.VISITS, // 5
95                Combined.IS_BOOKMARK, // 6
96        };
97
98        static final int INDEX_ID = 0;
99        static final int INDEX_DATE_LAST_VISITED = 1;
100        static final int INDEX_TITE = 2;
101        static final int INDEX_URL = 3;
102        static final int INDEX_FAVICON = 4;
103        static final int INDEX_VISITS = 5;
104        static final int INDEX_IS_BOOKMARK = 6;
105    }
106
107    private void copy(CharSequence text) {
108        ClipboardManager cm = (ClipboardManager) getActivity().getSystemService(
109                Context.CLIPBOARD_SERVICE);
110        cm.setText(text);
111    }
112
113    @Override
114    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
115        Uri.Builder combinedBuilder = Combined.CONTENT_URI.buildUpon();
116
117        switch (id) {
118            case LOADER_HISTORY: {
119                String sort = Combined.DATE_LAST_VISITED + " DESC";
120                String where = Combined.VISITS + " > 0";
121                CursorLoader loader = new CursorLoader(getActivity(), combinedBuilder.build(),
122                        HistoryQuery.PROJECTION, where, null, sort);
123                return loader;
124            }
125
126            case LOADER_MOST_VISITED: {
127                Uri uri = combinedBuilder
128                        .appendQueryParameter(BrowserContract.PARAM_LIMIT, mMostVisitsLimit)
129                        .build();
130                String where = Combined.VISITS + " > 0";
131                CursorLoader loader = new CursorLoader(getActivity(), uri,
132                        HistoryQuery.PROJECTION, where, null, Combined.VISITS + " DESC");
133                return loader;
134            }
135
136            default: {
137                throw new IllegalArgumentException();
138            }
139        }
140    }
141
142    void selectGroup(int position) {
143        mGroupItemClickListener.onItemClick(null,
144                mAdapter.getGroupView(position, false, null, null),
145                position, position);
146    }
147
148    void checkIfEmpty() {
149        if (mAdapter.mMostVisited != null && mAdapter.mHistoryCursor != null) {
150            // Both cursors have loaded - check to see if we have data
151            if (mAdapter.isEmpty()) {
152                mRoot.findViewById(R.id.history).setVisibility(View.GONE);
153                mRoot.findViewById(android.R.id.empty).setVisibility(View.VISIBLE);
154            } else {
155                mRoot.findViewById(R.id.history).setVisibility(View.VISIBLE);
156                mRoot.findViewById(android.R.id.empty).setVisibility(View.GONE);
157            }
158        }
159    }
160
161    @Override
162    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
163        switch (loader.getId()) {
164            case LOADER_HISTORY: {
165                mAdapter.changeCursor(data);
166                if (!mAdapter.isEmpty() && mGroupList != null
167                        && mGroupList.getCheckedItemPosition() == ListView.INVALID_POSITION) {
168                    selectGroup(0);
169                }
170
171                checkIfEmpty();
172                break;
173            }
174
175            case LOADER_MOST_VISITED: {
176                mAdapter.changeMostVisitedCursor(data);
177
178                checkIfEmpty();
179                break;
180            }
181
182            default: {
183                throw new IllegalArgumentException();
184            }
185        }
186    }
187
188    @Override
189    public void onLoaderReset(Loader<Cursor> loader) {
190    }
191
192    @Override
193    public void onCreate(Bundle icicle) {
194        super.onCreate(icicle);
195
196        setHasOptionsMenu(true);
197
198        Bundle args = getArguments();
199        mDisableNewWindow = args.getBoolean(BrowserBookmarksPage.EXTRA_DISABLE_WINDOW, false);
200        int mvlimit = getResources().getInteger(R.integer.most_visits_limit);
201        mMostVisitsLimit = Integer.toString(mvlimit);
202        mCallback = (CombinedBookmarksCallbacks) getActivity();
203    }
204
205    @Override
206    public View onCreateView(LayoutInflater inflater, ViewGroup container,
207            Bundle savedInstanceState) {
208        mRoot = inflater.inflate(R.layout.history, container, false);
209        mAdapter = new HistoryAdapter(getActivity());
210        ViewStub stub = (ViewStub) mRoot.findViewById(R.id.pref_stub);
211        if (stub != null) {
212            inflateTwoPane(stub);
213        } else {
214            inflateSinglePane();
215        }
216
217        // Start the loaders
218        getLoaderManager().restartLoader(LOADER_HISTORY, null, this);
219        getLoaderManager().restartLoader(LOADER_MOST_VISITED, null, this);
220
221        return mRoot;
222    }
223
224    private void inflateSinglePane() {
225        mHistoryList = (ExpandableListView) mRoot.findViewById(R.id.history);
226        mHistoryList.setAdapter(mAdapter);
227        mHistoryList.setOnChildClickListener(this);
228        registerForContextMenu(mHistoryList);
229    }
230
231    private void inflateTwoPane(ViewStub stub) {
232        stub.setLayoutResource(R.layout.preference_list_content);
233        stub.inflate();
234        mGroupList = (ListView) mRoot.findViewById(android.R.id.list);
235        mPrefsContainer = (ViewGroup) mRoot.findViewById(R.id.prefs_frame);
236        mFragmentBreadCrumbs = (FragmentBreadCrumbs) mRoot.findViewById(android.R.id.title);
237        mFragmentBreadCrumbs.setMaxVisible(1);
238        mFragmentBreadCrumbs.setActivity(getActivity());
239        mPrefsContainer.setVisibility(View.VISIBLE);
240        mGroupList.setAdapter(new HistoryGroupWrapper(mAdapter));
241        mGroupList.setOnItemClickListener(mGroupItemClickListener);
242        mGroupList.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
243        mChildWrapper = new HistoryChildWrapper(mAdapter);
244        mChildList = new ListView(getActivity());
245        mChildList.setAdapter(mChildWrapper);
246        mChildList.setOnItemClickListener(mChildItemClickListener);
247        registerForContextMenu(mChildList);
248        ViewGroup prefs = (ViewGroup) mRoot.findViewById(R.id.prefs);
249        prefs.addView(mChildList);
250    }
251
252    private OnItemClickListener mGroupItemClickListener = new OnItemClickListener() {
253        @Override
254        public void onItemClick(
255                AdapterView<?> parent, View view, int position, long id) {
256            CharSequence title = ((TextView) view).getText();
257            mFragmentBreadCrumbs.setTitle(title, title);
258            mChildWrapper.setSelectedGroup(position);
259            mGroupList.setItemChecked(position, true);
260        }
261    };
262
263    private OnItemClickListener mChildItemClickListener = new OnItemClickListener() {
264        @Override
265        public void onItemClick(
266                AdapterView<?> parent, View view, int position, long id) {
267            mCallback.openUrl(((HistoryItem) view).getUrl());
268        }
269    };
270
271    @Override
272    public boolean onChildClick(ExpandableListView parent, View view,
273            int groupPosition, int childPosition, long id) {
274        mCallback.openUrl(((HistoryItem) view).getUrl());
275        return true;
276    }
277
278    @Override
279    public void onDestroy() {
280        super.onDestroy();
281        getLoaderManager().destroyLoader(LOADER_HISTORY);
282        getLoaderManager().destroyLoader(LOADER_MOST_VISITED);
283    }
284
285    @Override
286    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
287        super.onCreateOptionsMenu(menu, inflater);
288        inflater.inflate(R.menu.history, menu);
289    }
290
291    void promptToClearHistory() {
292        final ContentResolver resolver = getActivity().getContentResolver();
293        final ClearHistoryTask clear = new ClearHistoryTask(resolver);
294        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
295                .setMessage(R.string.pref_privacy_clear_history_dlg)
296                .setIconAttribute(android.R.attr.alertDialogIcon)
297                .setNegativeButton(R.string.cancel, null)
298                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
299                     @Override
300                     public void onClick(DialogInterface dialog, int which) {
301                         if (which == DialogInterface.BUTTON_POSITIVE) {
302                             clear.start();
303                         }
304                     }
305                });
306        final Dialog dialog = builder.create();
307        dialog.show();
308    }
309
310    @Override
311    public boolean onOptionsItemSelected(MenuItem item) {
312        if (item.getItemId() == R.id.clear_history_menu_id) {
313            promptToClearHistory();
314            return true;
315        }
316        return super.onOptionsItemSelected(item);
317    }
318
319    static class ClearHistoryTask extends Thread {
320        ContentResolver mResolver;
321
322        public ClearHistoryTask(ContentResolver resolver) {
323            mResolver = resolver;
324        }
325
326        @Override
327        public void run() {
328            Browser.clearHistory(mResolver);
329        }
330    }
331
332    View getTargetView(ContextMenuInfo menuInfo) {
333        if (menuInfo instanceof AdapterContextMenuInfo) {
334            return ((AdapterContextMenuInfo) menuInfo).targetView;
335        }
336        if (menuInfo instanceof ExpandableListContextMenuInfo) {
337            return ((ExpandableListContextMenuInfo) menuInfo).targetView;
338        }
339        return null;
340    }
341
342    @Override
343    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
344
345        View targetView = getTargetView(menuInfo);
346        if (!(targetView instanceof HistoryItem)) {
347            return;
348        }
349        HistoryItem historyItem = (HistoryItem) targetView;
350
351        // Inflate the menu
352        Activity parent = getActivity();
353        MenuInflater inflater = parent.getMenuInflater();
354        inflater.inflate(R.menu.historycontext, menu);
355
356        // Setup the header
357        if (mContextHeader == null) {
358            mContextHeader = new HistoryItem(parent, false);
359            mContextHeader.setEnableScrolling(true);
360        } else if (mContextHeader.getParent() != null) {
361            ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader);
362        }
363        historyItem.copyTo(mContextHeader);
364        menu.setHeaderView(mContextHeader);
365
366        // Only show open in new tab if it was not explicitly disabled
367        if (mDisableNewWindow) {
368            menu.findItem(R.id.new_window_context_menu_id).setVisible(false);
369        }
370        // For a bookmark, provide the option to remove it from bookmarks
371        if (historyItem.isBookmark()) {
372            MenuItem item = menu.findItem(R.id.save_to_bookmarks_menu_id);
373            item.setTitle(R.string.remove_from_bookmarks);
374        }
375        // decide whether to show the share link option
376        PackageManager pm = parent.getPackageManager();
377        Intent send = new Intent(Intent.ACTION_SEND);
378        send.setType("text/plain");
379        ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
380        menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null);
381
382        super.onCreateContextMenu(menu, v, menuInfo);
383    }
384
385    @Override
386    public boolean onContextItemSelected(MenuItem item) {
387        ContextMenuInfo menuInfo = item.getMenuInfo();
388        if (menuInfo == null) {
389            return false;
390        }
391        View targetView = getTargetView(menuInfo);
392        if (!(targetView instanceof HistoryItem)) {
393            return false;
394        }
395        HistoryItem historyItem = (HistoryItem) targetView;
396        String url = historyItem.getUrl();
397        String title = historyItem.getName();
398        Activity activity = getActivity();
399        switch (item.getItemId()) {
400            case R.id.open_context_menu_id:
401                mCallback.openUrl(url);
402                return true;
403            case R.id.new_window_context_menu_id:
404                mCallback.openInNewTab(url);
405                return true;
406            case R.id.save_to_bookmarks_menu_id:
407                if (historyItem.isBookmark()) {
408                    Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(),
409                            url, title);
410                } else {
411                    Browser.saveBookmark(activity, title, url);
412                }
413                return true;
414            case R.id.share_link_context_menu_id:
415                Browser.sendString(activity, url,
416                        activity.getText(R.string.choosertitle_sharevia).toString());
417                return true;
418            case R.id.copy_url_context_menu_id:
419                copy(url);
420                return true;
421            case R.id.delete_context_menu_id:
422                Browser.deleteFromHistory(activity.getContentResolver(), url);
423                return true;
424            case R.id.homepage_context_menu_id:
425                BrowserSettings.getInstance().setHomePage(url);
426                Toast.makeText(activity, R.string.homepage_set, Toast.LENGTH_LONG).show();
427                return true;
428            default:
429                break;
430        }
431        return super.onContextItemSelected(item);
432    }
433
434    private static abstract class HistoryWrapper extends BaseAdapter {
435
436        protected HistoryAdapter mAdapter;
437        private DataSetObserver mObserver = new DataSetObserver() {
438            @Override
439            public void onChanged() {
440                super.onChanged();
441                notifyDataSetChanged();
442            }
443
444            @Override
445            public void onInvalidated() {
446                super.onInvalidated();
447                notifyDataSetInvalidated();
448            }
449        };
450
451        public HistoryWrapper(HistoryAdapter adapter) {
452            mAdapter = adapter;
453            mAdapter.registerDataSetObserver(mObserver);
454        }
455
456    }
457    private static class HistoryGroupWrapper extends HistoryWrapper {
458
459        public HistoryGroupWrapper(HistoryAdapter adapter) {
460            super(adapter);
461        }
462
463        @Override
464        public int getCount() {
465            return mAdapter.getGroupCount();
466        }
467
468        @Override
469        public Object getItem(int position) {
470            return null;
471        }
472
473        @Override
474        public long getItemId(int position) {
475            return position;
476        }
477
478        @Override
479        public View getView(int position, View convertView, ViewGroup parent) {
480            return mAdapter.getGroupView(position, false, convertView, parent);
481        }
482
483    }
484
485    private static class HistoryChildWrapper extends HistoryWrapper {
486
487        private int mSelectedGroup;
488
489        public HistoryChildWrapper(HistoryAdapter adapter) {
490            super(adapter);
491        }
492
493        void setSelectedGroup(int groupPosition) {
494            mSelectedGroup = groupPosition;
495            notifyDataSetChanged();
496        }
497
498        @Override
499        public int getCount() {
500            return mAdapter.getChildrenCount(mSelectedGroup);
501        }
502
503        @Override
504        public Object getItem(int position) {
505            return null;
506        }
507
508        @Override
509        public long getItemId(int position) {
510            return position;
511        }
512
513        @Override
514        public View getView(int position, View convertView, ViewGroup parent) {
515            return mAdapter.getChildView(mSelectedGroup, position,
516                    false, convertView, parent);
517        }
518
519    }
520
521    private class HistoryAdapter extends DateSortedExpandableListAdapter {
522
523        private Cursor mMostVisited, mHistoryCursor;
524        Drawable mFaviconBackground;
525
526        HistoryAdapter(Context context) {
527            super(context, HistoryQuery.INDEX_DATE_LAST_VISITED);
528            mFaviconBackground = BookmarkUtils.createListFaviconBackground(context);
529        }
530
531        @Override
532        public void changeCursor(Cursor cursor) {
533            mHistoryCursor = cursor;
534            super.changeCursor(cursor);
535        }
536
537        void changeMostVisitedCursor(Cursor cursor) {
538            if (mMostVisited == cursor) {
539                return;
540            }
541            if (mMostVisited != null) {
542                mMostVisited.unregisterDataSetObserver(mDataSetObserver);
543                mMostVisited.close();
544            }
545            mMostVisited = cursor;
546            if (mMostVisited != null) {
547                mMostVisited.registerDataSetObserver(mDataSetObserver);
548            }
549            notifyDataSetChanged();
550        }
551
552        @Override
553        public long getChildId(int groupPosition, int childPosition) {
554            if (moveCursorToChildPosition(groupPosition, childPosition)) {
555                Cursor cursor = getCursor(groupPosition);
556                return cursor.getLong(HistoryQuery.INDEX_ID);
557            }
558            return 0;
559        }
560
561        @Override
562        public int getGroupCount() {
563            return super.getGroupCount() + (!isMostVisitedEmpty() ? 1 : 0);
564        }
565
566        @Override
567        public int getChildrenCount(int groupPosition) {
568            if (groupPosition >= super.getGroupCount()) {
569                if (isMostVisitedEmpty()) {
570                    return 0;
571                }
572                return mMostVisited.getCount();
573            }
574            return super.getChildrenCount(groupPosition);
575        }
576
577        @Override
578        public boolean isEmpty() {
579            if (!super.isEmpty()) {
580                return false;
581            }
582            return isMostVisitedEmpty();
583        }
584
585        private boolean isMostVisitedEmpty() {
586            return mMostVisited == null
587                    || mMostVisited.isClosed()
588                    || mMostVisited.getCount() == 0;
589        }
590
591        Cursor getCursor(int groupPosition) {
592            if (groupPosition >= super.getGroupCount()) {
593                return mMostVisited;
594            }
595            return mHistoryCursor;
596        }
597
598        @Override
599        public View getGroupView(int groupPosition, boolean isExpanded,
600                View convertView, ViewGroup parent) {
601            if (groupPosition >= super.getGroupCount()) {
602                if (mMostVisited == null || mMostVisited.isClosed()) {
603                    throw new IllegalStateException("Data is not valid");
604                }
605                TextView item;
606                if (null == convertView || !(convertView instanceof TextView)) {
607                    LayoutInflater factory = LayoutInflater.from(getContext());
608                    item = (TextView) factory.inflate(R.layout.history_header, null);
609                } else {
610                    item = (TextView) convertView;
611                }
612                item.setText(R.string.tab_most_visited);
613                return item;
614            }
615            return super.getGroupView(groupPosition, isExpanded, convertView, parent);
616        }
617
618        @Override
619        boolean moveCursorToChildPosition(
620                int groupPosition, int childPosition) {
621            if (groupPosition >= super.getGroupCount()) {
622                if (mMostVisited != null && !mMostVisited.isClosed()) {
623                    mMostVisited.moveToPosition(childPosition);
624                    return true;
625                }
626                return false;
627            }
628            return super.moveCursorToChildPosition(groupPosition, childPosition);
629        }
630
631        @Override
632        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
633                View convertView, ViewGroup parent) {
634            HistoryItem item;
635            if (null == convertView || !(convertView instanceof HistoryItem)) {
636                item = new HistoryItem(getContext());
637                // Add padding on the left so it will be indented from the
638                // arrows on the group views.
639                item.setPadding(item.getPaddingLeft() + 10,
640                        item.getPaddingTop(),
641                        item.getPaddingRight(),
642                        item.getPaddingBottom());
643                item.setFaviconBackground(mFaviconBackground);
644            } else {
645                item = (HistoryItem) convertView;
646            }
647
648            // Bail early if the Cursor is closed.
649            if (!moveCursorToChildPosition(groupPosition, childPosition)) {
650                return item;
651            }
652
653            Cursor cursor = getCursor(groupPosition);
654            item.setName(cursor.getString(HistoryQuery.INDEX_TITE));
655            String url = cursor.getString(HistoryQuery.INDEX_URL);
656            item.setUrl(url);
657            byte[] data = cursor.getBlob(HistoryQuery.INDEX_FAVICON);
658            if (data != null) {
659                item.setFavicon(BitmapFactory.decodeByteArray(data, 0,
660                        data.length));
661            } else {
662                item.setFavicon(null);
663            }
664            item.setIsBookmark(cursor.getInt(HistoryQuery.INDEX_IS_BOOKMARK) == 1);
665            return item;
666        }
667    }
668}
669