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 17 package com.android.browser; 18 19 import com.android.browser.addbookmark.FolderSpinner; 20 import com.android.browser.addbookmark.FolderSpinnerAdapter; 21 22 import android.app.Activity; 23 import android.app.LoaderManager; 24 import android.app.LoaderManager.LoaderCallbacks; 25 import android.content.AsyncTaskLoader; 26 import android.content.ContentResolver; 27 import android.content.ContentUris; 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.content.CursorLoader; 31 import android.content.Loader; 32 import android.content.res.Resources; 33 import android.database.Cursor; 34 import android.graphics.Bitmap; 35 import android.graphics.drawable.Drawable; 36 import android.net.ParseException; 37 import android.net.Uri; 38 import android.net.WebAddress; 39 import android.os.AsyncTask; 40 import android.os.Bundle; 41 import android.os.Handler; 42 import android.os.Message; 43 import android.provider.BrowserContract; 44 import android.provider.BrowserContract.Accounts; 45 import android.text.TextUtils; 46 import android.util.AttributeSet; 47 import android.view.KeyEvent; 48 import android.view.LayoutInflater; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.view.Window; 52 import android.view.WindowManager; 53 import android.view.inputmethod.EditorInfo; 54 import android.view.inputmethod.InputMethodManager; 55 import android.widget.AdapterView; 56 import android.widget.AdapterView.OnItemSelectedListener; 57 import android.widget.ArrayAdapter; 58 import android.widget.CursorAdapter; 59 import android.widget.EditText; 60 import android.widget.ListView; 61 import android.widget.Spinner; 62 import android.widget.TextView; 63 import android.widget.Toast; 64 65 import java.net.URI; 66 import java.net.URISyntaxException; 67 68 public 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 getInputMethodManager().hideSoftInputFromWindow( 371 mListView.getWindowToken(), 0); 372 } 373 374 private void descendInto(String foldername, long id) { 375 if (id != DEFAULT_FOLDER_ID) { 376 mCrumbs.pushView(foldername, new Folder(foldername, id)); 377 mCrumbs.notifyController(); 378 } 379 } 380 381 private LoaderCallbacks<EditBookmarkInfo> mEditInfoLoaderCallbacks = 382 new LoaderCallbacks<EditBookmarkInfo>() { 383 384 @Override 385 public void onLoaderReset(Loader<EditBookmarkInfo> loader) { 386 // Don't care 387 } 388 389 @Override 390 public void onLoadFinished(Loader<EditBookmarkInfo> loader, 391 EditBookmarkInfo info) { 392 boolean setAccount = false; 393 if (info.id != -1) { 394 mEditingExisting = true; 395 showRemoveButton(); 396 mFakeTitle.setText(R.string.edit_bookmark); 397 mTitle.setText(info.title); 398 mFolderAdapter.setOtherFolderDisplayText(info.parentTitle); 399 mMap.putLong(BrowserContract.Bookmarks._ID, info.id); 400 setAccount = true; 401 setAccount(info.accountName, info.accountType); 402 mCurrentFolder = info.parentId; 403 onCurrentFolderFound(); 404 } 405 // TODO: Detect if lastUsedId is a subfolder of info.id in the 406 // editing folder case. For now, just don't show the last used 407 // folder at all to prevent any chance of the user adding a parent 408 // folder to a child folder 409 if (info.lastUsedId != -1 && info.lastUsedId != info.id 410 && !mEditingFolder) { 411 if (setAccount && info.lastUsedId != mRootFolder 412 && TextUtils.equals(info.lastUsedAccountName, info.accountName) 413 && TextUtils.equals(info.lastUsedAccountType, info.accountType)) { 414 mFolderAdapter.addRecentFolder(info.lastUsedId, info.lastUsedTitle); 415 } else if (!setAccount) { 416 setAccount = true; 417 setAccount(info.lastUsedAccountName, info.lastUsedAccountType); 418 if (info.lastUsedId != mRootFolder) { 419 mFolderAdapter.addRecentFolder(info.lastUsedId, 420 info.lastUsedTitle); 421 } 422 } 423 } 424 if (!setAccount) { 425 mAccountSpinner.setSelection(0); 426 } 427 } 428 429 @Override 430 public Loader<EditBookmarkInfo> onCreateLoader(int id, Bundle args) { 431 return new EditBookmarkInfoLoader(AddBookmarkPage.this, mMap); 432 } 433 }; 434 435 void setAccount(String accountName, String accountType) { 436 for (int i = 0; i < mAccountAdapter.getCount(); i++) { 437 BookmarkAccount account = mAccountAdapter.getItem(i); 438 if (TextUtils.equals(account.accountName, accountName) 439 && TextUtils.equals(account.accountType, accountType)) { 440 onRootFolderFound(account.rootFolderId); 441 mAccountSpinner.setSelection(i); 442 return; 443 } 444 } 445 } 446 447 @Override 448 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 449 String[] projection; 450 switch (id) { 451 case LOADER_ID_ACCOUNTS: 452 return new AccountsLoader(this); 453 case LOADER_ID_FOLDER_CONTENTS: 454 projection = new String[] { 455 BrowserContract.Bookmarks._ID, 456 BrowserContract.Bookmarks.TITLE, 457 BrowserContract.Bookmarks.IS_FOLDER 458 }; 459 String where = BrowserContract.Bookmarks.IS_FOLDER + " != 0"; 460 String whereArgs[] = null; 461 if (mEditingFolder) { 462 where += " AND " + BrowserContract.Bookmarks._ID + " != ?"; 463 whereArgs = new String[] { Long.toString(mMap.getLong( 464 BrowserContract.Bookmarks._ID)) }; 465 } 466 long currentFolder; 467 Object data = mCrumbs.getTopData(); 468 if (data != null) { 469 currentFolder = ((Folder) data).Id; 470 } else { 471 currentFolder = mRootFolder; 472 } 473 return new CursorLoader(this, 474 getUriForFolder(currentFolder), 475 projection, 476 where, 477 whereArgs, 478 BrowserContract.Bookmarks._ID + " ASC"); 479 default: 480 throw new AssertionError("Asking for nonexistant loader!"); 481 } 482 } 483 484 @Override 485 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 486 switch (loader.getId()) { 487 case LOADER_ID_ACCOUNTS: 488 mAccountAdapter.clear(); 489 while (cursor.moveToNext()) { 490 mAccountAdapter.add(new BookmarkAccount(this, cursor)); 491 } 492 getLoaderManager().destroyLoader(LOADER_ID_ACCOUNTS); 493 getLoaderManager().restartLoader(LOADER_ID_EDIT_INFO, null, 494 mEditInfoLoaderCallbacks); 495 break; 496 case LOADER_ID_FOLDER_CONTENTS: 497 mAdapter.changeCursor(cursor); 498 break; 499 } 500 } 501 502 public void onLoaderReset(Loader<Cursor> loader) { 503 switch (loader.getId()) { 504 case LOADER_ID_FOLDER_CONTENTS: 505 mAdapter.changeCursor(null); 506 break; 507 } 508 } 509 510 /** 511 * Move cursor to the position that has folderToFind as its "_id". 512 * @param cursor Cursor containing folders in the bookmarks database 513 * @param folderToFind "_id" of the folder to move to. 514 * @param idIndex Index in cursor of "_id" 515 * @throws AssertionError if cursor is empty or there is no row with folderToFind 516 * as its "_id". 517 */ 518 void moveCursorToFolder(Cursor cursor, long folderToFind, int idIndex) 519 throws AssertionError { 520 if (!cursor.moveToFirst()) { 521 throw new AssertionError("No folders in the database!"); 522 } 523 long folder; 524 do { 525 folder = cursor.getLong(idIndex); 526 } while (folder != folderToFind && cursor.moveToNext()); 527 if (cursor.isAfterLast()) { 528 throw new AssertionError("Folder(id=" + folderToFind 529 + ") holding this bookmark does not exist!"); 530 } 531 } 532 533 @Override 534 public void onItemClick(AdapterView<?> parent, View view, int position, 535 long id) { 536 TextView tv = (TextView) view.findViewById(android.R.id.text1); 537 // Switch to the folder that was clicked on. 538 descendInto(tv.getText().toString(), id); 539 } 540 541 private void setShowFolderNamer(boolean show) { 542 if (show != mIsFolderNamerShowing) { 543 mIsFolderNamerShowing = show; 544 if (show) { 545 // Set the selection to the folder namer so it will be in 546 // view. 547 mListView.addFooterView(mFolderNamerHolder); 548 } else { 549 mListView.removeFooterView(mFolderNamerHolder); 550 } 551 // Refresh the list. 552 mListView.setAdapter(mAdapter); 553 if (show) { 554 mListView.setSelection(mListView.getCount() - 1); 555 } 556 } 557 } 558 559 /** 560 * Shows a list of names of folders. 561 */ 562 private class FolderAdapter extends CursorAdapter { 563 public FolderAdapter(Context context) { 564 super(context, null); 565 } 566 567 @Override 568 public void bindView(View view, Context context, Cursor cursor) { 569 ((TextView) view.findViewById(android.R.id.text1)).setText( 570 cursor.getString(cursor.getColumnIndexOrThrow( 571 BrowserContract.Bookmarks.TITLE))); 572 } 573 574 @Override 575 public View newView(Context context, Cursor cursor, ViewGroup parent) { 576 View view = LayoutInflater.from(context).inflate( 577 R.layout.folder_list_item, null); 578 view.setBackgroundDrawable(context.getResources(). 579 getDrawable(android.R.drawable.list_selector_background)); 580 return view; 581 } 582 583 @Override 584 public boolean isEmpty() { 585 // Do not show the empty view if the user is creating a new folder. 586 return super.isEmpty() && !mIsFolderNamerShowing; 587 } 588 } 589 590 @Override 591 protected void onCreate(Bundle icicle) { 592 super.onCreate(icicle); 593 requestWindowFeature(Window.FEATURE_NO_TITLE); 594 595 mMap = getIntent().getExtras(); 596 597 setContentView(R.layout.browser_add_bookmark); 598 599 Window window = getWindow(); 600 601 String title = null; 602 String url = null; 603 604 mFakeTitle = (TextView) findViewById(R.id.fake_title); 605 606 if (mMap != null) { 607 Bundle b = mMap.getBundle(EXTRA_EDIT_BOOKMARK); 608 if (b != null) { 609 mEditingFolder = mMap.getBoolean(EXTRA_IS_FOLDER, false); 610 mMap = b; 611 mEditingExisting = true; 612 mFakeTitle.setText(R.string.edit_bookmark); 613 if (mEditingFolder) { 614 findViewById(R.id.row_address).setVisibility(View.GONE); 615 } else { 616 showRemoveButton(); 617 } 618 } else { 619 int gravity = mMap.getInt("gravity", -1); 620 if (gravity != -1) { 621 WindowManager.LayoutParams l = window.getAttributes(); 622 l.gravity = gravity; 623 window.setAttributes(l); 624 } 625 } 626 title = mMap.getString(BrowserContract.Bookmarks.TITLE); 627 url = mOriginalUrl = mMap.getString(BrowserContract.Bookmarks.URL); 628 mTouchIconUrl = mMap.getString(TOUCH_ICON_URL); 629 mCurrentFolder = mMap.getLong(BrowserContract.Bookmarks.PARENT, DEFAULT_FOLDER_ID); 630 } 631 632 mTitle = (EditText) findViewById(R.id.title); 633 mTitle.setText(title); 634 635 mAddress = (EditText) findViewById(R.id.address); 636 mAddress.setText(url); 637 638 mButton = (TextView) findViewById(R.id.OK); 639 mButton.setOnClickListener(this); 640 641 mCancelButton = findViewById(R.id.cancel); 642 mCancelButton.setOnClickListener(this); 643 644 mFolder = (FolderSpinner) findViewById(R.id.folder); 645 mFolderAdapter = new FolderSpinnerAdapter(this, !mEditingFolder); 646 mFolder.setAdapter(mFolderAdapter); 647 mFolder.setOnSetSelectionListener(this); 648 649 mDefaultView = findViewById(R.id.default_view); 650 mFolderSelector = findViewById(R.id.folder_selector); 651 652 mFolderNamerHolder = getLayoutInflater().inflate(R.layout.new_folder_layout, null); 653 mFolderNamer = (EditText) mFolderNamerHolder.findViewById(R.id.folder_namer); 654 mFolderNamer.setOnEditorActionListener(this); 655 mFolderCancel = mFolderNamerHolder.findViewById(R.id.close); 656 mFolderCancel.setOnClickListener(this); 657 658 mAddNewFolder = findViewById(R.id.add_new_folder); 659 mAddNewFolder.setOnClickListener(this); 660 mAddSeparator = findViewById(R.id.add_divider); 661 662 mCrumbs = (BreadCrumbView) findViewById(R.id.crumbs); 663 mCrumbs.setUseBackButton(true); 664 mCrumbs.setController(this); 665 mHeaderIcon = getResources().getDrawable(R.drawable.ic_folder_holo_dark); 666 mCrumbHolder = findViewById(R.id.crumb_holder); 667 mCrumbs.setMaxVisible(MAX_CRUMBS_SHOWN); 668 669 mAdapter = new FolderAdapter(this); 670 mListView = (CustomListView) findViewById(R.id.list); 671 View empty = findViewById(R.id.empty); 672 mListView.setEmptyView(empty); 673 mListView.setAdapter(mAdapter); 674 mListView.setOnItemClickListener(this); 675 mListView.addEditText(mFolderNamer); 676 677 mAccountAdapter = new ArrayAdapter<BookmarkAccount>(this, 678 android.R.layout.simple_spinner_item); 679 mAccountAdapter.setDropDownViewResource( 680 android.R.layout.simple_spinner_dropdown_item); 681 mAccountSpinner = (Spinner) findViewById(R.id.accounts); 682 mAccountSpinner.setAdapter(mAccountAdapter); 683 mAccountSpinner.setOnItemSelectedListener(this); 684 685 686 mFakeTitleHolder = findViewById(R.id.title_holder); 687 688 if (!window.getDecorView().isInTouchMode()) { 689 mButton.requestFocus(); 690 } 691 692 getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this); 693 } 694 695 private void showRemoveButton() { 696 findViewById(R.id.remove_divider).setVisibility(View.VISIBLE); 697 mRemoveLink = findViewById(R.id.remove); 698 mRemoveLink.setVisibility(View.VISIBLE); 699 mRemoveLink.setOnClickListener(this); 700 } 701 702 // Called once we have determined which folder is the root folder 703 private void onRootFolderFound(long root) { 704 mRootFolder = root; 705 mCurrentFolder = mRootFolder; 706 setupTopCrumb(); 707 onCurrentFolderFound(); 708 } 709 710 private void setupTopCrumb() { 711 mCrumbs.clear(); 712 String name = getString(R.string.bookmarks); 713 mTopLevelLabel = (TextView) mCrumbs.pushView(name, false, 714 new Folder(name, mRootFolder)); 715 // To better match the other folders. 716 mTopLevelLabel.setCompoundDrawablePadding(6); 717 } 718 719 private void onCurrentFolderFound() { 720 LoaderManager manager = getLoaderManager(); 721 if (mCurrentFolder != mRootFolder) { 722 // Since we're not in the root folder, change the selection to other 723 // folder now. The text will get changed once we select the correct 724 // folder. 725 mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 1 : 2); 726 } else { 727 setShowBookmarkIcon(true); 728 if (!mEditingFolder) { 729 // Initially the "Bookmarks" folder should be showing, rather than 730 // the home screen. In the editing folder case, home screen is not 731 // an option, so "Bookmarks" folder is already at the top. 732 mFolder.setSelectionIgnoringSelectionChange(FolderSpinnerAdapter.ROOT_FOLDER); 733 } 734 } 735 // Find the contents of the current folder 736 manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this); 737 } 738 739 /** 740 * Runnable to save a bookmark, so it can be performed in its own thread. 741 */ 742 private class SaveBookmarkRunnable implements Runnable { 743 // FIXME: This should be an async task. 744 private Message mMessage; 745 private Context mContext; 746 public SaveBookmarkRunnable(Context ctx, Message msg) { 747 mContext = ctx.getApplicationContext(); 748 mMessage = msg; 749 } 750 public void run() { 751 // Unbundle bookmark data. 752 Bundle bundle = mMessage.getData(); 753 String title = bundle.getString(BrowserContract.Bookmarks.TITLE); 754 String url = bundle.getString(BrowserContract.Bookmarks.URL); 755 boolean invalidateThumbnail = bundle.getBoolean(REMOVE_THUMBNAIL); 756 Bitmap thumbnail = invalidateThumbnail ? null 757 : (Bitmap) bundle.getParcelable(BrowserContract.Bookmarks.THUMBNAIL); 758 String touchIconUrl = bundle.getString(TOUCH_ICON_URL); 759 760 // Save to the bookmarks DB. 761 try { 762 final ContentResolver cr = getContentResolver(); 763 Bookmarks.addBookmark(AddBookmarkPage.this, false, url, 764 title, thumbnail, mCurrentFolder); 765 if (touchIconUrl != null) { 766 new DownloadTouchIcon(mContext, cr, url).execute(mTouchIconUrl); 767 } 768 mMessage.arg1 = 1; 769 } catch (IllegalStateException e) { 770 mMessage.arg1 = 0; 771 } 772 mMessage.sendToTarget(); 773 } 774 } 775 776 private static class UpdateBookmarkTask extends AsyncTask<ContentValues, Void, Void> { 777 Context mContext; 778 Long mId; 779 780 public UpdateBookmarkTask(Context context, long id) { 781 mContext = context.getApplicationContext(); 782 mId = id; 783 } 784 785 @Override 786 protected Void doInBackground(ContentValues... params) { 787 if (params.length != 1) { 788 throw new IllegalArgumentException("No ContentValues provided!"); 789 } 790 Uri uri = ContentUris.withAppendedId(BookmarkUtils.getBookmarksUri(mContext), mId); 791 mContext.getContentResolver().update( 792 uri, 793 params[0], null, null); 794 return null; 795 } 796 } 797 798 private void createHandler() { 799 if (mHandler == null) { 800 mHandler = new Handler() { 801 @Override 802 public void handleMessage(Message msg) { 803 switch (msg.what) { 804 case SAVE_BOOKMARK: 805 if (1 == msg.arg1) { 806 Toast.makeText(AddBookmarkPage.this, R.string.bookmark_saved, 807 Toast.LENGTH_LONG).show(); 808 } else { 809 Toast.makeText(AddBookmarkPage.this, R.string.bookmark_not_saved, 810 Toast.LENGTH_LONG).show(); 811 } 812 break; 813 case TOUCH_ICON_DOWNLOADED: 814 Bundle b = msg.getData(); 815 sendBroadcast(BookmarkUtils.createAddToHomeIntent( 816 AddBookmarkPage.this, 817 b.getString(BrowserContract.Bookmarks.URL), 818 b.getString(BrowserContract.Bookmarks.TITLE), 819 (Bitmap) b.getParcelable(BrowserContract.Bookmarks.TOUCH_ICON), 820 (Bitmap) b.getParcelable(BrowserContract.Bookmarks.FAVICON))); 821 break; 822 case BOOKMARK_DELETED: 823 finish(); 824 break; 825 } 826 } 827 }; 828 } 829 } 830 831 /** 832 * Parse the data entered in the dialog and post a message to update the bookmarks database. 833 */ 834 boolean save() { 835 createHandler(); 836 837 String title = mTitle.getText().toString().trim(); 838 String unfilteredUrl; 839 unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString()); 840 841 boolean emptyTitle = title.length() == 0; 842 boolean emptyUrl = unfilteredUrl.trim().length() == 0; 843 Resources r = getResources(); 844 if (emptyTitle || (emptyUrl && !mEditingFolder)) { 845 if (emptyTitle) { 846 mTitle.setError(r.getText(R.string.bookmark_needs_title)); 847 } 848 if (emptyUrl) { 849 mAddress.setError(r.getText(R.string.bookmark_needs_url)); 850 } 851 return false; 852 853 } 854 String url = unfilteredUrl.trim(); 855 if (!mEditingFolder) { 856 try { 857 // We allow bookmarks with a javascript: scheme, but these will in most cases 858 // fail URI parsing, so don't try it if that's the kind of bookmark we have. 859 860 if (!url.toLowerCase().startsWith("javascript:")) { 861 URI uriObj = new URI(url); 862 String scheme = uriObj.getScheme(); 863 if (!Bookmarks.urlHasAcceptableScheme(url)) { 864 // If the scheme was non-null, let the user know that we 865 // can't save their bookmark. If it was null, we'll assume 866 // they meant http when we parse it in the WebAddress class. 867 if (scheme != null) { 868 mAddress.setError(r.getText(R.string.bookmark_cannot_save_url)); 869 return false; 870 } 871 WebAddress address; 872 try { 873 address = new WebAddress(unfilteredUrl); 874 } catch (ParseException e) { 875 throw new URISyntaxException("", ""); 876 } 877 if (address.getHost().length() == 0) { 878 throw new URISyntaxException("", ""); 879 } 880 url = address.toString(); 881 } 882 } 883 } catch (URISyntaxException e) { 884 mAddress.setError(r.getText(R.string.bookmark_url_not_valid)); 885 return false; 886 } 887 } 888 889 if (mSaveToHomeScreen) { 890 mEditingExisting = false; 891 } 892 893 boolean urlUnmodified = url.equals(mOriginalUrl); 894 895 if (mEditingExisting) { 896 Long id = mMap.getLong(BrowserContract.Bookmarks._ID); 897 ContentValues values = new ContentValues(); 898 values.put(BrowserContract.Bookmarks.TITLE, title); 899 values.put(BrowserContract.Bookmarks.PARENT, mCurrentFolder); 900 if (!mEditingFolder) { 901 values.put(BrowserContract.Bookmarks.URL, url); 902 if (!urlUnmodified) { 903 values.putNull(BrowserContract.Bookmarks.THUMBNAIL); 904 } 905 } 906 if (values.size() > 0) { 907 new UpdateBookmarkTask(getApplicationContext(), id).execute(values); 908 } 909 setResult(RESULT_OK); 910 } else { 911 Bitmap thumbnail; 912 Bitmap favicon; 913 if (urlUnmodified) { 914 thumbnail = (Bitmap) mMap.getParcelable( 915 BrowserContract.Bookmarks.THUMBNAIL); 916 favicon = (Bitmap) mMap.getParcelable( 917 BrowserContract.Bookmarks.FAVICON); 918 } else { 919 thumbnail = null; 920 favicon = null; 921 } 922 923 Bundle bundle = new Bundle(); 924 bundle.putString(BrowserContract.Bookmarks.TITLE, title); 925 bundle.putString(BrowserContract.Bookmarks.URL, url); 926 bundle.putParcelable(BrowserContract.Bookmarks.FAVICON, favicon); 927 928 if (mSaveToHomeScreen) { 929 if (mTouchIconUrl != null && urlUnmodified) { 930 Message msg = Message.obtain(mHandler, 931 TOUCH_ICON_DOWNLOADED); 932 msg.setData(bundle); 933 DownloadTouchIcon icon = new DownloadTouchIcon(this, msg, 934 mMap.getString(USER_AGENT)); 935 icon.execute(mTouchIconUrl); 936 } else { 937 sendBroadcast(BookmarkUtils.createAddToHomeIntent(this, url, 938 title, null /*touchIcon*/, favicon)); 939 } 940 } else { 941 bundle.putParcelable(BrowserContract.Bookmarks.THUMBNAIL, thumbnail); 942 bundle.putBoolean(REMOVE_THUMBNAIL, !urlUnmodified); 943 bundle.putString(TOUCH_ICON_URL, mTouchIconUrl); 944 // Post a message to write to the DB. 945 Message msg = Message.obtain(mHandler, SAVE_BOOKMARK); 946 msg.setData(bundle); 947 // Start a new thread so as to not slow down the UI 948 Thread t = new Thread(new SaveBookmarkRunnable(getApplicationContext(), msg)); 949 t.start(); 950 } 951 setResult(RESULT_OK); 952 LogTag.logBookmarkAdded(url, "bookmarkview"); 953 } 954 return true; 955 } 956 957 @Override 958 public void onItemSelected(AdapterView<?> parent, View view, int position, 959 long id) { 960 if (mAccountSpinner == parent) { 961 long root = mAccountAdapter.getItem(position).rootFolderId; 962 if (root != mRootFolder) { 963 onRootFolderFound(root); 964 mFolderAdapter.clearRecentFolder(); 965 } 966 } 967 } 968 969 @Override 970 public void onNothingSelected(AdapterView<?> parent) { 971 // Don't care 972 } 973 974 /* 975 * Class used as a proxy for the InputMethodManager to get to mFolderNamer 976 */ 977 public static class CustomListView extends ListView { 978 private EditText mEditText; 979 980 public void addEditText(EditText editText) { 981 mEditText = editText; 982 } 983 984 public CustomListView(Context context) { 985 super(context); 986 } 987 988 public CustomListView(Context context, AttributeSet attrs) { 989 super(context, attrs); 990 } 991 992 public CustomListView(Context context, AttributeSet attrs, int defStyle) { 993 super(context, attrs, defStyle); 994 } 995 996 @Override 997 public boolean checkInputConnectionProxy(View view) { 998 return view == mEditText; 999 } 1000 } 1001 1002 static class AccountsLoader extends CursorLoader { 1003 1004 static final String[] PROJECTION = new String[] { 1005 Accounts.ACCOUNT_NAME, 1006 Accounts.ACCOUNT_TYPE, 1007 Accounts.ROOT_ID, 1008 }; 1009 1010 static final int COLUMN_INDEX_ACCOUNT_NAME = 0; 1011 static final int COLUMN_INDEX_ACCOUNT_TYPE = 1; 1012 static final int COLUMN_INDEX_ROOT_ID = 2; 1013 1014 public AccountsLoader(Context context) { 1015 super(context, Accounts.CONTENT_URI, PROJECTION, null, null, null); 1016 } 1017 1018 } 1019 1020 public static class BookmarkAccount { 1021 1022 private String mLabel; 1023 String accountName, accountType; 1024 public long rootFolderId; 1025 1026 public BookmarkAccount(Context context, Cursor cursor) { 1027 accountName = cursor.getString( 1028 AccountsLoader.COLUMN_INDEX_ACCOUNT_NAME); 1029 accountType = cursor.getString( 1030 AccountsLoader.COLUMN_INDEX_ACCOUNT_TYPE); 1031 rootFolderId = cursor.getLong( 1032 AccountsLoader.COLUMN_INDEX_ROOT_ID); 1033 mLabel = accountName; 1034 if (TextUtils.isEmpty(mLabel)) { 1035 mLabel = context.getString(R.string.local_bookmarks); 1036 } 1037 } 1038 1039 @Override 1040 public String toString() { 1041 return mLabel; 1042 } 1043 } 1044 1045 static class EditBookmarkInfo { 1046 long id = -1; 1047 long parentId = -1; 1048 String parentTitle; 1049 String title; 1050 String accountName; 1051 String accountType; 1052 1053 long lastUsedId = -1; 1054 String lastUsedTitle; 1055 String lastUsedAccountName; 1056 String lastUsedAccountType; 1057 } 1058 1059 static class EditBookmarkInfoLoader extends AsyncTaskLoader<EditBookmarkInfo> { 1060 1061 private Context mContext; 1062 private Bundle mMap; 1063 1064 public EditBookmarkInfoLoader(Context context, Bundle bundle) { 1065 super(context); 1066 mContext = context.getApplicationContext(); 1067 mMap = bundle; 1068 } 1069 1070 @Override 1071 public EditBookmarkInfo loadInBackground() { 1072 final ContentResolver cr = mContext.getContentResolver(); 1073 EditBookmarkInfo info = new EditBookmarkInfo(); 1074 Cursor c = null; 1075 1076 try { 1077 // First, let's lookup the bookmark (check for dupes, get needed info) 1078 String url = mMap.getString(BrowserContract.Bookmarks.URL); 1079 info.id = mMap.getLong(BrowserContract.Bookmarks._ID, -1); 1080 boolean checkForDupe = mMap.getBoolean(CHECK_FOR_DUPE); 1081 if (checkForDupe && info.id == -1 && !TextUtils.isEmpty(url)) { 1082 c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, 1083 new String[] { BrowserContract.Bookmarks._ID}, 1084 BrowserContract.Bookmarks.URL + "=?", 1085 new String[] { url }, null); 1086 if (c.getCount() == 1 && c.moveToFirst()) { 1087 info.id = c.getLong(0); 1088 } 1089 c.close(); 1090 } 1091 if (info.id != -1) { 1092 c = cr.query(ContentUris.withAppendedId( 1093 BrowserContract.Bookmarks.CONTENT_URI, info.id), 1094 new String[] { 1095 BrowserContract.Bookmarks.PARENT, 1096 BrowserContract.Bookmarks.ACCOUNT_NAME, 1097 BrowserContract.Bookmarks.ACCOUNT_TYPE, 1098 BrowserContract.Bookmarks.TITLE}, 1099 null, null, null); 1100 if (c.moveToFirst()) { 1101 info.parentId = c.getLong(0); 1102 info.accountName = c.getString(1); 1103 info.accountType = c.getString(2); 1104 info.title = c.getString(3); 1105 } 1106 c.close(); 1107 c = cr.query(ContentUris.withAppendedId( 1108 BrowserContract.Bookmarks.CONTENT_URI, info.parentId), 1109 new String[] { 1110 BrowserContract.Bookmarks.TITLE,}, 1111 null, null, null); 1112 if (c.moveToFirst()) { 1113 info.parentTitle = c.getString(0); 1114 } 1115 c.close(); 1116 } 1117 1118 // Figure out the last used folder/account 1119 c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, 1120 new String[] { 1121 BrowserContract.Bookmarks.PARENT, 1122 }, null, null, 1123 BrowserContract.Bookmarks.DATE_MODIFIED + " DESC LIMIT 1"); 1124 if (c.moveToFirst()) { 1125 long parent = c.getLong(0); 1126 c.close(); 1127 c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, 1128 new String[] { 1129 BrowserContract.Bookmarks.TITLE, 1130 BrowserContract.Bookmarks.ACCOUNT_NAME, 1131 BrowserContract.Bookmarks.ACCOUNT_TYPE}, 1132 BrowserContract.Bookmarks._ID + "=?", new String[] { 1133 Long.toString(parent)}, null); 1134 if (c.moveToFirst()) { 1135 info.lastUsedId = parent; 1136 info.lastUsedTitle = c.getString(0); 1137 info.lastUsedAccountName = c.getString(1); 1138 info.lastUsedAccountType = c.getString(2); 1139 } 1140 c.close(); 1141 } 1142 } finally { 1143 if (c != null) { 1144 c.close(); 1145 } 1146 } 1147 1148 return info; 1149 } 1150 1151 @Override 1152 protected void onStartLoading() { 1153 forceLoad(); 1154 } 1155 1156 } 1157 1158 } 1159