BookmarkThumbnailWidgetService.java revision a93982d1ac21696d04775b2758e150ed43c8f651
1/*
2 * Copyright (C) 2010 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.widget;
18
19import com.android.browser.BrowserActivity;
20import com.android.browser.BrowserBookmarksPage;
21import com.android.browser.R;
22
23import android.appwidget.AppWidgetManager;
24import android.content.ContentUris;
25import android.content.Context;
26import android.content.Intent;
27import android.content.SharedPreferences;
28import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
29import android.database.ContentObserver;
30import android.database.Cursor;
31import android.graphics.Bitmap;
32import android.graphics.Bitmap.Config;
33import android.graphics.BitmapFactory;
34import android.graphics.BitmapFactory.Options;
35import android.net.Uri;
36import android.os.AsyncTask;
37import android.os.Handler;
38import android.preference.PreferenceManager;
39import android.provider.BrowserContract;
40import android.provider.BrowserContract.Bookmarks;
41import android.text.TextUtils;
42import android.util.Log;
43import android.widget.RemoteViews;
44import android.widget.RemoteViewsService;
45
46import java.util.ArrayList;
47import java.util.HashMap;
48import java.util.List;
49import java.util.Map;
50import java.util.Stack;
51
52public class BookmarkThumbnailWidgetService extends RemoteViewsService {
53
54    static final String TAG = "BookmarkThumbnailWidgetService";
55    static final boolean USE_FOLDERS = true;
56
57    static final String ACTION_REMOVE_FACTORIES
58            = "com.android.browser.widget.REMOVE_FACTORIES";
59    static final String ACTION_CHANGE_FOLDER
60            = "com.android.browser.widget.CHANGE_FOLDER";
61
62    private static final String[] PROJECTION = new String[] {
63            BrowserContract.Bookmarks._ID,
64            BrowserContract.Bookmarks.TITLE,
65            BrowserContract.Bookmarks.URL,
66            BrowserContract.Bookmarks.FAVICON,
67            BrowserContract.Bookmarks.IS_FOLDER,
68            BrowserContract.Bookmarks.TOUCH_ICON,
69            BrowserContract.Bookmarks.POSITION, /* needed for order by */
70            BrowserContract.Bookmarks.THUMBNAIL};
71    private static final int BOOKMARK_INDEX_ID = 0;
72    private static final int BOOKMARK_INDEX_TITLE = 1;
73    private static final int BOOKMARK_INDEX_URL = 2;
74    private static final int BOOKMARK_INDEX_FAVICON = 3;
75    private static final int BOOKMARK_INDEX_IS_FOLDER = 4;
76    private static final int BOOKMARK_INDEX_TOUCH_ICON = 5;
77    private static final int BOOKMARK_INDEX_THUMBNAIL = 7;
78
79    // The service will likely be destroyed at any time, so we need to keep references to the
80    // factories across services connections.
81    private static final Map<Integer, BookmarkFactory> mFactories =
82            new HashMap<Integer, BookmarkFactory>();
83    private Handler mUiHandler;
84    private BookmarksObserver mBookmarksObserver;
85
86    @Override
87    public void onCreate() {
88        super.onCreate();
89        mUiHandler = new Handler();
90        mBookmarksObserver = new BookmarksObserver(mUiHandler);
91        getContentResolver().registerContentObserver(
92                BrowserContract.Bookmarks.CONTENT_URI, true, mBookmarksObserver);
93    }
94
95    @Override
96    public int onStartCommand(Intent intent, int flags, int startId) {
97        String action = intent.getAction();
98        if (Intent.ACTION_VIEW.equals(action)) {
99            if (intent.getData() == null) {
100                startActivity(new Intent(BrowserActivity.ACTION_SHOW_BROWSER, null,
101                        this, BrowserActivity.class)
102                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
103            } else {
104                Intent view = new Intent(intent);
105                view.setComponent(null);
106                startActivity(view);
107            }
108        } else if (ACTION_REMOVE_FACTORIES.equals(action)) {
109            int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
110            if (ids != null) {
111                for (int id : ids) {
112                    BookmarkFactory bf = mFactories.remove(id);
113                    if (bf != null) {
114                        // Workaround a known framework bug
115                        // onDestroy is currently never called
116                        bf.onDestroy();
117                    }
118                }
119            }
120        } else if (ACTION_CHANGE_FOLDER.equals(action)) {
121            int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
122            long folderId = intent.getLongExtra(Bookmarks._ID, -1);
123            BookmarkFactory fac = mFactories.get(widgetId);
124            if (fac != null && folderId >= 0) {
125                fac.changeFolder(folderId);
126            } else {
127                // This a workaround to the issue when the Browser process crashes, after which
128                // mFactories is not populated (due to onBind() not being called).  Calling
129                // notifyDataSetChanged() will trigger a connection to be made.
130                AppWidgetManager.getInstance(getApplicationContext())
131                    .notifyAppWidgetViewDataChanged(widgetId, R.id.bookmarks_list);
132            }
133        }
134        return START_STICKY;
135    }
136
137    @Override
138    public void onDestroy() {
139        super.onDestroy();
140        getContentResolver().unregisterContentObserver(mBookmarksObserver);
141    }
142
143    private class BookmarksObserver extends ContentObserver {
144        public BookmarksObserver(Handler handler) {
145            super(handler);
146        }
147
148        @Override
149        public void onChange(boolean selfChange) {
150            super.onChange(selfChange);
151
152            // Update all the bookmark widgets
153            if (mFactories != null) {
154                for (BookmarkFactory fac : mFactories.values()) {
155                    fac.loadData();
156                }
157            }
158        }
159    }
160
161    @Override
162    public RemoteViewsFactory onGetViewFactory(Intent intent) {
163        int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
164        if (widgetId < 0) {
165            Log.w(TAG, "Missing EXTRA_APPWIDGET_ID!");
166            return null;
167        } else {
168            BookmarkFactory fac = mFactories.get(widgetId);
169            if (fac == null) {
170                fac = new BookmarkFactory(getApplicationContext(), widgetId);
171            }
172            mFactories.put(widgetId, fac);
173            return fac;
174        }
175    }
176
177    private static class Breadcrumb {
178        long mId;
179        String mTitle;
180        public Breadcrumb(long id, String title) {
181            mId = id;
182            mTitle = title;
183        }
184    }
185
186    static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory,
187            OnSharedPreferenceChangeListener {
188        private List<RenderResult> mBookmarks;
189        private Context mContext;
190        private int mWidgetId;
191        private String mAccountType;
192        private String mAccountName;
193        private Stack<Breadcrumb> mBreadcrumbs;
194        private LoadBookmarksTask mLoadTask;
195
196        public BookmarkFactory(Context context, int widgetId) {
197            mBreadcrumbs = new Stack<Breadcrumb>();
198            mContext = context;
199            mWidgetId = widgetId;
200        }
201
202        void changeFolder(long folderId) {
203            if (mBookmarks == null) return;
204
205            if (!mBreadcrumbs.empty() && mBreadcrumbs.peek().mId == folderId) {
206                mBreadcrumbs.pop();
207                loadData();
208                return;
209            }
210
211            for (RenderResult res : mBookmarks) {
212                if (res.mId == folderId) {
213                    mBreadcrumbs.push(new Breadcrumb(res.mId, res.mTitle));
214                    loadData();
215                    break;
216                }
217            }
218        }
219
220        @Override
221        public int getCount() {
222            if (mBookmarks == null)
223                return 0;
224            return mBookmarks.size();
225        }
226
227        @Override
228        public long getItemId(int position) {
229            return position;
230        }
231
232        @Override
233        public RemoteViews getLoadingView() {
234            return null;
235        }
236
237        @Override
238        public RemoteViews getViewAt(int position) {
239            if (position < 0 || position >= getCount()) {
240                return null;
241            }
242
243            RenderResult res = mBookmarks.get(position);
244            Breadcrumb folder = mBreadcrumbs.empty() ? null : mBreadcrumbs.peek();
245
246            RemoteViews views = new RemoteViews(
247                    mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item);
248            Intent fillin;
249            if (res.mIsFolder) {
250                long nfi = res.mId;
251                fillin = new Intent(ACTION_CHANGE_FOLDER, null,
252                        mContext, BookmarkThumbnailWidgetService.class)
253                        .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId)
254                        .putExtra(Bookmarks._ID, nfi);
255            } else {
256                fillin = new Intent(Intent.ACTION_VIEW)
257                        .addCategory(Intent.CATEGORY_BROWSABLE);
258                if (!TextUtils.isEmpty(res.mUrl)) {
259                    fillin.setData(Uri.parse(res.mUrl));
260                }
261            }
262            views.setOnClickFillInIntent(R.id.list_item, fillin);
263            // Set the title of the bookmark. Use the url as a backup.
264            String displayTitle = res.mTitle;
265            if (TextUtils.isEmpty(displayTitle)) {
266                // The browser always requires a title for bookmarks, but jic...
267                displayTitle = res.mUrl;
268            }
269            views.setTextViewText(R.id.label, displayTitle);
270            if (res.mIsFolder) {
271                if (folder != null && res.mId == folder.mId) {
272                    views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_back_holo);
273                } else {
274                    views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_holo);
275                }
276                views.setImageViewResource(R.id.favicon, R.drawable.ic_bookmark_widget_bookmark_holo_dark);
277                views.setDrawableParameters(R.id.thumb, true, 0, -1, null, -1);
278            } else {
279                views.setDrawableParameters(R.id.thumb, true, 255, -1, null, -1);
280                if (res.mThumbnail != null) {
281                    views.setImageViewBitmap(R.id.thumb, res.mThumbnail);
282                } else {
283                    views.setImageViewResource(R.id.thumb,
284                            R.drawable.browser_thumbnail);
285                }
286                if (res.mIcon != null) {
287                    views.setImageViewBitmap(R.id.favicon, res.mIcon);
288                } else {
289                    views.setImageViewResource(R.id.favicon,
290                            R.drawable.app_web_browser_sm);
291                }
292            }
293            return views;
294        }
295
296        @Override
297        public int getViewTypeCount() {
298            return 1;
299        }
300
301        @Override
302        public boolean hasStableIds() {
303            return false;
304        }
305
306        @Override
307        public void onCreate() {
308            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
309            mAccountType = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null);
310            mAccountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null);
311            prefs.registerOnSharedPreferenceChangeListener(this);
312            loadData();
313        }
314
315        @Override
316        public void onDestroy() {
317            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
318            prefs.unregisterOnSharedPreferenceChangeListener(this);
319
320            // Workaround known framework bug
321            // This class currently leaks, so free as much memory as we can
322            recycleBitmaps();
323            mBookmarks.clear();
324            mBreadcrumbs.clear();
325            if (mLoadTask != null) {
326                mLoadTask.cancel(false);
327                mLoadTask = null;
328            }
329        }
330
331        @Override
332        public void onDataSetChanged() {
333        }
334
335        void loadData() {
336            if (mLoadTask != null) {
337                mLoadTask.cancel(false);
338            }
339            mLoadTask = new LoadBookmarksTask();
340            mLoadTask.execute();
341        }
342
343        class LoadBookmarksTask extends AsyncTask<Void, Void, List<RenderResult>> {
344            private Breadcrumb mFolder;
345
346            @Override
347            protected void onPreExecute() {
348                mFolder = mBreadcrumbs.empty() ? null : mBreadcrumbs.peek();
349            }
350
351            @Override
352            protected List<RenderResult> doInBackground(Void... params) {
353                return loadBookmarks(mFolder);
354            }
355
356            @Override
357            protected void onPostExecute(List<RenderResult> result) {
358                if (!isCancelled() && result != null) {
359                    recycleBitmaps();
360                    mBookmarks = result;
361                    AppWidgetManager.getInstance(mContext)
362                            .notifyAppWidgetViewDataChanged(mWidgetId, R.id.bookmarks_list);
363                }
364            }
365        }
366
367        List<RenderResult> loadBookmarks(Breadcrumb folder) {
368            String where = null;
369            Uri uri;
370            if (USE_FOLDERS) {
371                uri = BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER;
372                if (folder != null) {
373                    uri = ContentUris.withAppendedId(uri, folder.mId);
374                }
375            } else {
376                uri = BrowserContract.Bookmarks.CONTENT_URI;
377                where = Bookmarks.IS_FOLDER + " == 0";
378            }
379            uri = uri.buildUpon()
380                    .appendQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE, mAccountType)
381                    .appendQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME, mAccountName)
382                    .build();
383            Cursor c = null;
384            try {
385                c = mContext.getContentResolver().query(uri, PROJECTION,
386                        where, null, null);
387                if (c != null) {
388                    ArrayList<RenderResult> bookmarks
389                            = new ArrayList<RenderResult>(c.getCount() + 1);
390                    if (folder != null) {
391                        RenderResult res = new RenderResult(
392                                folder.mId, folder.mTitle, null);
393                        res.mIsFolder = true;
394                        bookmarks.add(res);
395                    }
396                    while (c.moveToNext()) {
397                        long id = c.getLong(BOOKMARK_INDEX_ID);
398                        String title = c.getString(BOOKMARK_INDEX_TITLE);
399                        String url = c.getString(BOOKMARK_INDEX_URL);
400                        RenderResult res = new RenderResult(id, title, url);
401                        res.mIsFolder = c.getInt(BOOKMARK_INDEX_IS_FOLDER) != 0;
402                        if (!res.mIsFolder) {
403                            // RemoteViews require a valid bitmap config
404                            Options options = new Options();
405                            options.inPreferredConfig = Config.ARGB_8888;
406                            Bitmap thumbnail = null, favicon = null;
407                            byte[] blob = c.getBlob(BOOKMARK_INDEX_THUMBNAIL);
408                            if (blob != null && blob.length > 0) {
409                                thumbnail = BitmapFactory.decodeByteArray(
410                                        blob, 0, blob.length, options);
411                            }
412                            blob = c.getBlob(BOOKMARK_INDEX_FAVICON);
413                            if (blob != null && blob.length > 0) {
414                                favicon = BitmapFactory.decodeByteArray(
415                                        blob, 0, blob.length, options);
416                            }
417                            res.mThumbnail = thumbnail;
418                            res.mIcon = favicon;
419                        }
420                        bookmarks.add(res);
421                    }
422                    if (bookmarks.size() == 0) {
423                        RenderResult res = new RenderResult(0, "", "");
424                        Bitmap thumbnail = BitmapFactory.decodeResource(
425                                mContext.getResources(),
426                                R.drawable.thumbnail_bookmarks_widget_no_bookmark_holo);
427                        Bitmap favicon = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
428                        res.mThumbnail = thumbnail;
429                        res.mIcon = favicon;
430                        for (int i = 0; i < 6; i++) {
431                            bookmarks.add(res);
432                        }
433                    }
434                    return bookmarks;
435                }
436            } catch (IllegalStateException e) {
437                Log.e(TAG, "update bookmark widget", e);
438            } finally {
439                if (c != null) {
440                    c.close();
441                }
442            }
443            return null;
444        }
445
446        private void recycleBitmaps() {
447            // Do a bit of house cleaning for the system
448            if (mBookmarks != null) {
449                for (RenderResult res : mBookmarks) {
450                    if (res.mThumbnail != null) {
451                        res.mThumbnail.recycle();
452                        res.mThumbnail = null;
453                    }
454                }
455            }
456        }
457
458        @Override
459        public void onSharedPreferenceChanged(
460                SharedPreferences prefs, String key) {
461            if (BrowserBookmarksPage.PREF_ACCOUNT_TYPE.equals(key)) {
462                mAccountType = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null);
463                mBreadcrumbs.clear();
464                loadData();
465            }
466            if (BrowserBookmarksPage.PREF_ACCOUNT_NAME.equals(key)) {
467                mAccountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null);
468                mBreadcrumbs.clear();
469                loadData();
470            }
471        }
472    }
473
474    // Class containing the rendering information for a specific bookmark.
475    private static class RenderResult {
476        final String mTitle;
477        final String mUrl;
478        Bitmap mThumbnail;
479        Bitmap mIcon;
480        boolean mIsFolder;
481        long mId;
482
483        RenderResult(long id, String title, String url) {
484            mId = id;
485            mTitle = title;
486            mUrl = url;
487        }
488
489    }
490
491}
492