blob: 0fc26430264083d9557053dcc0158ee630ec96a2 [file] [log] [blame]
The Android Open Source Project0c908882009-03-03 19:32:16 -08001/*
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;
Patrick Scotte09761e2009-03-24 20:43:37 -070024import android.graphics.BitmapFactory;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Paint;
Patrick Scott3918d442009-08-04 13:22:29 -040028import android.graphics.Path;
29import android.graphics.PorterDuff;
30import android.graphics.PorterDuffXfermode;
Patrick Scotte09761e2009-03-24 20:43:37 -070031import android.graphics.RectF;
The Android Open Source Project0c908882009-03-03 19:32:16 -080032import 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;
Leon Scroggins892df312009-07-14 14:48:02 -040042import android.view.LayoutInflater;
The Android Open Source Project0c908882009-03-03 19:32:16 -080043import android.view.Menu;
44import android.view.MenuInflater;
45import android.view.MenuItem;
46import android.view.View;
47import android.view.ViewGroup;
48import android.view.ContextMenu.ContextMenuInfo;
49import android.widget.AdapterView;
Leon Scroggins89c6d362009-07-15 16:54:37 -040050import android.widget.GridView;
The Android Open Source Project0c908882009-03-03 19:32:16 -080051import android.widget.ListView;
Leon Scrogginsfeb941d2009-05-28 17:27:38 -040052import android.widget.Toast;
The Android Open Source Project0c908882009-03-03 19:32:16 -080053
54/**
55 * View showing the user's bookmarks in the browser.
56 */
57public class BrowserBookmarksPage extends Activity implements
58 View.OnCreateContextMenuListener {
59
Leon Scroggins892df312009-07-14 14:48:02 -040060 private boolean mGridMode;
Leon Scroggins89c6d362009-07-15 16:54:37 -040061 private GridView mGridPage;
Leon Scroggins892df312009-07-14 14:48:02 -040062 private View mVerticalList;
The Android Open Source Project0c908882009-03-03 19:32:16 -080063 private BrowserBookmarksAdapter mBookmarksAdapter;
64 private static final int BOOKMARKS_SAVE = 1;
65 private boolean mMaxTabsOpen;
66 private BookmarkItem mContextHeader;
67 private AddNewBookmark mAddHeader;
68 private boolean mCanceled = false;
69 private boolean mCreateShortcut;
70 // XXX: There is no public string defining this intent so if Home changes
71 // the value, we have to update this string.
72 private static final String INSTALL_SHORTCUT =
73 "com.android.launcher.action.INSTALL_SHORTCUT";
74
75 private final static String LOGTAG = "browser";
76
77
78 @Override
79 public boolean onContextItemSelected(MenuItem item) {
80 // It is possible that the view has been canceled when we get to
81 // this point as back has a higher priority
82 if (mCanceled) {
83 return true;
84 }
85 AdapterView.AdapterContextMenuInfo i =
86 (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
87 // If we have no menu info, we can't tell which item was selected.
88 if (i == null) {
89 return true;
90 }
91
92 switch (item.getItemId()) {
93 case R.id.new_context_menu_id:
94 saveCurrentPage();
95 break;
96 case R.id.open_context_menu_id:
97 loadUrl(i.position);
98 break;
99 case R.id.edit_context_menu_id:
100 editBookmark(i.position);
101 break;
102 case R.id.shortcut_context_menu_id:
Patrick Scott3918d442009-08-04 13:22:29 -0400103 final Intent send = createShortcutIntent(i.position);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800104 send.setAction(INSTALL_SHORTCUT);
105 sendBroadcast(send);
106 break;
107 case R.id.delete_context_menu_id:
108 displayRemoveBookmarkDialog(i.position);
109 break;
110 case R.id.new_window_context_menu_id:
111 openInNewWindow(i.position);
112 break;
113 case R.id.send_context_menu_id:
114 Browser.sendString(BrowserBookmarksPage.this, getUrl(i.position));
115 break;
116 case R.id.copy_url_context_menu_id:
117 copy(getUrl(i.position));
Leon Scrogginsfeb941d2009-05-28 17:27:38 -0400118 break;
119 case R.id.homepage_context_menu_id:
120 BrowserSettings.getInstance().setHomePage(this,
121 getUrl(i.position));
122 Toast.makeText(this, R.string.homepage_set,
123 Toast.LENGTH_LONG).show();
124 break;
The Android Open Source Project0c908882009-03-03 19:32:16 -0800125 default:
126 return super.onContextItemSelected(item);
127 }
128 return true;
129 }
130
131 @Override
132 public void onCreateContextMenu(ContextMenu menu, View v,
133 ContextMenuInfo menuInfo) {
134 AdapterView.AdapterContextMenuInfo i =
135 (AdapterView.AdapterContextMenuInfo) menuInfo;
136
137 MenuInflater inflater = getMenuInflater();
138 inflater.inflate(R.menu.bookmarkscontext, menu);
139
140 if (0 == i.position) {
141 menu.setGroupVisible(R.id.CONTEXT_MENU, false);
142 if (mAddHeader == null) {
143 mAddHeader = new AddNewBookmark(BrowserBookmarksPage.this);
144 } else if (mAddHeader.getParent() != null) {
145 ((ViewGroup) mAddHeader.getParent()).
146 removeView(mAddHeader);
147 }
Leon Scroggins892df312009-07-14 14:48:02 -0400148 mAddHeader.setUrl(getIntent().getStringExtra("url"));
The Android Open Source Project0c908882009-03-03 19:32:16 -0800149 menu.setHeaderView(mAddHeader);
150 return;
151 }
152 menu.setGroupVisible(R.id.ADD_MENU, false);
Leon Scroggins892df312009-07-14 14:48:02 -0400153 if (mMaxTabsOpen) {
154 menu.findItem(R.id.new_window_context_menu_id).setVisible(
155 false);
156 }
The Android Open Source Project0c908882009-03-03 19:32:16 -0800157 if (mContextHeader == null) {
158 mContextHeader = new BookmarkItem(BrowserBookmarksPage.this);
159 } else if (mContextHeader.getParent() != null) {
160 ((ViewGroup) mContextHeader.getParent()).
161 removeView(mContextHeader);
162 }
Leon Scroggins892df312009-07-14 14:48:02 -0400163 if (mGridMode) {
164 mBookmarksAdapter.populateBookmarkItem(mContextHeader,
165 i.position);
166 } else {
167 BookmarkItem b = (BookmarkItem) i.targetView;
168 b.copyTo(mContextHeader);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800169 }
Leon Scroggins892df312009-07-14 14:48:02 -0400170 menu.setHeaderView(mContextHeader);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800171 }
172
173 /**
174 * Create a new BrowserBookmarksPage.
175 */
176 @Override
177 protected void onCreate(Bundle icicle) {
178 super.onCreate(icicle);
179
The Android Open Source Project0c908882009-03-03 19:32:16 -0800180 if (Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) {
181 mCreateShortcut = true;
182 }
The Android Open Source Project0c908882009-03-03 19:32:16 -0800183 mMaxTabsOpen = getIntent().getBooleanExtra("maxTabsOpen", false);
184
Leon Scroggins892df312009-07-14 14:48:02 -0400185 setTitle(R.string.browser_bookmarks_page_bookmarks_text);
186 mBookmarksAdapter = new BrowserBookmarksAdapter(this,
Leon Scroggins89c6d362009-07-15 16:54:37 -0400187 getIntent().getStringExtra("url"),
188 getIntent().getStringExtra("title"), mCreateShortcut);
189 switchViewMode(true);
Leon Scroggins892df312009-07-14 14:48:02 -0400190 }
The Android Open Source Project0c908882009-03-03 19:32:16 -0800191
Leon Scroggins892df312009-07-14 14:48:02 -0400192 /**
193 * Set the ContentView to be either the grid of thumbnails or the vertical
194 * list. Pass true to set it to the grid.
195 */
196 private void switchViewMode(boolean gridMode) {
197 mGridMode = gridMode;
198 mBookmarksAdapter.switchViewMode(gridMode);
199 if (mGridMode) {
200 if (mGridPage == null) {
Leon Scroggins89c6d362009-07-15 16:54:37 -0400201 mGridPage = new GridView(this);
202 mGridPage.setAdapter(mBookmarksAdapter);
Leon Scroggins892df312009-07-14 14:48:02 -0400203 mGridPage.setOnItemClickListener(mListener);
Leon Scroggins89c6d362009-07-15 16:54:37 -0400204 mGridPage.setNumColumns(GridView.AUTO_FIT);
205 // Keep this in sync with bookmark_thumb and
206 // BrowserActivity.updateScreenshot
207 mGridPage.setColumnWidth(100);
208 mGridPage.setFocusable(true);
209 mGridPage.setFocusableInTouchMode(true);
210 mGridPage.setSelector(android.R.drawable.gallery_thumb);
211 mGridPage.setVerticalSpacing(10);
Leon Scroggins892df312009-07-14 14:48:02 -0400212 if (!mCreateShortcut) {
213 mGridPage.setOnCreateContextMenuListener(this);
214 }
215 }
216 setContentView(mGridPage);
217 } else {
218 if (null == mVerticalList) {
219 LayoutInflater factory = LayoutInflater.from(this);
220 mVerticalList = factory.inflate(R.layout.browser_bookmarks_page,
221 null);
222
223 ListView listView
224 = (ListView) mVerticalList.findViewById(R.id.list);
225 listView.setAdapter(mBookmarksAdapter);
226 listView.setDrawSelectorOnTop(false);
227 listView.setVerticalScrollBarEnabled(true);
228 listView.setOnItemClickListener(mListener);
229
230 if (!mCreateShortcut) {
231 listView.setOnCreateContextMenuListener(this);
232 }
233 }
234 setContentView(mVerticalList);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800235 }
236 }
237
238 private static final int SAVE_CURRENT_PAGE = 1000;
239 private final Handler mHandler = new Handler() {
240 @Override
241 public void handleMessage(Message msg) {
242 if (msg.what == SAVE_CURRENT_PAGE) {
243 saveCurrentPage();
244 }
245 }
246 };
247
248 private AdapterView.OnItemClickListener mListener = new AdapterView.OnItemClickListener() {
249 public void onItemClick(AdapterView parent, View v, int position, long id) {
250 // It is possible that the view has been canceled when we get to
251 // this point as back has a higher priority
252 if (mCanceled) {
Leon Scroggins892df312009-07-14 14:48:02 -0400253 android.util.Log.e(LOGTAG, "item clicked when dismissing");
The Android Open Source Project0c908882009-03-03 19:32:16 -0800254 return;
255 }
256 if (!mCreateShortcut) {
257 if (0 == position) {
258 // XXX: Work-around for a framework issue.
259 mHandler.sendEmptyMessage(SAVE_CURRENT_PAGE);
260 } else {
261 loadUrl(position);
262 }
263 } else {
Patrick Scott3918d442009-08-04 13:22:29 -0400264 final Intent intent = createShortcutIntent(position);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800265 setResultToParent(RESULT_OK, intent);
266 finish();
267 }
268 }
269 };
270
Patrick Scott3918d442009-08-04 13:22:29 -0400271 private Intent createShortcutIntent(int position) {
272 String url = getUrl(position);
273 String title = getBookmarkTitle(position);
274 Bitmap touchIcon = getTouchIcon(position);
275
The Android Open Source Project0c908882009-03-03 19:32:16 -0800276 final Intent i = new Intent();
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700277 final Intent shortcutIntent = new Intent(Intent.ACTION_VIEW,
278 Uri.parse(url));
279 long urlHash = url.hashCode();
280 long uniqueId = (urlHash << 32) | shortcutIntent.hashCode();
281 shortcutIntent.putExtra(Browser.EXTRA_APPLICATION_ID,
282 Long.toString(uniqueId));
283 i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800284 i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
Patrick Scott3918d442009-08-04 13:22:29 -0400285 // Use the apple-touch-icon if available
286 if (touchIcon != null) {
287 // Make a copy so we can modify the pixels.
288 Bitmap copy = touchIcon.copy(Bitmap.Config.ARGB_8888, true);
Patrick Scotte09761e2009-03-24 20:43:37 -0700289 Canvas canvas = new Canvas(copy);
290
Patrick Scott3918d442009-08-04 13:22:29 -0400291 // Construct a path from a round rect. This will allow drawing with
292 // an inverse fill so we can punch a hole using the round rect.
293 Path path = new Path();
294 path.setFillType(Path.FillType.INVERSE_WINDING);
295 path.addRoundRect(new RectF(0, 0, touchIcon.getWidth(),
296 touchIcon.getHeight()), 8f, 8f, Path.Direction.CW);
Patrick Scotte09761e2009-03-24 20:43:37 -0700297
Patrick Scott3918d442009-08-04 13:22:29 -0400298 // Construct a paint that clears the outside of the rectangle and
299 // draw.
300 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
301 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
302 canvas.drawPath(path, paint);
Patrick Scotte09761e2009-03-24 20:43:37 -0700303
Patrick Scotte09761e2009-03-24 20:43:37 -0700304 i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy);
Patrick Scott3918d442009-08-04 13:22:29 -0400305 } else {
306 Bitmap favicon = getFavicon(position);
307 if (favicon == null) {
308 i.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
309 Intent.ShortcutIconResource.fromContext(
310 BrowserBookmarksPage.this,
311 R.drawable.ic_launcher_shortcut_browser_bookmark));
312 } else {
313 Bitmap icon = BitmapFactory.decodeResource(getResources(),
314 R.drawable.ic_launcher_shortcut_browser_bookmark);
315
316 // Make a copy of the regular icon so we can modify the pixels.
317 Bitmap copy = icon.copy(Bitmap.Config.ARGB_8888, true);
318 Canvas canvas = new Canvas(copy);
319
320 // Make a Paint for the white background rectangle and for
321 // filtering the favicon.
322 Paint p = new Paint(Paint.ANTI_ALIAS_FLAG
323 | Paint.FILTER_BITMAP_FLAG);
324 p.setStyle(Paint.Style.FILL_AND_STROKE);
325 p.setColor(Color.WHITE);
326
327 // Create a rectangle that is slightly wider than the favicon
328 final float iconSize = 16; // 16x16 favicon
329 final float padding = 2; // white padding around icon
330 final float rectSize = iconSize + 2 * padding;
331 final float y = icon.getHeight() - rectSize;
332 RectF r = new RectF(0, y, rectSize, y + rectSize);
333
334 // Draw a white rounded rectangle behind the favicon
335 canvas.drawRoundRect(r, 2, 2, p);
336
337 // Draw the favicon in the same rectangle as the rounded
338 // rectangle but inset by the padding
339 // (results in a 16x16 favicon).
340 r.inset(padding, padding);
341 canvas.drawBitmap(favicon, null, r, p);
342 i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy);
343 }
Patrick Scotte09761e2009-03-24 20:43:37 -0700344 }
The Android Open Source Project0c908882009-03-03 19:32:16 -0800345 // Do not allow duplicate items
346 i.putExtra("duplicate", false);
347 return i;
348 }
349
350 private void saveCurrentPage() {
351 Intent i = new Intent(BrowserBookmarksPage.this,
352 AddBookmarkPage.class);
353 i.putExtras(getIntent());
354 startActivityForResult(i, BOOKMARKS_SAVE);
355 }
356
357 private void loadUrl(int position) {
358 Intent intent = (new Intent()).setAction(getUrl(position));
359 setResultToParent(RESULT_OK, intent);
360 finish();
361 }
362
363 @Override
364 public boolean onCreateOptionsMenu(Menu menu) {
365 boolean result = super.onCreateOptionsMenu(menu);
366 if (!mCreateShortcut) {
367 MenuInflater inflater = getMenuInflater();
368 inflater.inflate(R.menu.bookmarks, menu);
369 return true;
370 }
371 return result;
372 }
373
374 @Override
Leon Scroggins0c786502009-08-04 16:04:55 -0400375 public boolean onPrepareOptionsMenu(Menu menu) {
376 menu.findItem(R.id.switch_mode_menu_id).setTitle(
377 mGridMode ? R.string.switch_to_list
378 : R.string.switch_to_thumbnails);
379 return true;
380 }
381
382 @Override
The Android Open Source Project0c908882009-03-03 19:32:16 -0800383 public boolean onOptionsItemSelected(MenuItem item) {
384 switch (item.getItemId()) {
Leon Scroggins892df312009-07-14 14:48:02 -0400385 case R.id.new_context_menu_id:
386 saveCurrentPage();
387 break;
388
389 case R.id.switch_mode_menu_id:
390 switchViewMode(!mGridMode);
391 break;
392
393 default:
394 return super.onOptionsItemSelected(item);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800395 }
396 return true;
397 }
398
399 private void openInNewWindow(int position) {
400 Bundle b = new Bundle();
401 b.putBoolean("new_window", true);
402 setResultToParent(RESULT_OK,
403 (new Intent()).setAction(getUrl(position)).putExtras(b));
404
405 finish();
406 }
407
408
409 private void editBookmark(int position) {
410 Intent intent = new Intent(BrowserBookmarksPage.this,
411 AddBookmarkPage.class);
412 intent.putExtra("bookmark", getRow(position));
413 startActivityForResult(intent, BOOKMARKS_SAVE);
414 }
415
416 @Override
417 protected void onActivityResult(int requestCode, int resultCode,
418 Intent data) {
419 switch(requestCode) {
420 case BOOKMARKS_SAVE:
421 if (resultCode == RESULT_OK) {
422 Bundle extras;
423 if (data != null && (extras = data.getExtras()) != null) {
424 // If there are extras, then we need to save
425 // the edited bookmark. This is done in updateRow()
426 String title = extras.getString("title");
427 String url = extras.getString("url");
428 if (title != null && url != null) {
429 mBookmarksAdapter.updateRow(extras);
430 }
431 } else {
432 // extras == null then a new bookmark was added to
433 // the database.
434 refreshList();
435 }
436 }
437 break;
438 default:
439 break;
440 }
441 }
442
443 private void displayRemoveBookmarkDialog(int position) {
444 // Put up a dialog asking if the user really wants to
445 // delete the bookmark
446 final int deletePos = position;
447 new AlertDialog.Builder(this)
448 .setTitle(R.string.delete_bookmark)
449 .setIcon(android.R.drawable.ic_dialog_alert)
450 .setMessage(getText(R.string.delete_bookmark_warning).toString().replace(
451 "%s", getBookmarkTitle(deletePos)))
452 .setPositiveButton(R.string.ok,
453 new DialogInterface.OnClickListener() {
454 public void onClick(DialogInterface dialog, int whichButton) {
455 deleteBookmark(deletePos);
456 }
457 })
458 .setNegativeButton(R.string.cancel, null)
459 .show();
460 }
461
462 /**
463 * Refresh the shown list after the database has changed.
464 */
Leon Scroggins892df312009-07-14 14:48:02 -0400465 private void refreshList() {
The Android Open Source Project0c908882009-03-03 19:32:16 -0800466 mBookmarksAdapter.refreshList();
467 }
468
469 /**
470 * Return a hashmap representing the currently highlighted row.
471 */
472 public Bundle getRow(int position) {
473 return mBookmarksAdapter.getRow(position);
474 }
475
476 /**
477 * Return the url of the currently highlighted row.
478 */
479 public String getUrl(int position) {
480 return mBookmarksAdapter.getUrl(position);
481 }
482
Patrick Scotte09761e2009-03-24 20:43:37 -0700483 /**
484 * Return the favicon of the currently highlighted row.
485 */
486 public Bitmap getFavicon(int position) {
487 return mBookmarksAdapter.getFavicon(position);
488 }
489
Patrick Scott3918d442009-08-04 13:22:29 -0400490 private Bitmap getTouchIcon(int position) {
491 return mBookmarksAdapter.getTouchIcon(position);
492 }
493
The Android Open Source Project0c908882009-03-03 19:32:16 -0800494 private void copy(CharSequence text) {
495 try {
496 IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
497 if (clip != null) {
498 clip.setClipboardText(text);
499 }
500 } catch (android.os.RemoteException e) {
501 Log.e(LOGTAG, "Copy failed", e);
502 }
503 }
504
505 public String getBookmarkTitle(int position) {
506 return mBookmarksAdapter.getTitle(position);
507 }
508
509 /**
510 * Delete the currently highlighted row.
511 */
512 public void deleteBookmark(int position) {
513 mBookmarksAdapter.deleteRow(position);
514 }
515
516 public boolean dispatchKeyEvent(KeyEvent event) {
517 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.isDown()) {
518 setResultToParent(RESULT_CANCELED, null);
519 mCanceled = true;
520 }
521 return super.dispatchKeyEvent(event);
522 }
523
524 // This Activity is generally a sub-Activity of CombinedHistoryActivity. In
525 // that situation, we need to pass our result code up to our parent.
526 // However, if someone calls this Activity directly, then this has no
527 // parent, and it needs to set it on itself.
528 private void setResultToParent(int resultCode, Intent data) {
529 Activity a = getParent() == null ? this : getParent();
530 a.setResult(resultCode, data);
531 }
532}