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