BrowserBookmarksPage.java revision 5942df0c38dff7e4335e352e2d03f100b07b8907
1/*
2 * Copyright (C) 2006 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.content.DialogInterface;
22import android.content.Intent;
23import android.content.SharedPreferences;
24import android.content.SharedPreferences.Editor;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.Canvas;
28import android.graphics.Color;
29import android.graphics.Paint;
30import android.graphics.Path;
31import android.graphics.PorterDuff;
32import android.graphics.PorterDuffXfermode;
33import android.graphics.RectF;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.Message;
38import android.os.ServiceManager;
39import android.provider.Browser;
40import android.text.IClipboard;
41import android.util.Log;
42import android.view.ContextMenu;
43import android.view.KeyEvent;
44import android.view.LayoutInflater;
45import android.view.Menu;
46import android.view.MenuInflater;
47import android.view.MenuItem;
48import android.view.View;
49import android.view.ViewGroup;
50import android.view.ViewGroup.LayoutParams;
51import android.view.ViewStub;
52import android.view.ContextMenu.ContextMenuInfo;
53import android.widget.AdapterView;
54import android.widget.GridView;
55import android.widget.ListView;
56import android.widget.Toast;
57
58/*package*/ enum BookmarkViewMode { NONE, GRID, LIST }
59/**
60 *  View showing the user's bookmarks in the browser.
61 */
62public class BrowserBookmarksPage extends Activity implements
63        View.OnCreateContextMenuListener {
64
65    private BookmarkViewMode        mViewMode = BookmarkViewMode.NONE;
66    private GridView                mGridPage;
67    private View                    mVerticalList;
68    private BrowserBookmarksAdapter mBookmarksAdapter;
69    private static final int        BOOKMARKS_SAVE = 1;
70    private boolean                 mDisableNewWindow;
71    private BookmarkItem            mContextHeader;
72    private AddNewBookmark          mAddHeader;
73    private boolean                 mCanceled = false;
74    private boolean                 mCreateShortcut;
75    private boolean                 mMostVisited;
76    private View                    mEmptyView;
77    // XXX: There is no public string defining this intent so if Home changes
78    // the value, we have to update this string.
79    private static final String     INSTALL_SHORTCUT =
80            "com.android.launcher.action.INSTALL_SHORTCUT";
81
82    private final static String LOGTAG = "browser";
83    private final static String PREF_BOOKMARK_VIEW_MODE = "pref_bookmark_view_mode";
84    private final static String PREF_MOST_VISITED_VIEW_MODE = "pref_most_visited_view_mode";
85
86    @Override
87    public boolean onContextItemSelected(MenuItem item) {
88        // It is possible that the view has been canceled when we get to
89        // this point as back has a higher priority
90        if (mCanceled) {
91            return true;
92        }
93        AdapterView.AdapterContextMenuInfo i =
94            (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
95        // If we have no menu info, we can't tell which item was selected.
96        if (i == null) {
97            return true;
98        }
99
100        switch (item.getItemId()) {
101        case R.id.new_context_menu_id:
102            saveCurrentPage();
103            break;
104        case R.id.open_context_menu_id:
105            loadUrl(i.position);
106            break;
107        case R.id.edit_context_menu_id:
108            editBookmark(i.position);
109            break;
110        case R.id.shortcut_context_menu_id:
111            final Intent send = createShortcutIntent(i.position);
112            send.setAction(INSTALL_SHORTCUT);
113            sendBroadcast(send);
114            break;
115        case R.id.delete_context_menu_id:
116            if (mMostVisited) {
117                Browser.deleteFromHistory(getContentResolver(),
118                        getUrl(i.position));
119                refreshList();
120            } else {
121                displayRemoveBookmarkDialog(i.position);
122            }
123            break;
124        case R.id.new_window_context_menu_id:
125            openInNewWindow(i.position);
126            break;
127        case R.id.share_link_context_menu_id:
128            Browser.sendString(BrowserBookmarksPage.this, getUrl(i.position));
129            break;
130        case R.id.copy_url_context_menu_id:
131            copy(getUrl(i.position));
132            break;
133        case R.id.homepage_context_menu_id:
134            BrowserSettings.getInstance().setHomePage(this,
135                    getUrl(i.position));
136            Toast.makeText(this, R.string.homepage_set,
137                    Toast.LENGTH_LONG).show();
138            break;
139        // Only for the Most visited page
140        case R.id.save_to_bookmarks_menu_id:
141            boolean isBookmark;
142            String name;
143            String url;
144            if (mViewMode == BookmarkViewMode.GRID) {
145                isBookmark = mBookmarksAdapter.getIsBookmark(i.position);
146                name = mBookmarksAdapter.getTitle(i.position);
147                url = mBookmarksAdapter.getUrl(i.position);
148            } else {
149                HistoryItem historyItem = ((HistoryItem) i.targetView);
150                isBookmark = historyItem.isBookmark();
151                name = historyItem.getName();
152                url = historyItem.getUrl();
153            }
154            // If the site is bookmarked, the item becomes remove from
155            // bookmarks.
156            if (isBookmark) {
157                Bookmarks.removeFromBookmarks(this, getContentResolver(), url);
158            } else {
159                Browser.saveBookmark(this, name, url);
160            }
161            break;
162        default:
163            return super.onContextItemSelected(item);
164        }
165        return true;
166    }
167
168    @Override
169    public void onCreateContextMenu(ContextMenu menu, View v,
170                ContextMenuInfo menuInfo) {
171            AdapterView.AdapterContextMenuInfo i =
172                    (AdapterView.AdapterContextMenuInfo) menuInfo;
173
174            MenuInflater inflater = getMenuInflater();
175            if (mMostVisited) {
176                inflater.inflate(R.menu.historycontext, menu);
177            } else {
178                inflater.inflate(R.menu.bookmarkscontext, menu);
179            }
180
181            if (0 == i.position && !mMostVisited) {
182                menu.setGroupVisible(R.id.CONTEXT_MENU, false);
183                if (mAddHeader == null) {
184                    mAddHeader = new AddNewBookmark(BrowserBookmarksPage.this);
185                } else if (mAddHeader.getParent() != null) {
186                    ((ViewGroup) mAddHeader.getParent()).
187                            removeView(mAddHeader);
188                }
189                mAddHeader.setUrl(getIntent().getStringExtra("url"));
190                menu.setHeaderView(mAddHeader);
191                return;
192            }
193            if (mMostVisited) {
194                if ((mViewMode == BookmarkViewMode.LIST
195                        && ((HistoryItem) i.targetView).isBookmark())
196                        || mBookmarksAdapter.getIsBookmark(i.position)) {
197                    MenuItem item = menu.findItem(
198                            R.id.save_to_bookmarks_menu_id);
199                    item.setTitle(R.string.remove_from_bookmarks);
200                }
201            } else {
202                // The historycontext menu has no ADD_MENU group.
203                menu.setGroupVisible(R.id.ADD_MENU, false);
204            }
205            if (mDisableNewWindow) {
206                menu.findItem(R.id.new_window_context_menu_id).setVisible(
207                        false);
208            }
209            if (mContextHeader == null) {
210                mContextHeader = new BookmarkItem(BrowserBookmarksPage.this);
211            } else if (mContextHeader.getParent() != null) {
212                ((ViewGroup) mContextHeader.getParent()).
213                        removeView(mContextHeader);
214            }
215            if (mViewMode == BookmarkViewMode.GRID) {
216                mBookmarksAdapter.populateBookmarkItem(mContextHeader,
217                        i.position);
218            } else {
219                BookmarkItem b = (BookmarkItem) i.targetView;
220                b.copyTo(mContextHeader);
221            }
222            menu.setHeaderView(mContextHeader);
223        }
224
225    /**
226     *  Create a new BrowserBookmarksPage.
227     */
228    @Override
229    protected void onCreate(Bundle icicle) {
230        super.onCreate(icicle);
231
232        if (Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) {
233            mCreateShortcut = true;
234        }
235        mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window",
236                false);
237        mMostVisited = getIntent().getBooleanExtra("mostVisited", false);
238
239        if (mCreateShortcut) {
240            setTitle(R.string.browser_bookmarks_page_bookmarks_text);
241        }
242        mBookmarksAdapter = new BrowserBookmarksAdapter(this,
243                        getIntent().getStringExtra("url"),
244                        getIntent().getStringExtra("title"), mCreateShortcut,
245                        mMostVisited);
246
247        setContentView(R.layout.empty_history);
248        mEmptyView = findViewById(R.id.empty_view);
249        mEmptyView.setVisibility(View.GONE);
250
251        SharedPreferences p = getPreferences(MODE_PRIVATE);
252
253        // See if the user has set a preference for the view mode of their
254        // bookmarks. Otherwise default to grid mode.
255        BookmarkViewMode preference = BookmarkViewMode.NONE;
256        if (mMostVisited) {
257            preference = BookmarkViewMode.values()[p.getInt(
258                    PREF_MOST_VISITED_VIEW_MODE,
259                    BookmarkViewMode.GRID.ordinal())];
260        } else {
261            preference = BookmarkViewMode.values()[p.getInt(
262                    PREF_BOOKMARK_VIEW_MODE, BookmarkViewMode.GRID.ordinal())];
263        }
264        switchViewMode(preference);
265    }
266
267    /**
268     *  Set the ContentView to be either the grid of thumbnails or the vertical
269     *  list.
270     */
271    private void switchViewMode(BookmarkViewMode gridMode) {
272        if (mViewMode == gridMode) {
273            return;
274        }
275
276        mViewMode = gridMode;
277
278        // Update the preferences to make the new view mode sticky.
279        Editor ed = getPreferences(MODE_PRIVATE).edit();
280        if (mMostVisited) {
281            ed.putInt(PREF_MOST_VISITED_VIEW_MODE, mViewMode.ordinal());
282        } else {
283            ed.putInt(PREF_BOOKMARK_VIEW_MODE, mViewMode.ordinal());
284        }
285        ed.commit();
286
287        mBookmarksAdapter.switchViewMode(gridMode);
288        if (mViewMode == BookmarkViewMode.GRID) {
289            if (mGridPage == null) {
290                mGridPage = new GridView(this);
291                mGridPage.setAdapter(mBookmarksAdapter);
292                mGridPage.setOnItemClickListener(mListener);
293                mGridPage.setNumColumns(GridView.AUTO_FIT);
294                // Keep this in sync with bookmark_thumb and
295                // BrowserActivity.updateScreenshot
296                mGridPage.setColumnWidth(100);
297                mGridPage.setFocusable(true);
298                mGridPage.setFocusableInTouchMode(true);
299                mGridPage.setSelector(android.R.drawable.gallery_thumb);
300                mGridPage.setVerticalSpacing(10);
301                if (mMostVisited) {
302                    mGridPage.setEmptyView(mEmptyView);
303                }
304                if (!mCreateShortcut) {
305                    mGridPage.setOnCreateContextMenuListener(this);
306                }
307            }
308            addContentView(mGridPage, FULL_SCREEN_PARAMS);
309            if (mVerticalList != null) {
310                ViewGroup parent = (ViewGroup) mVerticalList.getParent();
311                if (parent != null) {
312                    parent.removeView(mVerticalList);
313                }
314            }
315        } else {
316            if (null == mVerticalList) {
317                ListView listView = new ListView(this);
318                listView.setAdapter(mBookmarksAdapter);
319                listView.setDrawSelectorOnTop(false);
320                listView.setVerticalScrollBarEnabled(true);
321                listView.setOnItemClickListener(mListener);
322                if (mMostVisited) {
323                    listView.setEmptyView(mEmptyView);
324                }
325                if (!mCreateShortcut) {
326                    listView.setOnCreateContextMenuListener(this);
327                }
328                mVerticalList = listView;
329            }
330            addContentView(mVerticalList, FULL_SCREEN_PARAMS);
331            if (mGridPage != null) {
332                ViewGroup parent = (ViewGroup) mGridPage.getParent();
333                if (parent != null) {
334                    parent.removeView(mGridPage);
335                }
336            }
337        }
338    }
339
340    private static final ViewGroup.LayoutParams FULL_SCREEN_PARAMS
341            = new ViewGroup.LayoutParams(
342            ViewGroup.LayoutParams.FILL_PARENT,
343            ViewGroup.LayoutParams.FILL_PARENT);
344
345    private static final int SAVE_CURRENT_PAGE = 1000;
346    private final Handler mHandler = new Handler() {
347        @Override
348        public void handleMessage(Message msg) {
349            if (msg.what == SAVE_CURRENT_PAGE) {
350                saveCurrentPage();
351            }
352        }
353    };
354
355    private AdapterView.OnItemClickListener mListener = new AdapterView.OnItemClickListener() {
356        public void onItemClick(AdapterView parent, View v, int position, long id) {
357            // It is possible that the view has been canceled when we get to
358            // this point as back has a higher priority
359            if (mCanceled) {
360                android.util.Log.e(LOGTAG, "item clicked when dismissing");
361                return;
362            }
363            if (!mCreateShortcut) {
364                if (0 == position && !mMostVisited) {
365                    // XXX: Work-around for a framework issue.
366                    mHandler.sendEmptyMessage(SAVE_CURRENT_PAGE);
367                } else {
368                    loadUrl(position);
369                }
370            } else {
371                final Intent intent = createShortcutIntent(position);
372                setResultToParent(RESULT_OK, intent);
373                finish();
374            }
375        }
376    };
377
378    private Intent createShortcutIntent(int position) {
379        String url = getUrl(position);
380        String title = getBookmarkTitle(position);
381        Bitmap touchIcon = getTouchIcon(position);
382
383        final Intent i = new Intent();
384        final Intent shortcutIntent = new Intent(Intent.ACTION_VIEW,
385                Uri.parse(url));
386        long urlHash = url.hashCode();
387        long uniqueId = (urlHash << 32) | shortcutIntent.hashCode();
388        shortcutIntent.putExtra(Browser.EXTRA_APPLICATION_ID,
389                Long.toString(uniqueId));
390        i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
391        i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
392        // Use the apple-touch-icon if available
393        if (touchIcon != null) {
394            // Make a copy so we can modify the pixels.
395            Bitmap copy = touchIcon.copy(Bitmap.Config.ARGB_8888, true);
396            Canvas canvas = new Canvas(copy);
397
398            // Construct a path from a round rect. This will allow drawing with
399            // an inverse fill so we can punch a hole using the round rect.
400            Path path = new Path();
401            path.setFillType(Path.FillType.INVERSE_WINDING);
402            path.addRoundRect(new RectF(0, 0, touchIcon.getWidth(),
403                    touchIcon.getHeight()), 8f, 8f, Path.Direction.CW);
404
405            // Construct a paint that clears the outside of the rectangle and
406            // draw.
407            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
408            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
409            canvas.drawPath(path, paint);
410
411            i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy);
412        } else {
413            Bitmap favicon = getFavicon(position);
414            if (favicon == null) {
415                i.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
416                        Intent.ShortcutIconResource.fromContext(
417                                BrowserBookmarksPage.this,
418                                R.drawable.ic_launcher_shortcut_browser_bookmark));
419            } else {
420                Bitmap icon = BitmapFactory.decodeResource(getResources(),
421                        R.drawable.ic_launcher_shortcut_browser_bookmark);
422
423                // Make a copy of the regular icon so we can modify the pixels.
424                Bitmap copy = icon.copy(Bitmap.Config.ARGB_8888, true);
425                Canvas canvas = new Canvas(copy);
426
427                // Make a Paint for the white background rectangle and for
428                // filtering the favicon.
429                Paint p = new Paint(Paint.ANTI_ALIAS_FLAG
430                        | Paint.FILTER_BITMAP_FLAG);
431                p.setStyle(Paint.Style.FILL_AND_STROKE);
432                p.setColor(Color.WHITE);
433
434                // Create a rectangle that is slightly wider than the favicon
435                final float iconSize = 16; // 16x16 favicon
436                final float padding = 2;   // white padding around icon
437                final float rectSize = iconSize + 2 * padding;
438                final float y = icon.getHeight() - rectSize;
439                RectF r = new RectF(0, y, rectSize, y + rectSize);
440
441                // Draw a white rounded rectangle behind the favicon
442                canvas.drawRoundRect(r, 2, 2, p);
443
444                // Draw the favicon in the same rectangle as the rounded
445                // rectangle but inset by the padding
446                // (results in a 16x16 favicon).
447                r.inset(padding, padding);
448                canvas.drawBitmap(favicon, null, r, p);
449                i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy);
450            }
451        }
452        // Do not allow duplicate items
453        i.putExtra("duplicate", false);
454        return i;
455    }
456
457    private void saveCurrentPage() {
458        Intent i = new Intent(BrowserBookmarksPage.this,
459                AddBookmarkPage.class);
460        i.putExtras(getIntent());
461        startActivityForResult(i, BOOKMARKS_SAVE);
462    }
463
464    private void loadUrl(int position) {
465        Intent intent = (new Intent()).setAction(getUrl(position));
466        setResultToParent(RESULT_OK, intent);
467        finish();
468    }
469
470    @Override
471    public boolean onCreateOptionsMenu(Menu menu) {
472        boolean result = super.onCreateOptionsMenu(menu);
473        if (!mCreateShortcut) {
474            MenuInflater inflater = getMenuInflater();
475            inflater.inflate(R.menu.bookmarks, menu);
476            // Most visited page does not have an option to bookmark the last
477            // viewed page.
478            menu.findItem(R.id.new_context_menu_id).setVisible(!mMostVisited);
479            return true;
480        }
481        return result;
482    }
483
484    @Override
485    public boolean onPrepareOptionsMenu(Menu menu) {
486        boolean result = super.onPrepareOptionsMenu(menu);
487        if (mCreateShortcut || mBookmarksAdapter.getCount() == 0) {
488            // No need to show the menu if there are no items.
489            return result;
490        }
491        menu.findItem(R.id.switch_mode_menu_id).setTitle(
492                mViewMode == BookmarkViewMode.GRID ? R.string.switch_to_list
493                : R.string.switch_to_thumbnails);
494        return true;
495    }
496
497    @Override
498    public boolean onOptionsItemSelected(MenuItem item) {
499        switch (item.getItemId()) {
500        case R.id.new_context_menu_id:
501            saveCurrentPage();
502            break;
503
504        case R.id.switch_mode_menu_id:
505            if (mViewMode == BookmarkViewMode.GRID) {
506                switchViewMode(BookmarkViewMode.LIST);
507            } else {
508                switchViewMode(BookmarkViewMode.GRID);
509            }
510            break;
511
512        default:
513            return super.onOptionsItemSelected(item);
514        }
515        return true;
516    }
517
518    private void openInNewWindow(int position) {
519        Bundle b = new Bundle();
520        b.putBoolean("new_window", true);
521        setResultToParent(RESULT_OK,
522                (new Intent()).setAction(getUrl(position)).putExtras(b));
523
524        finish();
525    }
526
527
528    private void editBookmark(int position) {
529        Intent intent = new Intent(BrowserBookmarksPage.this,
530            AddBookmarkPage.class);
531        intent.putExtra("bookmark", getRow(position));
532        startActivityForResult(intent, BOOKMARKS_SAVE);
533    }
534
535    @Override
536    protected void onActivityResult(int requestCode, int resultCode,
537                                    Intent data) {
538        switch(requestCode) {
539            case BOOKMARKS_SAVE:
540                if (resultCode == RESULT_OK) {
541                    Bundle extras;
542                    if (data != null && (extras = data.getExtras()) != null) {
543                        // If there are extras, then we need to save
544                        // the edited bookmark. This is done in updateRow()
545                        String title = extras.getString("title");
546                        String url = extras.getString("url");
547                        if (title != null && url != null) {
548                            mBookmarksAdapter.updateRow(extras);
549                        }
550                    } else {
551                        // extras == null then a new bookmark was added to
552                        // the database.
553                        refreshList();
554                    }
555                }
556                break;
557            default:
558                break;
559        }
560    }
561
562    private void displayRemoveBookmarkDialog(int position) {
563        // Put up a dialog asking if the user really wants to
564        // delete the bookmark
565        final int deletePos = position;
566        new AlertDialog.Builder(this)
567                .setTitle(R.string.delete_bookmark)
568                .setIcon(android.R.drawable.ic_dialog_alert)
569                .setMessage(getText(R.string.delete_bookmark_warning).toString().replace(
570                        "%s", getBookmarkTitle(deletePos)))
571                .setPositiveButton(R.string.ok,
572                        new DialogInterface.OnClickListener() {
573                            public void onClick(DialogInterface dialog, int whichButton) {
574                                deleteBookmark(deletePos);
575                            }
576                        })
577                .setNegativeButton(R.string.cancel, null)
578                .show();
579    }
580
581    /**
582     *  Refresh the shown list after the database has changed.
583     */
584    private void refreshList() {
585        mBookmarksAdapter.refreshList();
586    }
587
588    /**
589     *  Return a hashmap representing the currently highlighted row.
590     */
591    public Bundle getRow(int position) {
592        return mBookmarksAdapter.getRow(position);
593    }
594
595    /**
596     *  Return the url of the currently highlighted row.
597     */
598    public String getUrl(int position) {
599        return mBookmarksAdapter.getUrl(position);
600    }
601
602    /**
603     * Return the favicon of the currently highlighted row.
604     */
605    public Bitmap getFavicon(int position) {
606        return mBookmarksAdapter.getFavicon(position);
607    }
608
609    private Bitmap getTouchIcon(int position) {
610        return mBookmarksAdapter.getTouchIcon(position);
611    }
612
613    private void copy(CharSequence text) {
614        try {
615            IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
616            if (clip != null) {
617                clip.setClipboardText(text);
618            }
619        } catch (android.os.RemoteException e) {
620            Log.e(LOGTAG, "Copy failed", e);
621        }
622    }
623
624    public String getBookmarkTitle(int position) {
625        return mBookmarksAdapter.getTitle(position);
626    }
627
628    /**
629     *  Delete the currently highlighted row.
630     */
631    public void deleteBookmark(int position) {
632        mBookmarksAdapter.deleteRow(position);
633    }
634
635    @Override
636    public void onBackPressed() {
637        setResultToParent(RESULT_CANCELED, null);
638        mCanceled = true;
639        super.onBackPressed();
640    }
641
642    // This Activity is generally a sub-Activity of CombinedHistoryActivity. In
643    // that situation, we need to pass our result code up to our parent.
644    // However, if someone calls this Activity directly, then this has no
645    // parent, and it needs to set it on itself.
646    private void setResultToParent(int resultCode, Intent data) {
647        Activity a = getParent() == null ? this : getParent();
648        a.setResult(resultCode, data);
649    }
650}
651