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