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