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