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