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