BookmarkThumbnailWidgetService.java revision 9b8cd1e564984874f2a6f5cc9bdc68cda8aa15ce
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 android.appwidget.AppWidgetManager; 20import android.content.ContentUris; 21import android.content.Context; 22import android.content.Intent; 23import android.content.SharedPreferences; 24import android.database.Cursor; 25import android.database.MergeCursor; 26import android.graphics.Bitmap; 27import android.graphics.Bitmap.Config; 28import android.graphics.BitmapFactory; 29import android.graphics.BitmapFactory.Options; 30import android.net.Uri; 31import android.os.Binder; 32import android.provider.BrowserContract; 33import android.provider.BrowserContract.Bookmarks; 34import android.text.TextUtils; 35import android.util.Log; 36import android.widget.RemoteViews; 37import android.widget.RemoteViewsService; 38 39import com.android.browser.BrowserActivity; 40import com.android.browser.R; 41import com.android.browser.provider.BrowserProvider2; 42 43import java.io.File; 44import java.io.FilenameFilter; 45import java.util.HashSet; 46import java.util.regex.Matcher; 47import java.util.regex.Pattern; 48 49public class BookmarkThumbnailWidgetService extends RemoteViewsService { 50 51 static final String TAG = "BookmarkThumbnailWidgetService"; 52 static final String ACTION_CHANGE_FOLDER 53 = "com.android.browser.widget.CHANGE_FOLDER"; 54 55 static final String STATE_CURRENT_FOLDER = "current_folder"; 56 static final String STATE_ROOT_FOLDER = "root_folder"; 57 58 private static final String[] PROJECTION = new String[] { 59 BrowserContract.Bookmarks._ID, 60 BrowserContract.Bookmarks.TITLE, 61 BrowserContract.Bookmarks.URL, 62 BrowserContract.Bookmarks.FAVICON, 63 BrowserContract.Bookmarks.IS_FOLDER, 64 BrowserContract.Bookmarks.POSITION, /* needed for order by */ 65 BrowserContract.Bookmarks.THUMBNAIL, 66 BrowserContract.Bookmarks.PARENT}; 67 private static final int BOOKMARK_INDEX_ID = 0; 68 private static final int BOOKMARK_INDEX_TITLE = 1; 69 private static final int BOOKMARK_INDEX_URL = 2; 70 private static final int BOOKMARK_INDEX_FAVICON = 3; 71 private static final int BOOKMARK_INDEX_IS_FOLDER = 4; 72 private static final int BOOKMARK_INDEX_THUMBNAIL = 6; 73 private static final int BOOKMARK_INDEX_PARENT_ID = 7; 74 75 @Override 76 public RemoteViewsFactory onGetViewFactory(Intent intent) { 77 int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); 78 if (widgetId < 0) { 79 Log.w(TAG, "Missing EXTRA_APPWIDGET_ID!"); 80 return null; 81 } 82 return new BookmarkFactory(getApplicationContext(), widgetId); 83 } 84 85 static SharedPreferences getWidgetState(Context context, int widgetId) { 86 return context.getSharedPreferences( 87 String.format("widgetState-%d", widgetId), 88 Context.MODE_PRIVATE); 89 } 90 91 static void deleteWidgetState(Context context, int widgetId) { 92 File file = context.getSharedPrefsFile( 93 String.format("widgetState-%d", widgetId)); 94 if (file.exists()) { 95 if (!file.delete()) { 96 file.deleteOnExit(); 97 } 98 } 99 } 100 101 static void changeFolder(Context context, Intent intent) { 102 int wid = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); 103 long fid = intent.getLongExtra(Bookmarks._ID, -1); 104 if (wid >= 0 && fid >= 0) { 105 SharedPreferences prefs = getWidgetState(context, wid); 106 prefs.edit().putLong(STATE_CURRENT_FOLDER, fid).commit(); 107 AppWidgetManager.getInstance(context) 108 .notifyAppWidgetViewDataChanged(wid, R.id.bookmarks_list); 109 } 110 } 111 112 static void setupWidgetState(Context context, int widgetId, long rootFolder) { 113 SharedPreferences pref = getWidgetState(context, widgetId); 114 pref.edit() 115 .putLong(STATE_CURRENT_FOLDER, rootFolder) 116 .putLong(STATE_ROOT_FOLDER, rootFolder) 117 .commit(); 118 } 119 120 /** 121 * Checks for any state files that may have not received onDeleted 122 */ 123 static void removeOrphanedStates(Context context, int[] widgetIds) { 124 File prefsDirectory = context.getSharedPrefsFile("null").getParentFile(); 125 File[] widgetStates = prefsDirectory.listFiles(new StateFilter(widgetIds)); 126 for (File f : widgetStates) { 127 Log.w(TAG, "Found orphaned state: " + f.getName()); 128 if (!f.delete()) { 129 f.deleteOnExit(); 130 } 131 } 132 } 133 134 static class StateFilter implements FilenameFilter { 135 136 static final Pattern sStatePattern = Pattern.compile("widgetState-(\\d+)\\.xml"); 137 HashSet<Integer> mWidgetIds; 138 139 StateFilter(int[] ids) { 140 mWidgetIds = new HashSet<Integer>(); 141 for (int id : ids) { 142 mWidgetIds.add(id); 143 } 144 } 145 146 @Override 147 public boolean accept(File dir, String filename) { 148 Matcher m = sStatePattern.matcher(filename); 149 if (m.matches()) { 150 int id = Integer.parseInt(m.group(1)); 151 if (!mWidgetIds.contains(id)) { 152 return true; 153 } 154 } 155 return false; 156 } 157 158 } 159 160 static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory { 161 private Cursor mBookmarks; 162 private Context mContext; 163 private int mWidgetId; 164 private long mCurrentFolder = -1; 165 private long mRootFolder = -1; 166 private SharedPreferences mPreferences = null; 167 168 public BookmarkFactory(Context context, int widgetId) { 169 mContext = context; 170 mWidgetId = widgetId; 171 } 172 173 void syncState() { 174 if (mPreferences == null) { 175 mPreferences = getWidgetState(mContext, mWidgetId); 176 } 177 long currentFolder = mPreferences.getLong(STATE_CURRENT_FOLDER, -1); 178 mRootFolder = mPreferences.getLong(STATE_ROOT_FOLDER, -1); 179 if (currentFolder != mCurrentFolder) { 180 resetBookmarks(); 181 mCurrentFolder = currentFolder; 182 } 183 } 184 185 void saveState() { 186 if (mPreferences == null) { 187 mPreferences = getWidgetState(mContext, mWidgetId); 188 } 189 mPreferences.edit() 190 .putLong(STATE_CURRENT_FOLDER, mCurrentFolder) 191 .putLong(STATE_ROOT_FOLDER, mRootFolder) 192 .commit(); 193 } 194 195 @Override 196 public int getCount() { 197 if (mBookmarks == null) 198 return 0; 199 return mBookmarks.getCount(); 200 } 201 202 @Override 203 public long getItemId(int position) { 204 return position; 205 } 206 207 @Override 208 public RemoteViews getLoadingView() { 209 return new RemoteViews( 210 mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item); 211 } 212 213 @Override 214 public RemoteViews getViewAt(int position) { 215 if (!mBookmarks.moveToPosition(position)) { 216 return null; 217 } 218 219 long id = mBookmarks.getLong(BOOKMARK_INDEX_ID); 220 String title = mBookmarks.getString(BOOKMARK_INDEX_TITLE); 221 String url = mBookmarks.getString(BOOKMARK_INDEX_URL); 222 boolean isFolder = mBookmarks.getInt(BOOKMARK_INDEX_IS_FOLDER) != 0; 223 224 RemoteViews views = new RemoteViews( 225 mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item); 226 // Set the title of the bookmark. Use the url as a backup. 227 String displayTitle = title; 228 if (TextUtils.isEmpty(displayTitle)) { 229 // The browser always requires a title for bookmarks, but jic... 230 displayTitle = url; 231 } 232 views.setTextViewText(R.id.label, displayTitle); 233 if (isFolder) { 234 if (id == mCurrentFolder) { 235 id = mBookmarks.getLong(BOOKMARK_INDEX_PARENT_ID); 236 views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_back_holo); 237 } else { 238 views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_holo); 239 } 240 views.setImageViewResource(R.id.favicon, R.drawable.ic_bookmark_widget_bookmark_holo_dark); 241 views.setDrawableParameters(R.id.thumb, true, 0, -1, null, -1); 242 } else { 243 // RemoteViews require a valid bitmap config 244 Options options = new Options(); 245 options.inPreferredConfig = Config.ARGB_8888; 246 Bitmap thumbnail = null, favicon = null; 247 byte[] blob = mBookmarks.getBlob(BOOKMARK_INDEX_THUMBNAIL); 248 views.setDrawableParameters(R.id.thumb, true, 255, -1, null, -1); 249 if (blob != null && blob.length > 0) { 250 thumbnail = BitmapFactory.decodeByteArray( 251 blob, 0, blob.length, options); 252 views.setImageViewBitmap(R.id.thumb, thumbnail); 253 } else { 254 views.setImageViewResource(R.id.thumb, 255 R.drawable.browser_thumbnail); 256 } 257 blob = mBookmarks.getBlob(BOOKMARK_INDEX_FAVICON); 258 if (blob != null && blob.length > 0) { 259 favicon = BitmapFactory.decodeByteArray( 260 blob, 0, blob.length, options); 261 views.setImageViewBitmap(R.id.favicon, favicon); 262 } else { 263 views.setImageViewResource(R.id.favicon, 264 R.drawable.app_web_browser_sm); 265 } 266 } 267 Intent fillin; 268 if (isFolder) { 269 fillin = new Intent(ACTION_CHANGE_FOLDER) 270 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) 271 .putExtra(Bookmarks._ID, id); 272 } else { 273 if (!TextUtils.isEmpty(url)) { 274 fillin = new Intent(Intent.ACTION_VIEW) 275 .addCategory(Intent.CATEGORY_BROWSABLE) 276 .setData(Uri.parse(url)); 277 } else { 278 fillin = new Intent(BrowserActivity.ACTION_SHOW_BROWSER); 279 } 280 } 281 views.setOnClickFillInIntent(R.id.list_item, fillin); 282 return views; 283 } 284 285 @Override 286 public int getViewTypeCount() { 287 return 1; 288 } 289 290 @Override 291 public boolean hasStableIds() { 292 return false; 293 } 294 295 @Override 296 public void onCreate() { 297 } 298 299 @Override 300 public void onDestroy() { 301 if (mBookmarks != null) { 302 mBookmarks.close(); 303 mBookmarks = null; 304 } 305 deleteWidgetState(mContext, mWidgetId); 306 } 307 308 @Override 309 public void onDataSetChanged() { 310 long token = Binder.clearCallingIdentity(); 311 syncState(); 312 if (mRootFolder < 0 || mCurrentFolder < 0) { 313 // This shouldn't happen, but JIC default to the local account 314 mRootFolder = BrowserProvider2.FIXED_ID_ROOT; 315 mCurrentFolder = mRootFolder; 316 saveState(); 317 } 318 loadBookmarks(); 319 Binder.restoreCallingIdentity(token); 320 } 321 322 private void resetBookmarks() { 323 if (mBookmarks != null) { 324 mBookmarks.close(); 325 mBookmarks = null; 326 } 327 } 328 329 void loadBookmarks() { 330 resetBookmarks(); 331 332 Uri uri = ContentUris.withAppendedId( 333 BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER, 334 mCurrentFolder); 335 mBookmarks = mContext.getContentResolver().query(uri, PROJECTION, 336 null, null, null); 337 if (mCurrentFolder != mRootFolder) { 338 uri = ContentUris.withAppendedId( 339 BrowserContract.Bookmarks.CONTENT_URI, 340 mCurrentFolder); 341 Cursor c = mContext.getContentResolver().query(uri, PROJECTION, 342 null, null, null); 343 mBookmarks = new MergeCursor(new Cursor[] { c, mBookmarks }); 344 } 345 } 346 } 347 348} 349