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