AddBookmarkPage.java revision a9bad830efad4c098e6f874704dedc005958aedf
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.provider.BrowserProvider2;
20
21import android.app.Activity;
22import android.app.LoaderManager;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.CursorLoader;
27import android.content.Intent;
28import android.content.Loader;
29import android.content.SharedPreferences;
30import android.content.res.Resources;
31import android.database.Cursor;
32import android.graphics.Bitmap;
33import android.net.ParseException;
34import android.net.WebAddress;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.Message;
38import android.preference.PreferenceManager;
39import android.provider.BrowserContract;
40import android.text.TextUtils;
41import android.view.KeyEvent;
42import android.view.LayoutInflater;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.Window;
46import android.view.inputmethod.EditorInfo;
47import android.view.inputmethod.InputMethodManager;
48import android.widget.AdapterView;
49import android.widget.CursorAdapter;
50import android.widget.EditText;
51import android.widget.ListView;
52import android.widget.TextView;
53import android.widget.Toast;
54
55import java.net.URI;
56import java.net.URISyntaxException;
57import java.util.ArrayList;
58import java.util.Date;
59
60import android.util.Log;
61
62public class AddBookmarkPage extends Activity
63        implements View.OnClickListener, TextView.OnEditorActionListener,
64        AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor> {
65
66    private final String LOGTAG = "Bookmarks";
67
68    // IDs for the CursorLoaders that are used.
69    private final int LOADER_ID_FOLDER_CONTENTS = 0;
70    private final int LOADER_ID_ALL_FOLDERS = 1;
71
72    private EditText    mTitle;
73    private EditText    mAddress;
74    private TextView    mButton;
75    private View        mCancelButton;
76    private boolean     mEditingExisting;
77    private Bundle      mMap;
78    private String      mTouchIconUrl;
79    private Bitmap      mThumbnail;
80    private String      mOriginalUrl;
81    private TextView mFolder;
82    private View mDefaultView;
83    private View mFolderSelector;
84    private EditText mFolderNamer;
85    private View mAddNewFolder;
86    private long mCurrentFolder = 0;
87    private FolderAdapter mAdapter;
88    private ArrayList<Folder> mPaths;
89    private TextView    mPath;
90
91    private static class Folder {
92        String Name;
93        long Id;
94        Folder(String name, long id) {
95            Name = name;
96            Id = id;
97        }
98    }
99
100    // Message IDs
101    private static final int SAVE_BOOKMARK = 100;
102
103    private Handler mHandler;
104
105    @Override
106    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
107        if (v == mFolderNamer) {
108            if (v.getText().length() > 0) {
109                if (actionId == EditorInfo.IME_NULL) {
110                    // Only want to do this once.
111                    if (event.getAction() == KeyEvent.ACTION_UP) {
112                        // Add the folder to the database
113                        ContentValues values = new ContentValues();
114                        values.put(BrowserContract.Bookmarks.TITLE,
115                                v.getText().toString());
116                        values.put(BrowserContract.Bookmarks.IS_FOLDER, 1);
117                        values.put(BrowserContract.Bookmarks.PARENT,
118                                mCurrentFolder);
119                        getContentResolver().insert(
120                                BrowserContract.Bookmarks.CONTENT_URI, values);
121
122                        mFolderNamer.setVisibility(View.GONE);
123                        InputMethodManager.getInstance(this)
124                                .hideSoftInputFromWindow(
125                                mFolderNamer.getWindowToken(), 0);
126                    }
127                    // Steal the key press for both up and down
128                    return true;
129                }
130            }
131        }
132        return false;
133    }
134
135    @Override
136    public void onClick(View v) {
137        if (v == mButton) {
138            if (mFolderSelector.getVisibility() == View.VISIBLE) {
139                // We are showing the folder selector.  This means that the user
140                // has selected a folder.  Go back to the opening page
141                mFolderSelector.setVisibility(View.GONE);
142                mDefaultView.setVisibility(View.VISIBLE);
143                setTitle(R.string.bookmark_this_page);
144            } else if (save()) {
145                finish();
146            }
147        } else if (v == mCancelButton) {
148            finish();
149        } else if (v == mFolder) {
150            switchToFolderSelector();
151        } else if (v == mAddNewFolder) {
152            mFolderNamer.setVisibility(View.VISIBLE);
153            mFolderNamer.setText(R.string.new_folder);
154            mFolderNamer.requestFocus();
155            InputMethodManager.getInstance(this).showSoftInput(mFolderNamer,
156                    InputMethodManager.SHOW_IMPLICIT);
157        }
158    }
159
160    private void switchToFolderSelector() {
161        mDefaultView.setVisibility(View.GONE);
162        mFolderSelector.setVisibility(View.VISIBLE);
163        setTitle(R.string.containing_folder);
164    }
165
166    @Override
167    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
168        String[] projection;
169        switch (id) {
170            case LOADER_ID_ALL_FOLDERS:
171                projection = new String[] {
172                        BrowserContract.Bookmarks._ID,
173                        BrowserContract.Bookmarks.PARENT,
174                        BrowserContract.Bookmarks.TITLE,
175                        BrowserContract.Bookmarks.IS_FOLDER
176                };
177                return new CursorLoader(this,
178                        BrowserContract.Bookmarks.CONTENT_URI,
179                        projection,
180                        BrowserContract.Bookmarks.IS_FOLDER + " != 0",
181                        null,
182                        null);
183            case LOADER_ID_FOLDER_CONTENTS:
184                projection = new String[] {
185                        BrowserContract.Bookmarks._ID,
186                        BrowserContract.Bookmarks.TITLE,
187                        BrowserContract.Bookmarks.IS_FOLDER
188                };
189
190                return new CursorLoader(this,
191                        BrowserContract.Bookmarks.buildFolderUri(
192                        mCurrentFolder),
193                        projection,
194                        BrowserContract.Bookmarks.IS_FOLDER + " != 0",
195                        null,
196                        null);
197            default:
198                throw new AssertionError("Asking for nonexistant loader!");
199        }
200    }
201
202    @Override
203    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
204        switch (loader.getId()) {
205            case LOADER_ID_FOLDER_CONTENTS:
206                mAdapter.changeCursor(cursor);
207                break;
208            case LOADER_ID_ALL_FOLDERS:
209                long parent = mCurrentFolder;
210                int idIndex = cursor.getColumnIndexOrThrow(
211                        BrowserContract.Bookmarks._ID);
212                int titleIndex = cursor.getColumnIndexOrThrow(
213                        BrowserContract.Bookmarks.TITLE);
214                int parentIndex = cursor.getColumnIndexOrThrow(
215                        BrowserContract.Bookmarks.PARENT);
216                while (parent != BrowserProvider2.FIXED_ID_ROOT) {
217                    // First, find the folder corresponding to the current
218                    // folder
219                    if (!cursor.moveToFirst()) {
220                        throw new AssertionError("No folders in the database!");
221                    }
222                    long folder;
223                    do {
224                        folder = cursor.getLong(idIndex);
225                    } while (folder != parent && cursor.moveToNext());
226                    if (cursor.isAfterLast()) {
227                        throw new AssertionError("Folder(id=" + parent
228                                + ") holding this bookmark does not exist!");
229                    }
230                    String name = cursor.getString(titleIndex);
231                    mPaths.add(1, new Folder(name, parent));
232                    parent = cursor.getLong(parentIndex);
233                }
234                getLoaderManager().stopLoader(LOADER_ID_ALL_FOLDERS);
235                updatePathString();
236                break;
237            default:
238                break;
239        }
240    }
241
242    /**
243     * Update the TextViews in both modes to display the full path of the
244     * current location to insert.
245     */
246    private void updatePathString() {
247        String path = mPaths.get(0).Name;
248        int size = mPaths.size();
249        for (int i = 1; i < size; i++) {
250            path += " / " + mPaths.get(i).Name;
251        }
252        mPath.setText(path);
253        mFolder.setText(path);
254    }
255
256    @Override
257    public void onItemClick(AdapterView<?> parent, View view, int position,
258            long id) {
259        // Switch to the folder that was clicked on.
260        mCurrentFolder = id;
261        mPaths.add(new Folder(((TextView) view).getText().toString(), id));
262        updatePathString();
263
264        getLoaderManager().restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
265    }
266
267    /**
268     * Shows a list of names of folders.
269     */
270    private class FolderAdapter extends CursorAdapter {
271        public FolderAdapter(Context context) {
272            super(context, null);
273        }
274
275        @Override
276        public void bindView(View view, Context context, Cursor cursor) {
277            ((TextView) view.findViewById(android.R.id.text1)).setText(
278                    cursor.getString(cursor.getColumnIndexOrThrow(
279                    BrowserContract.Bookmarks.TITLE)));
280        }
281
282        @Override
283        public View newView(Context context, Cursor cursor, ViewGroup parent) {
284            return LayoutInflater.from(context).inflate(
285                    android.R.layout.simple_list_item_1, null);
286        }
287    }
288
289    protected void onCreate(Bundle icicle) {
290        super.onCreate(icicle);
291        requestWindowFeature(Window.FEATURE_LEFT_ICON);
292
293        mMap = getIntent().getExtras();
294
295        setContentView(R.layout.browser_add_bookmark);
296
297        setTitle(R.string.bookmark_this_page);
298        getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.ic_list_bookmark);
299
300        String title = null;
301        String url = null;
302
303        if (mMap != null) {
304            Bundle b = mMap.getBundle("bookmark");
305            if (b != null) {
306                mMap = b;
307                mEditingExisting = true;
308                setTitle(R.string.edit_bookmark);
309            }
310            title = mMap.getString("title");
311            url = mOriginalUrl = mMap.getString("url");
312            mTouchIconUrl = mMap.getString("touch_icon_url");
313            mThumbnail = (Bitmap) mMap.getParcelable("thumbnail");
314            mCurrentFolder = mMap.getLong(BrowserContract.Bookmarks.PARENT, -1);
315        }
316        if (mCurrentFolder == -1) {
317            mCurrentFolder = getBookmarksBarId(this);
318        }
319
320        mTitle = (EditText) findViewById(R.id.title);
321        mTitle.setText(title);
322
323        mAddress = (EditText) findViewById(R.id.address);
324        mAddress.setText(url);
325
326        mButton = (TextView) findViewById(R.id.OK);
327        mButton.setOnClickListener(this);
328
329        mCancelButton = findViewById(R.id.cancel);
330        mCancelButton.setOnClickListener(this);
331
332        mFolder = (TextView) findViewById(R.id.folder);
333        mFolder.setOnClickListener(this);
334
335        mDefaultView = findViewById(R.id.default_view);
336        mFolderSelector = findViewById(R.id.folder_selector);
337
338        mFolderNamer = (EditText) findViewById(R.id.folder_namer);
339        mFolderNamer.setOnEditorActionListener(this);
340
341        mAddNewFolder = findViewById(R.id.add_new_folder);
342        mAddNewFolder.setOnClickListener(this);
343
344        mPath = (TextView) findViewById(R.id.path);
345                ListView list = (ListView) findViewById(R.id.list);
346
347        mPaths = new ArrayList<Folder>();
348        mPaths.add(0, new Folder(getString(R.string.bookmarks), 0));
349        mAdapter = new FolderAdapter(this);
350        list.setAdapter(mAdapter);
351        list.setOnItemClickListener(this);
352        LoaderManager manager = getLoaderManager();
353        if (mCurrentFolder != BrowserProvider2.FIXED_ID_ROOT) {
354            // Find all the folders
355            manager.initLoader(LOADER_ID_ALL_FOLDERS, null, this);
356        }
357        manager.initLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
358
359
360        if (!getWindow().getDecorView().isInTouchMode()) {
361            mButton.requestFocus();
362        }
363    }
364
365    // FIXME: Use a CursorLoader
366    private long getBookmarksBarId(Context context) {
367        SharedPreferences prefs
368                = PreferenceManager.getDefaultSharedPreferences(context);
369        String accountName =
370                prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null);
371        String accountType =
372                prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null);
373        if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
374            return BrowserProvider2.FIXED_ID_ROOT;
375        }
376        Cursor cursor = null;
377        try {
378            cursor = context.getContentResolver().query(
379                    BrowserContract.Bookmarks.CONTENT_URI,
380                    new String[] { BrowserContract.Bookmarks._ID },
381                    BrowserContract.ChromeSyncColumns.SERVER_UNIQUE + "=? AND "
382                            + BrowserContract.Bookmarks.ACCOUNT_NAME + "=? AND "
383                            + BrowserContract.Bookmarks.ACCOUNT_TYPE + "=?",
384                    new String[] {
385                            BrowserContract.ChromeSyncColumns
386                                    .FOLDER_NAME_BOOKMARKS_BAR,
387                            accountName,
388                            accountType },
389                    null);
390            if (cursor != null && cursor.moveToFirst()) {
391                return cursor.getLong(0);
392            }
393        } finally {
394            if (cursor != null) cursor.close();
395        }
396        return BrowserProvider2.FIXED_ID_ROOT;
397    }
398
399    @Override
400    public boolean dispatchKeyEvent (KeyEvent event) {
401        if (mFolderSelector.getVisibility() == View.VISIBLE
402                && KeyEvent.KEYCODE_BACK == event.getKeyCode()) {
403            if (KeyEvent.ACTION_UP == event.getAction()) {
404                int size = mPaths.size();
405                if (1 == size) {
406                    // We have reached the top level
407                    finish();
408                } else {
409                    // Go up a level
410                    mPaths.remove(size - 1);
411                    mCurrentFolder = mPaths.get(size - 2).Id;
412                    updatePathString();
413                    getLoaderManager().restartLoader(LOADER_ID_FOLDER_CONTENTS,
414                            null, this);
415                }
416            }
417            return true;
418        }
419        return super.dispatchKeyEvent(event);
420    }
421
422    /**
423     * Runnable to save a bookmark, so it can be performed in its own thread.
424     */
425    private class SaveBookmarkRunnable implements Runnable {
426        // FIXME: This should be an async task.
427        private Message mMessage;
428        public SaveBookmarkRunnable(Message msg) {
429            mMessage = msg;
430        }
431        public void run() {
432            // Unbundle bookmark data.
433            Bundle bundle = mMessage.getData();
434            String title = bundle.getString("title");
435            String url = bundle.getString("url");
436            boolean invalidateThumbnail = bundle.getBoolean(
437                    "invalidateThumbnail");
438            Bitmap thumbnail = invalidateThumbnail ? null
439                    : (Bitmap) bundle.getParcelable("thumbnail");
440            String touchIconUrl = bundle.getString("touchIconUrl");
441
442            // Save to the bookmarks DB.
443            try {
444                final ContentResolver cr = getContentResolver();
445                Bookmarks.addBookmark(AddBookmarkPage.this, false, url,
446                        title, thumbnail, true, mCurrentFolder);
447                if (touchIconUrl != null) {
448                    new DownloadTouchIcon(AddBookmarkPage.this, cr, url).execute(mTouchIconUrl);
449                }
450                mMessage.arg1 = 1;
451            } catch (IllegalStateException e) {
452                mMessage.arg1 = 0;
453            }
454            mMessage.sendToTarget();
455        }
456    }
457
458    private void createHandler() {
459        if (mHandler == null) {
460            mHandler = new Handler() {
461                @Override
462                public void handleMessage(Message msg) {
463                    switch (msg.what) {
464                        case SAVE_BOOKMARK:
465                            if (1 == msg.arg1) {
466                                Toast.makeText(AddBookmarkPage.this, R.string.bookmark_saved,
467                                        Toast.LENGTH_LONG).show();
468                            } else {
469                                Toast.makeText(AddBookmarkPage.this, R.string.bookmark_not_saved,
470                                        Toast.LENGTH_LONG).show();
471                            }
472                            break;
473                    }
474                }
475            };
476        }
477    }
478
479    /**
480     * Parse the data entered in the dialog and post a message to update the bookmarks database.
481     */
482    boolean save() {
483        createHandler();
484
485        String title = mTitle.getText().toString().trim();
486        String unfilteredUrl;
487        unfilteredUrl = BrowserActivity.fixUrl(mAddress.getText().toString());
488
489        boolean emptyTitle = title.length() == 0;
490        boolean emptyUrl = unfilteredUrl.trim().length() == 0;
491        Resources r = getResources();
492        if (emptyTitle || emptyUrl) {
493            if (emptyTitle) {
494                mTitle.setError(r.getText(R.string.bookmark_needs_title));
495            }
496            if (emptyUrl) {
497                mAddress.setError(r.getText(R.string.bookmark_needs_url));
498            }
499            return false;
500
501        }
502        String url = unfilteredUrl.trim();
503        try {
504            // We allow bookmarks with a javascript: scheme, but these will in most cases
505            // fail URI parsing, so don't try it if that's the kind of bookmark we have.
506
507            if (!url.toLowerCase().startsWith("javascript:")) {
508                URI uriObj = new URI(url);
509                String scheme = uriObj.getScheme();
510                if (!Bookmarks.urlHasAcceptableScheme(url)) {
511                    // If the scheme was non-null, let the user know that we
512                    // can't save their bookmark. If it was null, we'll assume
513                    // they meant http when we parse it in the WebAddress class.
514                    if (scheme != null) {
515                        mAddress.setError(r.getText(R.string.bookmark_cannot_save_url));
516                        return false;
517                    }
518                    WebAddress address;
519                    try {
520                        address = new WebAddress(unfilteredUrl);
521                    } catch (ParseException e) {
522                        throw new URISyntaxException("", "");
523                    }
524                    if (address.mHost.length() == 0) {
525                        throw new URISyntaxException("", "");
526                    }
527                    url = address.toString();
528                }
529            }
530        } catch (URISyntaxException e) {
531            mAddress.setError(r.getText(R.string.bookmark_url_not_valid));
532            return false;
533        }
534
535        if (mEditingExisting) {
536            mMap.putString("title", title);
537            mMap.putString("url", url);
538            mMap.putBoolean("invalidateThumbnail", !url.equals(mOriginalUrl));
539            // FIXME: This does not work yet
540            mMap.putLong(BrowserContract.Bookmarks.PARENT, mCurrentFolder);
541            setResult(RESULT_OK, (new Intent()).setAction(
542                    getIntent().toString()).putExtras(mMap));
543        } else {
544            // Post a message to write to the DB.
545            Bundle bundle = new Bundle();
546            bundle.putString("title", title);
547            bundle.putString("url", url);
548            bundle.putParcelable("thumbnail", mThumbnail);
549            bundle.putBoolean("invalidateThumbnail", !url.equals(mOriginalUrl));
550            bundle.putString("touchIconUrl", mTouchIconUrl);
551            Message msg = Message.obtain(mHandler, SAVE_BOOKMARK);
552            msg.setData(bundle);
553            // Start a new thread so as to not slow down the UI
554            Thread t = new Thread(new SaveBookmarkRunnable(msg));
555            t.start();
556            setResult(RESULT_OK);
557            LogTag.logBookmarkAdded(url, "bookmarkview");
558        }
559        return true;
560    }
561}
562