OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.bookmarkswidget; |
| 6 |
| 7 import android.appwidget.AppWidgetManager; |
| 8 import android.content.Context; |
| 9 import android.content.Intent; |
| 10 import android.content.SharedPreferences; |
| 11 import android.graphics.Bitmap.Config; |
| 12 import android.graphics.BitmapFactory; |
| 13 import android.graphics.BitmapFactory.Options; |
| 14 import android.net.Uri; |
| 15 import android.os.AsyncTask; |
| 16 import android.os.Binder; |
| 17 import android.provider.Browser.BookmarkColumns; |
| 18 import android.text.TextUtils; |
| 19 import android.util.Log; |
| 20 import android.widget.RemoteViews; |
| 21 import android.widget.RemoteViewsService; |
| 22 |
| 23 import com.google.android.apps.chrome.R; |
| 24 import com.google.android.apps.chrome.appwidget.bookmarks.BookmarkThumbnailWidge
tProvider; |
| 25 |
| 26 import org.chromium.base.ThreadUtils; |
| 27 import org.chromium.base.annotations.SuppressFBWarnings; |
| 28 import org.chromium.base.library_loader.ProcessInitException; |
| 29 import org.chromium.chrome.browser.ChromeBrowserProvider.BookmarkNode; |
| 30 import org.chromium.chrome.browser.ChromeBrowserProviderClient; |
| 31 import org.chromium.chrome.browser.ChromiumApplication; |
| 32 import org.chromium.chrome.browser.util.IntentUtils; |
| 33 import org.chromium.sync.AndroidSyncSettings; |
| 34 |
| 35 /** |
| 36 * Service to support bookmarks on the Android home screen |
| 37 */ |
| 38 public class BookmarkThumbnailWidgetService extends RemoteViewsService { |
| 39 |
| 40 static final String TAG = "BookmarkThumbnailWidgetService"; |
| 41 static final String ACTION_CHANGE_FOLDER_SUFFIX = ".CHANGE_FOLDER"; |
| 42 static final String STATE_CURRENT_FOLDER = "current_folder"; |
| 43 |
| 44 @Override |
| 45 public RemoteViewsFactory onGetViewFactory(Intent intent) { |
| 46 int widgetId = IntentUtils.safeGetIntExtra(intent, AppWidgetManager.EXTR
A_APPWIDGET_ID, -1); |
| 47 if (widgetId < 0) { |
| 48 Log.w(TAG, "Missing EXTRA_APPWIDGET_ID!"); |
| 49 return null; |
| 50 } |
| 51 return new BookmarkFactory(this, widgetId); |
| 52 } |
| 53 |
| 54 static String getChangeFolderAction(Context context) { |
| 55 return context.getPackageName() + ACTION_CHANGE_FOLDER_SUFFIX; |
| 56 } |
| 57 |
| 58 private static SharedPreferences getWidgetState(Context context, int widgetI
d) { |
| 59 return context.getSharedPreferences( |
| 60 String.format("widgetState-%d", widgetId), |
| 61 Context.MODE_PRIVATE); |
| 62 } |
| 63 |
| 64 static void deleteWidgetState(Context context, int widgetId) { |
| 65 // Android Browser's widget used private API methods to access the share
d prefs |
| 66 // files and deleted them. This is the best we can do with the public AP
I. |
| 67 SharedPreferences preferences = getWidgetState(context, widgetId); |
| 68 if (preferences != null) preferences.edit().clear().commit(); |
| 69 } |
| 70 |
| 71 static void changeFolder(Context context, Intent intent) { |
| 72 int widgetId = IntentUtils.safeGetIntExtra(intent, AppWidgetManager.EXTR
A_APPWIDGET_ID, -1); |
| 73 long folderId = IntentUtils.safeGetLongExtra(intent, BookmarkColumns._ID
, |
| 74 ChromeBrowserProviderClient.INVALID_BOOKMARK_ID); |
| 75 if (widgetId >= 0 && folderId >= 0) { |
| 76 SharedPreferences prefs = getWidgetState(context, widgetId); |
| 77 prefs.edit().putLong(STATE_CURRENT_FOLDER, folderId).commit(); |
| 78 AppWidgetManager.getInstance(context) |
| 79 .notifyAppWidgetViewDataChanged(widgetId, R.id.bookmarks_lis
t); |
| 80 } |
| 81 } |
| 82 |
| 83 static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactor
y, |
| 84 BookmarkWidgetUpdateListener.UpdateListener { |
| 85 |
| 86 private final ChromiumApplication mContext; |
| 87 private final int mWidgetId; |
| 88 private final SharedPreferences mPreferences; |
| 89 private BookmarkWidgetUpdateListener mUpdateListener; |
| 90 private BookmarkNode mCurrentFolder; |
| 91 private final Object mLock = new Object(); |
| 92 |
| 93 public BookmarkFactory(Context context, int widgetId) { |
| 94 mContext = (ChromiumApplication) context.getApplicationContext(); |
| 95 mWidgetId = widgetId; |
| 96 mPreferences = getWidgetState(mContext, mWidgetId); |
| 97 } |
| 98 |
| 99 private static long getFolderId(BookmarkNode folder) { |
| 100 return folder != null ? folder.id() : ChromeBrowserProviderClient.IN
VALID_BOOKMARK_ID; |
| 101 } |
| 102 |
| 103 @SuppressFBWarnings("DM_EXIT") |
| 104 @Override |
| 105 public void onCreate() { |
| 106 // Required to be applied here redundantly to prevent crashes in the
cases where the |
| 107 // package data is deleted or the Chrome application forced to stop. |
| 108 try { |
| 109 mContext.startBrowserProcessesAndLoadLibrariesSync(mContext, tru
e); |
| 110 } catch (ProcessInitException e) { |
| 111 Log.e(TAG, "Failed to start browser process.", e); |
| 112 // Since the library failed to initialize nothing in the applica
tion |
| 113 // can work, so kill the whole application not just the activity |
| 114 System.exit(-1); |
| 115 } |
| 116 mUpdateListener = new BookmarkWidgetUpdateListener(mContext, this); |
| 117 } |
| 118 |
| 119 @Override |
| 120 public void onDestroy() { |
| 121 if (mUpdateListener != null) mUpdateListener.destroy(); |
| 122 deleteWidgetState(mContext, mWidgetId); |
| 123 } |
| 124 |
| 125 @Override |
| 126 public void onBookmarkModelUpdated() { |
| 127 refreshWidget(); |
| 128 } |
| 129 |
| 130 @Override |
| 131 public void onSyncEnabledStatusUpdated(boolean enabled) { |
| 132 synchronized (mLock) { |
| 133 // Need to operate in a separate thread as it involves queries t
o our provider. |
| 134 new SyncEnabledStatusUpdatedTask(enabled, getFolderId(mCurrentFo
lder)).execute(); |
| 135 } |
| 136 } |
| 137 |
| 138 @Override |
| 139 public void onThumbnailUpdated(String url) { |
| 140 synchronized (mLock) { |
| 141 if (mCurrentFolder == null) return; |
| 142 |
| 143 for (BookmarkNode child : mCurrentFolder.children()) { |
| 144 if (child.isUrl() && url.equals(child.url())) { |
| 145 refreshWidget(); |
| 146 break; |
| 147 } |
| 148 } |
| 149 } |
| 150 } |
| 151 |
| 152 void refreshWidget() { |
| 153 mContext.sendBroadcast(new Intent( |
| 154 BookmarkThumbnailWidgetProviderBase.getBookmarkAppWidgetUpda
teAction(mContext), |
| 155 null, mContext, BookmarkThumbnailWidgetProvider.class) |
| 156 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId)); |
| 157 } |
| 158 |
| 159 void requestFolderChange(long folderId) { |
| 160 mContext.sendBroadcast(new Intent(getChangeFolderAction(mContext)) |
| 161 .setClass(mContext, BookmarkWidgetProxy.class) |
| 162 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId
) |
| 163 .putExtra(BookmarkColumns._ID, folderId)); |
| 164 } |
| 165 |
| 166 // Performs the required checks to trigger an update of the widget after
changing the sync |
| 167 // enable settings. The required provider methods cannot be accessed in
the UI thread. |
| 168 private class SyncEnabledStatusUpdatedTask extends AsyncTask<Void, Void,
Void> { |
| 169 private final boolean mEnabled; |
| 170 private final long mCurrentFolderId; |
| 171 |
| 172 public SyncEnabledStatusUpdatedTask(boolean enabled, long currentFol
derId) { |
| 173 mEnabled = enabled; |
| 174 mCurrentFolderId = currentFolderId; |
| 175 } |
| 176 |
| 177 @Override |
| 178 protected Void doInBackground(Void... params) { |
| 179 // If we're in the Mobile Bookmarks folder the icon to go up the
hierarchy |
| 180 // will either appear or disappear. Need to refresh. |
| 181 long mobileBookmarksFolderId = |
| 182 ChromeBrowserProviderClient.getMobileBookmarksFolderId(m
Context); |
| 183 if (mCurrentFolderId == mobileBookmarksFolderId) { |
| 184 refreshWidget(); |
| 185 return null; |
| 186 } |
| 187 |
| 188 // If disabling sync, we need to move to the Mobile Bookmarks fo
lder if we're |
| 189 // not inside that branch of the bookmark hierarchy (will become
not accessible). |
| 190 if (!mEnabled && !ChromeBrowserProviderClient.isBookmarkInMobile
BookmarksBranch( |
| 191 mContext, mCurrentFolderId)) { |
| 192 requestFolderChange(mobileBookmarksFolderId); |
| 193 } |
| 194 |
| 195 return null; |
| 196 } |
| 197 } |
| 198 |
| 199 // ---------------------------------------------------------------- // |
| 200 // ------- Methods below this line run in different thread -------- // |
| 201 // ---------------------------------------------------------------- // |
| 202 |
| 203 private void syncState() { |
| 204 long currentFolderId = mPreferences.getLong(STATE_CURRENT_FOLDER, |
| 205 ChromeBrowserProviderClient.INVALID_BOOKMARK_ID); |
| 206 |
| 207 // Keep outside the synchronized block to avoid deadlocks in case lo
ading the folder |
| 208 // triggers an update that locks when trying to read mCurrentFolder. |
| 209 BookmarkNode newFolder = loadBookmarkFolder(currentFolderId); |
| 210 |
| 211 synchronized (mLock) { |
| 212 mCurrentFolder = |
| 213 getFolderId(newFolder) != ChromeBrowserProviderClient.IN
VALID_BOOKMARK_ID |
| 214 ? newFolder : null; |
| 215 } |
| 216 |
| 217 mPreferences.edit() |
| 218 .putLong(STATE_CURRENT_FOLDER, getFolderId(mCurrentFolder)) |
| 219 .apply(); |
| 220 } |
| 221 |
| 222 private BookmarkNode loadBookmarkFolder(long folderId) { |
| 223 if (ThreadUtils.runningOnUiThread()) { |
| 224 Log.e(TAG, "Trying to load bookmark folder from the UI thread.")
; |
| 225 return null; |
| 226 } |
| 227 |
| 228 // If the current folder id doesn't exist (it was deleted) try the c
urrent parent. |
| 229 // If this fails too then fallback to Mobile Bookmarks. |
| 230 if (!ChromeBrowserProviderClient.bookmarkNodeExists(mContext, folder
Id)) { |
| 231 folderId = mCurrentFolder != null ? getFolderId(mCurrentFolder.p
arent()) |
| 232 : ChromeBrowserProviderClient.INVALID_BOOKMARK_ID; |
| 233 if (!ChromeBrowserProviderClient.bookmarkNodeExists(mContext, fo
lderId)) { |
| 234 folderId = ChromeBrowserProviderClient.INVALID_BOOKMARK_ID; |
| 235 } |
| 236 } |
| 237 |
| 238 // Need to verify this always because the package data might be clea
red while the |
| 239 // widget is in the Mobile Bookmarks folder with sync enabled. In th
at case the |
| 240 // hierarchy up folder would still work (we can't update the widget)
but the parent |
| 241 // folders should not be accessible because sync has been reset when
clearing data. |
| 242 if (folderId != ChromeBrowserProviderClient.INVALID_BOOKMARK_ID |
| 243 && !AndroidSyncSettings.isSyncEnabled(mContext) |
| 244 && !ChromeBrowserProviderClient.isBookmarkInMobileBookmarksB
ranch( |
| 245 mContext, folderId)) { |
| 246 folderId = ChromeBrowserProviderClient.INVALID_BOOKMARK_ID; |
| 247 } |
| 248 |
| 249 // Use the Mobile Bookmarks folder by default. |
| 250 if (folderId < 0 || folderId == ChromeBrowserProviderClient.INVALID_
BOOKMARK_ID) { |
| 251 folderId = ChromeBrowserProviderClient.getMobileBookmarksFolderI
d(mContext); |
| 252 if (folderId == ChromeBrowserProviderClient.INVALID_BOOKMARK_ID)
return null; |
| 253 } |
| 254 |
| 255 return ChromeBrowserProviderClient.getBookmarkNode(mContext, folderI
d, |
| 256 ChromeBrowserProviderClient.GET_PARENT |
| 257 | ChromeBrowserProviderClient.GET_CHILDREN |
| 258 | ChromeBrowserProviderClient.GET_FAVICONS |
| 259 | ChromeBrowserProviderClient.GET_THUMBNAILS); |
| 260 } |
| 261 |
| 262 private BookmarkNode getBookmarkForPosition(int position) { |
| 263 if (mCurrentFolder == null) return null; |
| 264 |
| 265 // The position 0 is saved for an entry of the current folder used t
o go up. |
| 266 // This is not the case when the current node has no parent (it's th
e root node). |
| 267 return (mCurrentFolder.parent() == null) |
| 268 ? mCurrentFolder.children().get(position) |
| 269 : (position == 0 |
| 270 ? mCurrentFolder : mCurrentFolder.children().get(pos
ition - 1)); |
| 271 } |
| 272 |
| 273 @Override |
| 274 public void onDataSetChanged() { |
| 275 long token = Binder.clearCallingIdentity(); |
| 276 syncState(); |
| 277 Binder.restoreCallingIdentity(token); |
| 278 } |
| 279 |
| 280 @Override |
| 281 public int getViewTypeCount() { |
| 282 return 2; |
| 283 } |
| 284 |
| 285 @Override |
| 286 public boolean hasStableIds() { |
| 287 return false; |
| 288 } |
| 289 |
| 290 @Override |
| 291 public int getCount() { |
| 292 if (mCurrentFolder == null) return 0; |
| 293 return mCurrentFolder.children().size() + (mCurrentFolder.parent() !
= null ? 1 : 0); |
| 294 } |
| 295 |
| 296 @Override |
| 297 public long getItemId(int position) { |
| 298 return getFolderId(getBookmarkForPosition(position)); |
| 299 } |
| 300 |
| 301 @Override |
| 302 public RemoteViews getLoadingView() { |
| 303 return new RemoteViews(mContext.getPackageName(), |
| 304 R.layout.bookmark_thumbnail_widget_item); |
| 305 } |
| 306 |
| 307 @Override |
| 308 public RemoteViews getViewAt(int position) { |
| 309 if (mCurrentFolder == null) { |
| 310 Log.w(TAG, "No current folder data available."); |
| 311 return null; |
| 312 } |
| 313 |
| 314 BookmarkNode bookmark = getBookmarkForPosition(position); |
| 315 if (bookmark == null) { |
| 316 Log.w(TAG, "Couldn't get bookmark for position " + position); |
| 317 return null; |
| 318 } |
| 319 |
| 320 if (bookmark == mCurrentFolder && bookmark.parent() == null) { |
| 321 Log.w(TAG, "Invalid bookmark data: loop detected."); |
| 322 return null; |
| 323 } |
| 324 |
| 325 String title = bookmark.name(); |
| 326 String url = bookmark.url(); |
| 327 long id = (bookmark == mCurrentFolder) ? bookmark.parent().id() : bo
okmark.id(); |
| 328 |
| 329 // Two layouts are needed because RemoteView does not supporting cha
nging the scale type |
| 330 // of an ImageView: boomarks crop their thumbnails, while folders st
retch their icon. |
| 331 RemoteViews views = !bookmark.isUrl() |
| 332 ? new RemoteViews(mContext.getPackageName(), |
| 333 R.layout.bookmark_thumbnail_widget_item_folder) |
| 334 : new RemoteViews(mContext.getPackageName(), |
| 335 R.layout.bookmark_thumbnail_widget_item); |
| 336 |
| 337 // Set the title of the bookmark. Use the url as a backup. |
| 338 views.setTextViewText(R.id.label, TextUtils.isEmpty(title) ? url : t
itle); |
| 339 |
| 340 if (!bookmark.isUrl()) { |
| 341 int thumbId = (bookmark == mCurrentFolder) |
| 342 ? R.drawable.thumb_bookmark_widget_folder_back_holo |
| 343 : R.drawable.thumb_bookmark_widget_folder_holo; |
| 344 views.setImageViewResource(R.id.thumb, thumbId); |
| 345 views.setImageViewResource(R.id.favicon, |
| 346 R.drawable.ic_bookmark_widget_bookmark_holo_dark); |
| 347 } else { |
| 348 // RemoteViews require a valid bitmap config. |
| 349 Options options = new Options(); |
| 350 options.inPreferredConfig = Config.ARGB_8888; |
| 351 |
| 352 byte[] favicon = bookmark.favicon(); |
| 353 if (favicon != null && favicon.length > 0) { |
| 354 views.setImageViewBitmap(R.id.favicon, |
| 355 BitmapFactory.decodeByteArray(favicon, 0, favicon.le
ngth, options)); |
| 356 } else { |
| 357 views.setImageViewResource(R.id.favicon, |
| 358 org.chromium.chrome.R.drawable.globe_favicon); |
| 359 } |
| 360 |
| 361 byte[] thumbnail = bookmark.thumbnail(); |
| 362 if (thumbnail != null && thumbnail.length > 0) { |
| 363 views.setImageViewBitmap(R.id.thumb, |
| 364 BitmapFactory.decodeByteArray(thumbnail, 0, thumbnai
l.length, options)); |
| 365 } else { |
| 366 views.setImageViewResource(R.id.thumb, R.drawable.browser_th
umbnail); |
| 367 } |
| 368 } |
| 369 |
| 370 Intent fillIn; |
| 371 if (!bookmark.isUrl()) { |
| 372 fillIn = new Intent(getChangeFolderAction(mContext)) |
| 373 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId
) |
| 374 .putExtra(BookmarkColumns._ID, id); |
| 375 } else { |
| 376 fillIn = new Intent(Intent.ACTION_VIEW); |
| 377 if (!TextUtils.isEmpty(url)) { |
| 378 fillIn = fillIn.addCategory(Intent.CATEGORY_BROWSABLE) |
| 379 .setData(Uri.parse(url)); |
| 380 } else { |
| 381 fillIn = fillIn.addCategory(Intent.CATEGORY_LAUNCHER); |
| 382 } |
| 383 } |
| 384 views.setOnClickFillInIntent(R.id.list_item, fillIn); |
| 385 return views; |
| 386 } |
| 387 } |
| 388 } |
OLD | NEW |