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