AddBookmarkPage.java revision 30b0e3eb03104ca27c7bcd84e3199644ef43ca49
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 com.android.browser.addbookmark.FolderSpinner; 20import com.android.browser.addbookmark.FolderSpinnerAdapter; 21 22import android.app.Activity; 23import android.app.LoaderManager; 24import android.app.LoaderManager.LoaderCallbacks; 25import android.content.AsyncTaskLoader; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.CursorLoader; 31import android.content.Loader; 32import android.content.res.Resources; 33import android.database.Cursor; 34import android.graphics.Bitmap; 35import android.graphics.drawable.Drawable; 36import android.net.ParseException; 37import android.net.Uri; 38import android.net.WebAddress; 39import android.os.AsyncTask; 40import android.os.Bundle; 41import android.os.Handler; 42import android.os.Message; 43import android.provider.BrowserContract; 44import android.provider.BrowserContract.Accounts; 45import android.text.TextUtils; 46import android.util.AttributeSet; 47import android.view.KeyEvent; 48import android.view.LayoutInflater; 49import android.view.View; 50import android.view.ViewGroup; 51import android.view.Window; 52import android.view.WindowManager; 53import android.view.inputmethod.EditorInfo; 54import android.view.inputmethod.InputMethodManager; 55import android.widget.AdapterView; 56import android.widget.AdapterView.OnItemSelectedListener; 57import android.widget.ArrayAdapter; 58import android.widget.CursorAdapter; 59import android.widget.EditText; 60import android.widget.ListView; 61import android.widget.Spinner; 62import android.widget.TextView; 63import android.widget.Toast; 64 65import java.net.URI; 66import java.net.URISyntaxException; 67 68public class AddBookmarkPage extends Activity 69 implements View.OnClickListener, TextView.OnEditorActionListener, 70 AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor>, 71 BreadCrumbView.Controller, FolderSpinner.OnSetSelectionListener, 72 OnItemSelectedListener { 73 74 public static final long DEFAULT_FOLDER_ID = -1; 75 public static final String TOUCH_ICON_URL = "touch_icon_url"; 76 // Place on an edited bookmark to remove the saved thumbnail 77 public static final String REMOVE_THUMBNAIL = "remove_thumbnail"; 78 public static final String USER_AGENT = "user_agent"; 79 public static final String CHECK_FOR_DUPE = "check_for_dupe"; 80 81 /* package */ static final String EXTRA_EDIT_BOOKMARK = "bookmark"; 82 /* package */ static final String EXTRA_IS_FOLDER = "is_folder"; 83 84 private static final int MAX_CRUMBS_SHOWN = 2; 85 86 private final String LOGTAG = "Bookmarks"; 87 88 // IDs for the CursorLoaders that are used. 89 private final int LOADER_ID_ACCOUNTS = 0; 90 private final int LOADER_ID_FOLDER_CONTENTS = 1; 91 private final int LOADER_ID_EDIT_INFO = 2; 92 93 private EditText mTitle; 94 private EditText mAddress; 95 private TextView mButton; 96 private View mCancelButton; 97 private boolean mEditingExisting; 98 private boolean mEditingFolder; 99 private Bundle mMap; 100 private String mTouchIconUrl; 101 private String mOriginalUrl; 102 private FolderSpinner mFolder; 103 private View mDefaultView; 104 private View mFolderSelector; 105 private EditText mFolderNamer; 106 private View mFolderCancel; 107 private boolean mIsFolderNamerShowing; 108 private View mFolderNamerHolder; 109 private View mAddNewFolder; 110 private View mAddSeparator; 111 private long mCurrentFolder; 112 private FolderAdapter mAdapter; 113 private BreadCrumbView mCrumbs; 114 private TextView mFakeTitle; 115 private View mCrumbHolder; 116 private CustomListView mListView; 117 private boolean mSaveToHomeScreen; 118 private long mRootFolder; 119 private TextView mTopLevelLabel; 120 private Drawable mHeaderIcon; 121 private View mRemoveLink; 122 private View mFakeTitleHolder; 123 private FolderSpinnerAdapter mFolderAdapter; 124 private Spinner mAccountSpinner; 125 private ArrayAdapter<BookmarkAccount> mAccountAdapter; 126 127 private static class Folder { 128 String Name; 129 long Id; 130 Folder(String name, long id) { 131 Name = name; 132 Id = id; 133 } 134 } 135 136 // Message IDs 137 private static final int SAVE_BOOKMARK = 100; 138 private static final int TOUCH_ICON_DOWNLOADED = 101; 139 private static final int BOOKMARK_DELETED = 102; 140 141 private Handler mHandler; 142 143 private InputMethodManager getInputMethodManager() { 144 return (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 145 } 146 147 private Uri getUriForFolder(long folder) { 148 return BrowserContract.Bookmarks.buildFolderUri(folder); 149 } 150 151 @Override 152 public void onTop(BreadCrumbView view, int level, Object data) { 153 if (null == data) return; 154 Folder folderData = (Folder) data; 155 long folder = folderData.Id; 156 LoaderManager manager = getLoaderManager(); 157 CursorLoader loader = (CursorLoader) ((Loader<?>) manager.getLoader( 158 LOADER_ID_FOLDER_CONTENTS)); 159 loader.setUri(getUriForFolder(folder)); 160 loader.forceLoad(); 161 if (mIsFolderNamerShowing) { 162 completeOrCancelFolderNaming(true); 163 } 164 setShowBookmarkIcon(level == 1); 165 } 166 167 /** 168 * Show or hide the icon for bookmarks next to "Bookmarks" in the crumb view. 169 * @param show True if the icon should visible, false otherwise. 170 */ 171 private void setShowBookmarkIcon(boolean show) { 172 Drawable drawable = show ? mHeaderIcon: null; 173 mTopLevelLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); 174 } 175 176 @Override 177 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 178 if (v == mFolderNamer) { 179 if (v.getText().length() > 0) { 180 if (actionId == EditorInfo.IME_NULL) { 181 // Only want to do this once. 182 if (event.getAction() == KeyEvent.ACTION_UP) { 183 completeOrCancelFolderNaming(false); 184 } 185 } 186 } 187 // Steal the key press; otherwise a newline will be added 188 return true; 189 } 190 return false; 191 } 192 193 private void switchToDefaultView(boolean changedFolder) { 194 mFolderSelector.setVisibility(View.GONE); 195 mDefaultView.setVisibility(View.VISIBLE); 196 mCrumbHolder.setVisibility(View.GONE); 197 mFakeTitleHolder.setVisibility(View.VISIBLE); 198 if (changedFolder) { 199 Object data = mCrumbs.getTopData(); 200 if (data != null) { 201 Folder folder = (Folder) data; 202 mCurrentFolder = folder.Id; 203 if (mCurrentFolder == mRootFolder) { 204 // The Spinner changed to show "Other folder ..." Change 205 // it back to "Bookmarks", which is position 0 if we are 206 // editing a folder, 1 otherwise. 207 mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 0 : 1); 208 } else { 209 mFolderAdapter.setOtherFolderDisplayText(folder.Name); 210 } 211 } 212 } else { 213 // The user canceled selecting a folder. Revert back to the earlier 214 // selection. 215 if (mSaveToHomeScreen) { 216 mFolder.setSelectionIgnoringSelectionChange(0); 217 } else { 218 if (mCurrentFolder == mRootFolder) { 219 mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 0 : 1); 220 } else { 221 Object data = mCrumbs.getTopData(); 222 if (data != null && ((Folder) data).Id == mCurrentFolder) { 223 // We are showing the correct folder hierarchy. The 224 // folder selector will say "Other folder..." Change it 225 // to say the name of the folder once again. 226 mFolderAdapter.setOtherFolderDisplayText(((Folder) data).Name); 227 } else { 228 // We are not showing the correct folder hierarchy. 229 // Clear the Crumbs and find the proper folder 230 setupTopCrumb(); 231 LoaderManager manager = getLoaderManager(); 232 manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this); 233 234 } 235 } 236 } 237 } 238 } 239 240 @Override 241 public void onClick(View v) { 242 if (v == mButton) { 243 if (mFolderSelector.getVisibility() == View.VISIBLE) { 244 // We are showing the folder selector. 245 if (mIsFolderNamerShowing) { 246 completeOrCancelFolderNaming(false); 247 } else { 248 // User has selected a folder. Go back to the opening page 249 mSaveToHomeScreen = false; 250 switchToDefaultView(true); 251 } 252 } else if (save()) { 253 finish(); 254 } 255 } else if (v == mCancelButton) { 256 if (mIsFolderNamerShowing) { 257 completeOrCancelFolderNaming(true); 258 } else if (mFolderSelector.getVisibility() == View.VISIBLE) { 259 switchToDefaultView(false); 260 } else { 261 finish(); 262 } 263 } else if (v == mFolderCancel) { 264 completeOrCancelFolderNaming(true); 265 } else if (v == mAddNewFolder) { 266 setShowFolderNamer(true); 267 mFolderNamer.setText(R.string.new_folder); 268 mFolderNamer.requestFocus(); 269 mAddNewFolder.setVisibility(View.GONE); 270 mAddSeparator.setVisibility(View.GONE); 271 InputMethodManager imm = getInputMethodManager(); 272 // Set the InputMethodManager to focus on the ListView so that it 273 // can transfer the focus to mFolderNamer. 274 imm.focusIn(mListView); 275 imm.showSoftInput(mFolderNamer, InputMethodManager.SHOW_IMPLICIT); 276 } else if (v == mRemoveLink) { 277 if (!mEditingExisting) { 278 throw new AssertionError("Remove button should not be shown for" 279 + " new bookmarks"); 280 } 281 long id = mMap.getLong(BrowserContract.Bookmarks._ID); 282 createHandler(); 283 Message msg = Message.obtain(mHandler, BOOKMARK_DELETED); 284 BookmarkUtils.displayRemoveBookmarkDialog(id, 285 mTitle.getText().toString(), this, msg); 286 } 287 } 288 289 // FolderSpinner.OnSetSelectionListener 290 291 @Override 292 public void onSetSelection(long id) { 293 int intId = (int) id; 294 switch (intId) { 295 case FolderSpinnerAdapter.ROOT_FOLDER: 296 mCurrentFolder = mRootFolder; 297 mSaveToHomeScreen = false; 298 break; 299 case FolderSpinnerAdapter.HOME_SCREEN: 300 // Create a short cut to the home screen 301 mSaveToHomeScreen = true; 302 break; 303 case FolderSpinnerAdapter.OTHER_FOLDER: 304 switchToFolderSelector(); 305 break; 306 case FolderSpinnerAdapter.RECENT_FOLDER: 307 mCurrentFolder = mFolderAdapter.recentFolderId(); 308 mSaveToHomeScreen = false; 309 // In case the user decides to select OTHER_FOLDER 310 // and choose a different one, so that we will start from 311 // the correct place. 312 LoaderManager manager = getLoaderManager(); 313 manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this); 314 break; 315 default: 316 break; 317 } 318 } 319 320 /** 321 * Finish naming a folder, and close the IME 322 * @param cancel If true, the new folder is not created. If false, the new 323 * folder is created and the user is taken inside it. 324 */ 325 private void completeOrCancelFolderNaming(boolean cancel) { 326 if (!cancel && !TextUtils.isEmpty(mFolderNamer.getText())) { 327 String name = mFolderNamer.getText().toString(); 328 long id = addFolderToCurrent(mFolderNamer.getText().toString()); 329 descendInto(name, id); 330 } 331 setShowFolderNamer(false); 332 mAddNewFolder.setVisibility(View.VISIBLE); 333 mAddSeparator.setVisibility(View.VISIBLE); 334 getInputMethodManager().hideSoftInputFromWindow( 335 mListView.getWindowToken(), 0); 336 } 337 338 private long addFolderToCurrent(String name) { 339 // Add the folder to the database 340 ContentValues values = new ContentValues(); 341 values.put(BrowserContract.Bookmarks.TITLE, 342 name); 343 values.put(BrowserContract.Bookmarks.IS_FOLDER, 1); 344 long currentFolder; 345 Object data = mCrumbs.getTopData(); 346 if (data != null) { 347 currentFolder = ((Folder) data).Id; 348 } else { 349 currentFolder = mRootFolder; 350 } 351 values.put(BrowserContract.Bookmarks.PARENT, currentFolder); 352 Uri uri = getContentResolver().insert( 353 BrowserContract.Bookmarks.CONTENT_URI, values); 354 if (uri != null) { 355 return ContentUris.parseId(uri); 356 } else { 357 return -1; 358 } 359 } 360 361 private void switchToFolderSelector() { 362 // Set the list to the top in case it is scrolled. 363 mListView.setSelection(0); 364 mDefaultView.setVisibility(View.GONE); 365 mFolderSelector.setVisibility(View.VISIBLE); 366 mCrumbHolder.setVisibility(View.VISIBLE); 367 mFakeTitleHolder.setVisibility(View.GONE); 368 mAddNewFolder.setVisibility(View.VISIBLE); 369 mAddSeparator.setVisibility(View.VISIBLE); 370 } 371 372 private void descendInto(String foldername, long id) { 373 if (id != DEFAULT_FOLDER_ID) { 374 mCrumbs.pushView(foldername, new Folder(foldername, id)); 375 mCrumbs.notifyController(); 376 } 377 } 378 379 private LoaderCallbacks<EditBookmarkInfo> mEditInfoLoaderCallbacks = 380 new LoaderCallbacks<EditBookmarkInfo>() { 381 382 @Override 383 public void onLoaderReset(Loader<EditBookmarkInfo> loader) { 384 // Don't care 385 } 386 387 @Override 388 public void onLoadFinished(Loader<EditBookmarkInfo> loader, 389 EditBookmarkInfo info) { 390 boolean setAccount = false; 391 if (info.id != -1) { 392 mEditingExisting = true; 393 showRemoveButton(); 394 mFakeTitle.setText(R.string.edit_bookmark); 395 mTitle.setText(info.title); 396 mFolderAdapter.setOtherFolderDisplayText(info.parentTitle); 397 mMap.putLong(BrowserContract.Bookmarks._ID, info.id); 398 setAccount = true; 399 setAccount(info.accountName, info.accountType); 400 mCurrentFolder = info.parentId; 401 onCurrentFolderFound(); 402 } 403 // TODO: Detect if lastUsedId is a subfolder of info.id in the 404 // editing folder case. For now, just don't show the last used 405 // folder at all to prevent any chance of the user adding a parent 406 // folder to a child folder 407 if (info.lastUsedId != -1 && info.lastUsedId != info.id 408 && !mEditingFolder) { 409 if (setAccount && info.lastUsedId != mRootFolder 410 && TextUtils.equals(info.lastUsedAccountName, info.accountName) 411 && TextUtils.equals(info.lastUsedAccountType, info.accountType)) { 412 mFolderAdapter.addRecentFolder(info.lastUsedId, info.lastUsedTitle); 413 } else if (!setAccount) { 414 setAccount = true; 415 setAccount(info.lastUsedAccountName, info.lastUsedAccountType); 416 if (info.lastUsedId != mRootFolder) { 417 mFolderAdapter.addRecentFolder(info.lastUsedId, 418 info.lastUsedTitle); 419 } 420 } 421 } 422 if (!setAccount) { 423 mAccountSpinner.setSelection(0); 424 } 425 } 426 427 @Override 428 public Loader<EditBookmarkInfo> onCreateLoader(int id, Bundle args) { 429 return new EditBookmarkInfoLoader(AddBookmarkPage.this, mMap); 430 } 431 }; 432 433 void setAccount(String accountName, String accountType) { 434 for (int i = 0; i < mAccountAdapter.getCount(); i++) { 435 BookmarkAccount account = mAccountAdapter.getItem(i); 436 if (TextUtils.equals(account.accountName, accountName) 437 && TextUtils.equals(account.accountType, accountType)) { 438 onRootFolderFound(account.rootFolderId); 439 mAccountSpinner.setSelection(i); 440 return; 441 } 442 } 443 } 444 445 @Override 446 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 447 String[] projection; 448 switch (id) { 449 case LOADER_ID_ACCOUNTS: 450 return new AccountsLoader(this); 451 case LOADER_ID_FOLDER_CONTENTS: 452 projection = new String[] { 453 BrowserContract.Bookmarks._ID, 454 BrowserContract.Bookmarks.TITLE, 455 BrowserContract.Bookmarks.IS_FOLDER 456 }; 457 String where = BrowserContract.Bookmarks.IS_FOLDER + " != 0"; 458 String whereArgs[] = null; 459 if (mEditingFolder) { 460 where += " AND " + BrowserContract.Bookmarks._ID + " != ?"; 461 whereArgs = new String[] { Long.toString(mMap.getLong( 462 BrowserContract.Bookmarks._ID)) }; 463 } 464 long currentFolder; 465 Object data = mCrumbs.getTopData(); 466 if (data != null) { 467 currentFolder = ((Folder) data).Id; 468 } else { 469 currentFolder = mRootFolder; 470 } 471 return new CursorLoader(this, 472 getUriForFolder(currentFolder), 473 projection, 474 where, 475 whereArgs, 476 BrowserContract.Bookmarks._ID + " ASC"); 477 default: 478 throw new AssertionError("Asking for nonexistant loader!"); 479 } 480 } 481 482 @Override 483 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 484 switch (loader.getId()) { 485 case LOADER_ID_ACCOUNTS: 486 mAccountAdapter.clear(); 487 while (cursor.moveToNext()) { 488 mAccountAdapter.add(new BookmarkAccount(this, cursor)); 489 } 490 getLoaderManager().destroyLoader(LOADER_ID_ACCOUNTS); 491 getLoaderManager().restartLoader(LOADER_ID_EDIT_INFO, null, 492 mEditInfoLoaderCallbacks); 493 break; 494 case LOADER_ID_FOLDER_CONTENTS: 495 mAdapter.changeCursor(cursor); 496 break; 497 } 498 } 499 500 public void onLoaderReset(Loader<Cursor> loader) { 501 switch (loader.getId()) { 502 case LOADER_ID_FOLDER_CONTENTS: 503 mAdapter.changeCursor(null); 504 break; 505 } 506 } 507 508 /** 509 * Move cursor to the position that has folderToFind as its "_id". 510 * @param cursor Cursor containing folders in the bookmarks database 511 * @param folderToFind "_id" of the folder to move to. 512 * @param idIndex Index in cursor of "_id" 513 * @throws AssertionError if cursor is empty or there is no row with folderToFind 514 * as its "_id". 515 */ 516 void moveCursorToFolder(Cursor cursor, long folderToFind, int idIndex) 517 throws AssertionError { 518 if (!cursor.moveToFirst()) { 519 throw new AssertionError("No folders in the database!"); 520 } 521 long folder; 522 do { 523 folder = cursor.getLong(idIndex); 524 } while (folder != folderToFind && cursor.moveToNext()); 525 if (cursor.isAfterLast()) { 526 throw new AssertionError("Folder(id=" + folderToFind 527 + ") holding this bookmark does not exist!"); 528 } 529 } 530 531 @Override 532 public void onItemClick(AdapterView<?> parent, View view, int position, 533 long id) { 534 TextView tv = (TextView) view.findViewById(android.R.id.text1); 535 // Switch to the folder that was clicked on. 536 descendInto(tv.getText().toString(), id); 537 } 538 539 private void setShowFolderNamer(boolean show) { 540 if (show != mIsFolderNamerShowing) { 541 mIsFolderNamerShowing = show; 542 if (show) { 543 // Set the selection to the folder namer so it will be in 544 // view. 545 mListView.addFooterView(mFolderNamerHolder); 546 } else { 547 mListView.removeFooterView(mFolderNamerHolder); 548 } 549 // Refresh the list. 550 mListView.setAdapter(mAdapter); 551 if (show) { 552 mListView.setSelection(mListView.getCount() - 1); 553 } 554 } 555 } 556 557 /** 558 * Shows a list of names of folders. 559 */ 560 private class FolderAdapter extends CursorAdapter { 561 public FolderAdapter(Context context) { 562 super(context, null); 563 } 564 565 @Override 566 public void bindView(View view, Context context, Cursor cursor) { 567 ((TextView) view.findViewById(android.R.id.text1)).setText( 568 cursor.getString(cursor.getColumnIndexOrThrow( 569 BrowserContract.Bookmarks.TITLE))); 570 } 571 572 @Override 573 public View newView(Context context, Cursor cursor, ViewGroup parent) { 574 View view = LayoutInflater.from(context).inflate( 575 R.layout.folder_list_item, null); 576 view.setBackgroundDrawable(context.getResources(). 577 getDrawable(android.R.drawable.list_selector_background)); 578 return view; 579 } 580 581 @Override 582 public boolean isEmpty() { 583 // Do not show the empty view if the user is creating a new folder. 584 return super.isEmpty() && !mIsFolderNamerShowing; 585 } 586 } 587 588 @Override 589 protected void onCreate(Bundle icicle) { 590 super.onCreate(icicle); 591 requestWindowFeature(Window.FEATURE_NO_TITLE); 592 593 mMap = getIntent().getExtras(); 594 595 setContentView(R.layout.browser_add_bookmark); 596 597 Window window = getWindow(); 598 599 String title = null; 600 String url = null; 601 602 mFakeTitle = (TextView) findViewById(R.id.fake_title); 603 604 if (mMap != null) { 605 Bundle b = mMap.getBundle(EXTRA_EDIT_BOOKMARK); 606 if (b != null) { 607 mEditingFolder = mMap.getBoolean(EXTRA_IS_FOLDER, false); 608 mMap = b; 609 mEditingExisting = true; 610 mFakeTitle.setText(R.string.edit_bookmark); 611 if (mEditingFolder) { 612 findViewById(R.id.row_address).setVisibility(View.GONE); 613 } else { 614 showRemoveButton(); 615 } 616 } else { 617 int gravity = mMap.getInt("gravity", -1); 618 if (gravity != -1) { 619 WindowManager.LayoutParams l = window.getAttributes(); 620 l.gravity = gravity; 621 window.setAttributes(l); 622 } 623 } 624 title = mMap.getString(BrowserContract.Bookmarks.TITLE); 625 url = mOriginalUrl = mMap.getString(BrowserContract.Bookmarks.URL); 626 mTouchIconUrl = mMap.getString(TOUCH_ICON_URL); 627 mCurrentFolder = mMap.getLong(BrowserContract.Bookmarks.PARENT, DEFAULT_FOLDER_ID); 628 } 629 630 mTitle = (EditText) findViewById(R.id.title); 631 mTitle.setText(title); 632 633 mAddress = (EditText) findViewById(R.id.address); 634 mAddress.setText(url); 635 636 mButton = (TextView) findViewById(R.id.OK); 637 mButton.setOnClickListener(this); 638 639 mCancelButton = findViewById(R.id.cancel); 640 mCancelButton.setOnClickListener(this); 641 642 mFolder = (FolderSpinner) findViewById(R.id.folder); 643 mFolderAdapter = new FolderSpinnerAdapter(this, !mEditingFolder); 644 mFolder.setAdapter(mFolderAdapter); 645 mFolder.setOnSetSelectionListener(this); 646 647 mDefaultView = findViewById(R.id.default_view); 648 mFolderSelector = findViewById(R.id.folder_selector); 649 650 mFolderNamerHolder = getLayoutInflater().inflate(R.layout.new_folder_layout, null); 651 mFolderNamer = (EditText) mFolderNamerHolder.findViewById(R.id.folder_namer); 652 mFolderNamer.setOnEditorActionListener(this); 653 mFolderCancel = mFolderNamerHolder.findViewById(R.id.close); 654 mFolderCancel.setOnClickListener(this); 655 656 mAddNewFolder = findViewById(R.id.add_new_folder); 657 mAddNewFolder.setOnClickListener(this); 658 mAddSeparator = findViewById(R.id.add_divider); 659 660 mCrumbs = (BreadCrumbView) findViewById(R.id.crumbs); 661 mCrumbs.setUseBackButton(true); 662 mCrumbs.setController(this); 663 mHeaderIcon = getResources().getDrawable(R.drawable.ic_folder_holo_dark); 664 mCrumbHolder = findViewById(R.id.crumb_holder); 665 mCrumbs.setMaxVisible(MAX_CRUMBS_SHOWN); 666 667 mAdapter = new FolderAdapter(this); 668 mListView = (CustomListView) findViewById(R.id.list); 669 View empty = findViewById(R.id.empty); 670 mListView.setEmptyView(empty); 671 mListView.setAdapter(mAdapter); 672 mListView.setOnItemClickListener(this); 673 mListView.addEditText(mFolderNamer); 674 675 mAccountAdapter = new ArrayAdapter<BookmarkAccount>(this, 676 android.R.layout.simple_spinner_item); 677 mAccountAdapter.setDropDownViewResource( 678 android.R.layout.simple_spinner_dropdown_item); 679 mAccountSpinner = (Spinner) findViewById(R.id.accounts); 680 mAccountSpinner.setAdapter(mAccountAdapter); 681 mAccountSpinner.setOnItemSelectedListener(this); 682 683 684 mFakeTitleHolder = findViewById(R.id.title_holder); 685 686 if (!window.getDecorView().isInTouchMode()) { 687 mButton.requestFocus(); 688 } 689 690 getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this); 691 } 692 693 private void showRemoveButton() { 694 findViewById(R.id.remove_divider).setVisibility(View.VISIBLE); 695 mRemoveLink = findViewById(R.id.remove); 696 mRemoveLink.setVisibility(View.VISIBLE); 697 mRemoveLink.setOnClickListener(this); 698 } 699 700 // Called once we have determined which folder is the root folder 701 private void onRootFolderFound(long root) { 702 mRootFolder = root; 703 mCurrentFolder = mRootFolder; 704 setupTopCrumb(); 705 onCurrentFolderFound(); 706 } 707 708 private void setupTopCrumb() { 709 mCrumbs.clear(); 710 String name = getString(R.string.bookmarks); 711 mTopLevelLabel = (TextView) mCrumbs.pushView(name, false, 712 new Folder(name, mRootFolder)); 713 // To better match the other folders. 714 mTopLevelLabel.setCompoundDrawablePadding(6); 715 } 716 717 private void onCurrentFolderFound() { 718 LoaderManager manager = getLoaderManager(); 719 if (mCurrentFolder != mRootFolder) { 720 // Since we're not in the root folder, change the selection to other 721 // folder now. The text will get changed once we select the correct 722 // folder. 723 mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 1 : 2); 724 } else { 725 setShowBookmarkIcon(true); 726 if (!mEditingFolder) { 727 // Initially the "Bookmarks" folder should be showing, rather than 728 // the home screen. In the editing folder case, home screen is not 729 // an option, so "Bookmarks" folder is already at the top. 730 mFolder.setSelectionIgnoringSelectionChange(FolderSpinnerAdapter.ROOT_FOLDER); 731 } 732 } 733 // Find the contents of the current folder 734 manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this); 735 } 736 737 /** 738 * Runnable to save a bookmark, so it can be performed in its own thread. 739 */ 740 private class SaveBookmarkRunnable implements Runnable { 741 // FIXME: This should be an async task. 742 private Message mMessage; 743 private Context mContext; 744 public SaveBookmarkRunnable(Context ctx, Message msg) { 745 mContext = ctx; 746 mMessage = msg; 747 } 748 public void run() { 749 // Unbundle bookmark data. 750 Bundle bundle = mMessage.getData(); 751 String title = bundle.getString(BrowserContract.Bookmarks.TITLE); 752 String url = bundle.getString(BrowserContract.Bookmarks.URL); 753 boolean invalidateThumbnail = bundle.getBoolean(REMOVE_THUMBNAIL); 754 Bitmap thumbnail = invalidateThumbnail ? null 755 : (Bitmap) bundle.getParcelable(BrowserContract.Bookmarks.THUMBNAIL); 756 String touchIconUrl = bundle.getString(TOUCH_ICON_URL); 757 758 // Save to the bookmarks DB. 759 try { 760 final ContentResolver cr = getContentResolver(); 761 Bookmarks.addBookmark(AddBookmarkPage.this, false, url, 762 title, thumbnail, true, mCurrentFolder); 763 if (touchIconUrl != null) { 764 new DownloadTouchIcon(mContext, cr, url).execute(mTouchIconUrl); 765 } 766 mMessage.arg1 = 1; 767 } catch (IllegalStateException e) { 768 mMessage.arg1 = 0; 769 } 770 mMessage.sendToTarget(); 771 } 772 } 773 774 private static class UpdateBookmarkTask extends AsyncTask<ContentValues, Void, Void> { 775 Context mContext; 776 Long mId; 777 778 public UpdateBookmarkTask(Context context, long id) { 779 mContext = context; 780 mId = id; 781 } 782 783 @Override 784 protected Void doInBackground(ContentValues... params) { 785 if (params.length != 1) { 786 throw new IllegalArgumentException("No ContentValues provided!"); 787 } 788 Uri uri = ContentUris.withAppendedId(BookmarkUtils.getBookmarksUri(mContext), mId); 789 mContext.getContentResolver().update( 790 uri, 791 params[0], null, null); 792 return null; 793 } 794 } 795 796 private void createHandler() { 797 if (mHandler == null) { 798 mHandler = new Handler() { 799 @Override 800 public void handleMessage(Message msg) { 801 switch (msg.what) { 802 case SAVE_BOOKMARK: 803 if (1 == msg.arg1) { 804 Toast.makeText(AddBookmarkPage.this, R.string.bookmark_saved, 805 Toast.LENGTH_LONG).show(); 806 } else { 807 Toast.makeText(AddBookmarkPage.this, R.string.bookmark_not_saved, 808 Toast.LENGTH_LONG).show(); 809 } 810 break; 811 case TOUCH_ICON_DOWNLOADED: 812 Bundle b = msg.getData(); 813 sendBroadcast(BookmarkUtils.createAddToHomeIntent( 814 AddBookmarkPage.this, 815 b.getString(BrowserContract.Bookmarks.URL), 816 b.getString(BrowserContract.Bookmarks.TITLE), 817 (Bitmap) b.getParcelable(BrowserContract.Bookmarks.TOUCH_ICON), 818 (Bitmap) b.getParcelable(BrowserContract.Bookmarks.FAVICON))); 819 break; 820 case BOOKMARK_DELETED: 821 finish(); 822 break; 823 } 824 } 825 }; 826 } 827 } 828 829 /** 830 * Parse the data entered in the dialog and post a message to update the bookmarks database. 831 */ 832 boolean save() { 833 createHandler(); 834 835 String title = mTitle.getText().toString().trim(); 836 String unfilteredUrl; 837 unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString()); 838 839 boolean emptyTitle = title.length() == 0; 840 boolean emptyUrl = unfilteredUrl.trim().length() == 0; 841 Resources r = getResources(); 842 if (emptyTitle || (emptyUrl && !mEditingFolder)) { 843 if (emptyTitle) { 844 mTitle.setError(r.getText(R.string.bookmark_needs_title)); 845 } 846 if (emptyUrl) { 847 mAddress.setError(r.getText(R.string.bookmark_needs_url)); 848 } 849 return false; 850 851 } 852 String url = unfilteredUrl.trim(); 853 if (!mEditingFolder) { 854 try { 855 // We allow bookmarks with a javascript: scheme, but these will in most cases 856 // fail URI parsing, so don't try it if that's the kind of bookmark we have. 857 858 if (!url.toLowerCase().startsWith("javascript:")) { 859 URI uriObj = new URI(url); 860 String scheme = uriObj.getScheme(); 861 if (!Bookmarks.urlHasAcceptableScheme(url)) { 862 // If the scheme was non-null, let the user know that we 863 // can't save their bookmark. If it was null, we'll assume 864 // they meant http when we parse it in the WebAddress class. 865 if (scheme != null) { 866 mAddress.setError(r.getText(R.string.bookmark_cannot_save_url)); 867 return false; 868 } 869 WebAddress address; 870 try { 871 address = new WebAddress(unfilteredUrl); 872 } catch (ParseException e) { 873 throw new URISyntaxException("", ""); 874 } 875 if (address.getHost().length() == 0) { 876 throw new URISyntaxException("", ""); 877 } 878 url = address.toString(); 879 } 880 } 881 } catch (URISyntaxException e) { 882 mAddress.setError(r.getText(R.string.bookmark_url_not_valid)); 883 return false; 884 } 885 } 886 887 if (mSaveToHomeScreen) { 888 mEditingExisting = false; 889 } 890 891 boolean urlUnmodified = url.equals(mOriginalUrl); 892 893 if (mEditingExisting) { 894 Long id = mMap.getLong(BrowserContract.Bookmarks._ID); 895 ContentValues values = new ContentValues(); 896 values.put(BrowserContract.Bookmarks.TITLE, title); 897 values.put(BrowserContract.Bookmarks.PARENT, mCurrentFolder); 898 if (!mEditingFolder) { 899 values.put(BrowserContract.Bookmarks.URL, url); 900 if (!urlUnmodified) { 901 values.putNull(BrowserContract.Bookmarks.THUMBNAIL); 902 } 903 } 904 if (values.size() > 0) { 905 new UpdateBookmarkTask(getApplicationContext(), id).execute(values); 906 } 907 setResult(RESULT_OK); 908 } else { 909 Bitmap thumbnail; 910 Bitmap favicon; 911 if (urlUnmodified) { 912 thumbnail = (Bitmap) mMap.getParcelable( 913 BrowserContract.Bookmarks.THUMBNAIL); 914 favicon = (Bitmap) mMap.getParcelable( 915 BrowserContract.Bookmarks.FAVICON); 916 } else { 917 thumbnail = null; 918 favicon = null; 919 } 920 921 Bundle bundle = new Bundle(); 922 bundle.putString(BrowserContract.Bookmarks.TITLE, title); 923 bundle.putString(BrowserContract.Bookmarks.URL, url); 924 bundle.putParcelable(BrowserContract.Bookmarks.FAVICON, favicon); 925 926 if (mSaveToHomeScreen) { 927 if (mTouchIconUrl != null && urlUnmodified) { 928 Message msg = Message.obtain(mHandler, 929 TOUCH_ICON_DOWNLOADED); 930 msg.setData(bundle); 931 DownloadTouchIcon icon = new DownloadTouchIcon(this, msg, 932 mMap.getString(USER_AGENT)); 933 icon.execute(mTouchIconUrl); 934 } else { 935 sendBroadcast(BookmarkUtils.createAddToHomeIntent(this, url, 936 title, null /*touchIcon*/, favicon)); 937 } 938 } else { 939 bundle.putParcelable(BrowserContract.Bookmarks.THUMBNAIL, thumbnail); 940 bundle.putBoolean(REMOVE_THUMBNAIL, !urlUnmodified); 941 bundle.putString(TOUCH_ICON_URL, mTouchIconUrl); 942 // Post a message to write to the DB. 943 Message msg = Message.obtain(mHandler, SAVE_BOOKMARK); 944 msg.setData(bundle); 945 // Start a new thread so as to not slow down the UI 946 Thread t = new Thread(new SaveBookmarkRunnable(getApplicationContext(), msg)); 947 t.start(); 948 } 949 setResult(RESULT_OK); 950 LogTag.logBookmarkAdded(url, "bookmarkview"); 951 } 952 return true; 953 } 954 955 @Override 956 public void onItemSelected(AdapterView<?> parent, View view, int position, 957 long id) { 958 if (mAccountSpinner == parent) { 959 long root = mAccountAdapter.getItem(position).rootFolderId; 960 if (root != mRootFolder) { 961 onRootFolderFound(root); 962 } 963 } 964 } 965 966 @Override 967 public void onNothingSelected(AdapterView<?> parent) { 968 // Don't care 969 } 970 971 /* 972 * Class used as a proxy for the InputMethodManager to get to mFolderNamer 973 */ 974 public static class CustomListView extends ListView { 975 private EditText mEditText; 976 977 public void addEditText(EditText editText) { 978 mEditText = editText; 979 } 980 981 public CustomListView(Context context) { 982 super(context); 983 } 984 985 public CustomListView(Context context, AttributeSet attrs) { 986 super(context, attrs); 987 } 988 989 public CustomListView(Context context, AttributeSet attrs, int defStyle) { 990 super(context, attrs, defStyle); 991 } 992 993 @Override 994 public boolean checkInputConnectionProxy(View view) { 995 return view == mEditText; 996 } 997 } 998 999 static class AccountsLoader extends CursorLoader { 1000 1001 static final String[] PROJECTION = new String[] { 1002 Accounts.ACCOUNT_NAME, 1003 Accounts.ACCOUNT_TYPE, 1004 Accounts.ROOT_ID, 1005 }; 1006 1007 static final int COLUMN_INDEX_ACCOUNT_NAME = 0; 1008 static final int COLUMN_INDEX_ACCOUNT_TYPE = 1; 1009 static final int COLUMN_INDEX_ROOT_ID = 2; 1010 1011 public AccountsLoader(Context context) { 1012 super(context, Accounts.CONTENT_URI, PROJECTION, null, null, 1013 Accounts.ACCOUNT_NAME + " ASC"); 1014 } 1015 1016 } 1017 1018 static class BookmarkAccount { 1019 1020 private String mLabel; 1021 String accountName, accountType; 1022 long rootFolderId; 1023 1024 public BookmarkAccount(Context context, Cursor cursor) { 1025 accountName = cursor.getString( 1026 AccountsLoader.COLUMN_INDEX_ACCOUNT_NAME); 1027 accountType = cursor.getString( 1028 AccountsLoader.COLUMN_INDEX_ACCOUNT_TYPE); 1029 rootFolderId = cursor.getLong( 1030 AccountsLoader.COLUMN_INDEX_ROOT_ID); 1031 mLabel = accountName; 1032 if (TextUtils.isEmpty(mLabel)) { 1033 mLabel = context.getString(R.string.local_bookmarks); 1034 } 1035 } 1036 1037 @Override 1038 public String toString() { 1039 return mLabel; 1040 } 1041 } 1042 1043 static class EditBookmarkInfo { 1044 long id = -1; 1045 long parentId = -1; 1046 String parentTitle; 1047 String title; 1048 String accountName; 1049 String accountType; 1050 1051 long lastUsedId = -1; 1052 String lastUsedTitle; 1053 String lastUsedAccountName; 1054 String lastUsedAccountType; 1055 } 1056 1057 static class EditBookmarkInfoLoader extends AsyncTaskLoader<EditBookmarkInfo> { 1058 1059 private Context mContext; 1060 private Bundle mMap; 1061 1062 public EditBookmarkInfoLoader(Context context, Bundle bundle) { 1063 super(context); 1064 mContext = context; 1065 mMap = bundle; 1066 } 1067 1068 @Override 1069 public EditBookmarkInfo loadInBackground() { 1070 final ContentResolver cr = mContext.getContentResolver(); 1071 EditBookmarkInfo info = new EditBookmarkInfo(); 1072 Cursor c = null; 1073 1074 try { 1075 // First, let's lookup the bookmark (check for dupes, get needed info) 1076 String url = mMap.getString(BrowserContract.Bookmarks.URL); 1077 info.id = mMap.getLong(BrowserContract.Bookmarks._ID, -1); 1078 boolean checkForDupe = mMap.getBoolean(CHECK_FOR_DUPE); 1079 if (checkForDupe && info.id == -1 && !TextUtils.isEmpty(url)) { 1080 c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, 1081 new String[] { BrowserContract.Bookmarks._ID}, 1082 BrowserContract.Bookmarks.URL + "=?", 1083 new String[] { url }, null); 1084 if (c.getCount() == 1 && c.moveToFirst()) { 1085 info.id = c.getLong(0); 1086 } 1087 c.close(); 1088 } 1089 if (info.id != -1) { 1090 c = cr.query(ContentUris.withAppendedId( 1091 BrowserContract.Bookmarks.CONTENT_URI, info.id), 1092 new String[] { 1093 BrowserContract.Bookmarks.PARENT, 1094 BrowserContract.Bookmarks.ACCOUNT_NAME, 1095 BrowserContract.Bookmarks.ACCOUNT_TYPE, 1096 BrowserContract.Bookmarks.TITLE}, 1097 null, null, null); 1098 if (c.moveToFirst()) { 1099 info.parentId = c.getLong(0); 1100 info.accountName = c.getString(1); 1101 info.accountType = c.getString(2); 1102 info.title = c.getString(3); 1103 } 1104 c.close(); 1105 c = cr.query(ContentUris.withAppendedId( 1106 BrowserContract.Bookmarks.CONTENT_URI, info.parentId), 1107 new String[] { 1108 BrowserContract.Bookmarks.TITLE,}, 1109 null, null, null); 1110 if (c.moveToFirst()) { 1111 info.parentTitle = c.getString(0); 1112 } 1113 c.close(); 1114 } 1115 1116 // Figure out the last used folder/account 1117 c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, 1118 new String[] { 1119 BrowserContract.Bookmarks.PARENT, 1120 }, null, null, 1121 BrowserContract.Bookmarks.DATE_MODIFIED + " DESC LIMIT 1"); 1122 if (c.moveToFirst()) { 1123 long parent = c.getLong(0); 1124 c.close(); 1125 c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, 1126 new String[] { 1127 BrowserContract.Bookmarks.TITLE, 1128 BrowserContract.Bookmarks.ACCOUNT_NAME, 1129 BrowserContract.Bookmarks.ACCOUNT_TYPE}, 1130 BrowserContract.Bookmarks._ID + "=?", new String[] { 1131 Long.toString(parent)}, null); 1132 if (c.moveToFirst()) { 1133 info.lastUsedId = parent; 1134 info.lastUsedTitle = c.getString(0); 1135 info.lastUsedAccountName = c.getString(1); 1136 info.lastUsedAccountType = c.getString(2); 1137 } 1138 c.close(); 1139 } 1140 } finally { 1141 if (c != null) { 1142 c.close(); 1143 } 1144 } 1145 1146 return info; 1147 } 1148 1149 @Override 1150 protected void onStartLoading() { 1151 forceLoad(); 1152 } 1153 1154 } 1155 1156} 1157