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 17 package com.android.browser.widget; 18 19 import android.appwidget.AppWidgetManager; 20 import android.content.ContentUris; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.database.Cursor; 25 import android.database.MergeCursor; 26 import android.graphics.Bitmap; 27 import android.graphics.Bitmap.Config; 28 import android.graphics.BitmapFactory; 29 import android.graphics.BitmapFactory.Options; 30 import android.net.Uri; 31 import android.os.Binder; 32 import android.provider.BrowserContract; 33 import android.provider.BrowserContract.Bookmarks; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.widget.RemoteViews; 37 import android.widget.RemoteViewsService; 38 39 import com.android.browser.BrowserActivity; 40 import com.android.browser.R; 41 import com.android.browser.provider.BrowserProvider2; 42 43 import java.io.File; 44 import java.io.FilenameFilter; 45 import java.util.HashSet; 46 import java.util.regex.Matcher; 47 import java.util.regex.Pattern; 48 49 public 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 .apply(); 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 if (widgetStates != null) { 127 for (File f : widgetStates) { 128 Log.w(TAG, "Found orphaned state: " + f.getName()); 129 if (!f.delete()) { 130 f.deleteOnExit(); 131 } 132 } 133 } 134 } 135 136 static class StateFilter implements FilenameFilter { 137 138 static final Pattern sStatePattern = Pattern.compile("widgetState-(\\d+)\\.xml"); 139 HashSet<Integer> mWidgetIds; 140 141 StateFilter(int[] ids) { 142 mWidgetIds = new HashSet<Integer>(); 143 for (int id : ids) { 144 mWidgetIds.add(id); 145 } 146 } 147 148 @Override 149 public boolean accept(File dir, String filename) { 150 Matcher m = sStatePattern.matcher(filename); 151 if (m.matches()) { 152 int id = Integer.parseInt(m.group(1)); 153 if (!mWidgetIds.contains(id)) { 154 return true; 155 } 156 } 157 return false; 158 } 159 160 } 161 162 static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory { 163 private Cursor mBookmarks; 164 private Context mContext; 165 private int mWidgetId; 166 private long mCurrentFolder = -1; 167 private long mRootFolder = -1; 168 private SharedPreferences mPreferences = null; 169 170 public BookmarkFactory(Context context, int widgetId) { 171 mContext = context.getApplicationContext(); 172 mWidgetId = widgetId; 173 } 174 175 void syncState() { 176 if (mPreferences == null) { 177 mPreferences = getWidgetState(mContext, mWidgetId); 178 } 179 long currentFolder = mPreferences.getLong(STATE_CURRENT_FOLDER, -1); 180 mRootFolder = mPreferences.getLong(STATE_ROOT_FOLDER, -1); 181 if (currentFolder != mCurrentFolder) { 182 resetBookmarks(); 183 mCurrentFolder = currentFolder; 184 } 185 } 186 187 void saveState() { 188 if (mPreferences == null) { 189 mPreferences = getWidgetState(mContext, mWidgetId); 190 } 191 mPreferences.edit() 192 .putLong(STATE_CURRENT_FOLDER, mCurrentFolder) 193 .putLong(STATE_ROOT_FOLDER, mRootFolder) 194 .commit(); 195 } 196 197 @Override 198 public int getCount() { 199 if (mBookmarks == null) 200 return 0; 201 return mBookmarks.getCount(); 202 } 203 204 @Override 205 public long getItemId(int position) { 206 return position; 207 } 208 209 @Override 210 public RemoteViews getLoadingView() { 211 return new RemoteViews( 212 mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item); 213 } 214 215 @Override 216 public RemoteViews getViewAt(int position) { 217 if (!mBookmarks.moveToPosition(position)) { 218 return null; 219 } 220 221 long id = mBookmarks.getLong(BOOKMARK_INDEX_ID); 222 String title = mBookmarks.getString(BOOKMARK_INDEX_TITLE); 223 String url = mBookmarks.getString(BOOKMARK_INDEX_URL); 224 boolean isFolder = mBookmarks.getInt(BOOKMARK_INDEX_IS_FOLDER) != 0; 225 226 RemoteViews views; 227 // Two layouts are needed because of b/5387153 228 if (isFolder) { 229 views = new RemoteViews(mContext.getPackageName(), 230 R.layout.bookmarkthumbnailwidget_item_folder); 231 } else { 232 views = new RemoteViews(mContext.getPackageName(), 233 R.layout.bookmarkthumbnailwidget_item); 234 } 235 // Set the title of the bookmark. Use the url as a backup. 236 String displayTitle = title; 237 if (TextUtils.isEmpty(displayTitle)) { 238 // The browser always requires a title for bookmarks, but jic... 239 displayTitle = url; 240 } 241 views.setTextViewText(R.id.label, displayTitle); 242 if (isFolder) { 243 if (id == mCurrentFolder) { 244 id = mBookmarks.getLong(BOOKMARK_INDEX_PARENT_ID); 245 views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_back_holo); 246 } else { 247 views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_holo); 248 } 249 views.setImageViewResource(R.id.favicon, R.drawable.ic_bookmark_widget_bookmark_holo_dark); 250 views.setDrawableParameters(R.id.thumb, true, 0, -1, null, -1); 251 } else { 252 // RemoteViews require a valid bitmap config 253 Options options = new Options(); 254 options.inPreferredConfig = Config.ARGB_8888; 255 Bitmap thumbnail = null, favicon = null; 256 byte[] blob = mBookmarks.getBlob(BOOKMARK_INDEX_THUMBNAIL); 257 views.setDrawableParameters(R.id.thumb, true, 255, -1, null, -1); 258 if (blob != null && blob.length > 0) { 259 thumbnail = BitmapFactory.decodeByteArray( 260 blob, 0, blob.length, options); 261 views.setImageViewBitmap(R.id.thumb, thumbnail); 262 } else { 263 views.setImageViewResource(R.id.thumb, 264 R.drawable.browser_thumbnail); 265 } 266 blob = mBookmarks.getBlob(BOOKMARK_INDEX_FAVICON); 267 if (blob != null && blob.length > 0) { 268 favicon = BitmapFactory.decodeByteArray( 269 blob, 0, blob.length, options); 270 views.setImageViewBitmap(R.id.favicon, favicon); 271 } else { 272 views.setImageViewResource(R.id.favicon, 273 R.drawable.app_web_browser_sm); 274 } 275 } 276 Intent fillin; 277 if (isFolder) { 278 fillin = new Intent(ACTION_CHANGE_FOLDER) 279 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) 280 .putExtra(Bookmarks._ID, id); 281 } else { 282 if (!TextUtils.isEmpty(url)) { 283 fillin = new Intent(Intent.ACTION_VIEW) 284 .addCategory(Intent.CATEGORY_BROWSABLE) 285 .setData(Uri.parse(url)); 286 } else { 287 fillin = new Intent(BrowserActivity.ACTION_SHOW_BROWSER); 288 } 289 } 290 views.setOnClickFillInIntent(R.id.list_item, fillin); 291 return views; 292 } 293 294 @Override 295 public int getViewTypeCount() { 296 return 2; 297 } 298 299 @Override 300 public boolean hasStableIds() { 301 return false; 302 } 303 304 @Override 305 public void onCreate() { 306 } 307 308 @Override 309 public void onDestroy() { 310 if (mBookmarks != null) { 311 mBookmarks.close(); 312 mBookmarks = null; 313 } 314 deleteWidgetState(mContext, mWidgetId); 315 } 316 317 @Override 318 public void onDataSetChanged() { 319 long token = Binder.clearCallingIdentity(); 320 syncState(); 321 if (mRootFolder < 0 || mCurrentFolder < 0) { 322 // This shouldn't happen, but JIC default to the local account 323 mRootFolder = BrowserProvider2.FIXED_ID_ROOT; 324 mCurrentFolder = mRootFolder; 325 saveState(); 326 } 327 loadBookmarks(); 328 Binder.restoreCallingIdentity(token); 329 } 330 331 private void resetBookmarks() { 332 if (mBookmarks != null) { 333 mBookmarks.close(); 334 mBookmarks = null; 335 } 336 } 337 338 void loadBookmarks() { 339 resetBookmarks(); 340 341 Uri uri = ContentUris.withAppendedId( 342 BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER, 343 mCurrentFolder); 344 mBookmarks = mContext.getContentResolver().query(uri, PROJECTION, 345 null, null, null); 346 if (mCurrentFolder != mRootFolder) { 347 uri = ContentUris.withAppendedId( 348 BrowserContract.Bookmarks.CONTENT_URI, 349 mCurrentFolder); 350 Cursor c = mContext.getContentResolver().query(uri, PROJECTION, 351 null, null, null); 352 mBookmarks = new MergeCursor(new Cursor[] { c, mBookmarks }); 353 } 354 } 355 } 356 357 } 358