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