Index: chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkswidget/BookmarkThumbnailWidgetService.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkswidget/BookmarkThumbnailWidgetService.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkswidget/BookmarkThumbnailWidgetService.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..13c2ce8480863f6ad85612e5f173b3677636adee |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkswidget/BookmarkThumbnailWidgetService.java |
@@ -0,0 +1,388 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.bookmarkswidget; |
+ |
+import android.appwidget.AppWidgetManager; |
+import android.content.Context; |
+import android.content.Intent; |
+import android.content.SharedPreferences; |
+import android.graphics.Bitmap.Config; |
+import android.graphics.BitmapFactory; |
+import android.graphics.BitmapFactory.Options; |
+import android.net.Uri; |
+import android.os.AsyncTask; |
+import android.os.Binder; |
+import android.provider.Browser.BookmarkColumns; |
+import android.text.TextUtils; |
+import android.util.Log; |
+import android.widget.RemoteViews; |
+import android.widget.RemoteViewsService; |
+ |
+import com.google.android.apps.chrome.R; |
+import com.google.android.apps.chrome.appwidget.bookmarks.BookmarkThumbnailWidgetProvider; |
+ |
+import org.chromium.base.ThreadUtils; |
+import org.chromium.base.annotations.SuppressFBWarnings; |
+import org.chromium.base.library_loader.ProcessInitException; |
+import org.chromium.chrome.browser.ChromeBrowserProvider.BookmarkNode; |
+import org.chromium.chrome.browser.ChromeBrowserProviderClient; |
+import org.chromium.chrome.browser.ChromiumApplication; |
+import org.chromium.chrome.browser.util.IntentUtils; |
+import org.chromium.sync.AndroidSyncSettings; |
+ |
+/** |
+ * Service to support bookmarks on the Android home screen |
+ */ |
+public class BookmarkThumbnailWidgetService extends RemoteViewsService { |
+ |
+ static final String TAG = "BookmarkThumbnailWidgetService"; |
+ static final String ACTION_CHANGE_FOLDER_SUFFIX = ".CHANGE_FOLDER"; |
+ static final String STATE_CURRENT_FOLDER = "current_folder"; |
+ |
+ @Override |
+ public RemoteViewsFactory onGetViewFactory(Intent intent) { |
+ int widgetId = IntentUtils.safeGetIntExtra(intent, AppWidgetManager.EXTRA_APPWIDGET_ID, -1); |
+ if (widgetId < 0) { |
+ Log.w(TAG, "Missing EXTRA_APPWIDGET_ID!"); |
+ return null; |
+ } |
+ return new BookmarkFactory(this, widgetId); |
+ } |
+ |
+ static String getChangeFolderAction(Context context) { |
+ return context.getPackageName() + ACTION_CHANGE_FOLDER_SUFFIX; |
+ } |
+ |
+ private static SharedPreferences getWidgetState(Context context, int widgetId) { |
+ return context.getSharedPreferences( |
+ String.format("widgetState-%d", widgetId), |
+ Context.MODE_PRIVATE); |
+ } |
+ |
+ static void deleteWidgetState(Context context, int widgetId) { |
+ // Android Browser's widget used private API methods to access the shared prefs |
+ // files and deleted them. This is the best we can do with the public API. |
+ SharedPreferences preferences = getWidgetState(context, widgetId); |
+ if (preferences != null) preferences.edit().clear().commit(); |
+ } |
+ |
+ static void changeFolder(Context context, Intent intent) { |
+ int widgetId = IntentUtils.safeGetIntExtra(intent, AppWidgetManager.EXTRA_APPWIDGET_ID, -1); |
+ long folderId = IntentUtils.safeGetLongExtra(intent, BookmarkColumns._ID, |
+ ChromeBrowserProviderClient.INVALID_BOOKMARK_ID); |
+ if (widgetId >= 0 && folderId >= 0) { |
+ SharedPreferences prefs = getWidgetState(context, widgetId); |
+ prefs.edit().putLong(STATE_CURRENT_FOLDER, folderId).commit(); |
+ AppWidgetManager.getInstance(context) |
+ .notifyAppWidgetViewDataChanged(widgetId, R.id.bookmarks_list); |
+ } |
+ } |
+ |
+ static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory, |
+ BookmarkWidgetUpdateListener.UpdateListener { |
+ |
+ private final ChromiumApplication mContext; |
+ private final int mWidgetId; |
+ private final SharedPreferences mPreferences; |
+ private BookmarkWidgetUpdateListener mUpdateListener; |
+ private BookmarkNode mCurrentFolder; |
+ private final Object mLock = new Object(); |
+ |
+ public BookmarkFactory(Context context, int widgetId) { |
+ mContext = (ChromiumApplication) context.getApplicationContext(); |
+ mWidgetId = widgetId; |
+ mPreferences = getWidgetState(mContext, mWidgetId); |
+ } |
+ |
+ private static long getFolderId(BookmarkNode folder) { |
+ return folder != null ? folder.id() : ChromeBrowserProviderClient.INVALID_BOOKMARK_ID; |
+ } |
+ |
+ @SuppressFBWarnings("DM_EXIT") |
+ @Override |
+ public void onCreate() { |
+ // Required to be applied here redundantly to prevent crashes in the cases where the |
+ // package data is deleted or the Chrome application forced to stop. |
+ try { |
+ mContext.startBrowserProcessesAndLoadLibrariesSync(mContext, true); |
+ } catch (ProcessInitException e) { |
+ Log.e(TAG, "Failed to start browser process.", e); |
+ // Since the library failed to initialize nothing in the application |
+ // can work, so kill the whole application not just the activity |
+ System.exit(-1); |
+ } |
+ mUpdateListener = new BookmarkWidgetUpdateListener(mContext, this); |
+ } |
+ |
+ @Override |
+ public void onDestroy() { |
+ if (mUpdateListener != null) mUpdateListener.destroy(); |
+ deleteWidgetState(mContext, mWidgetId); |
+ } |
+ |
+ @Override |
+ public void onBookmarkModelUpdated() { |
+ refreshWidget(); |
+ } |
+ |
+ @Override |
+ public void onSyncEnabledStatusUpdated(boolean enabled) { |
+ synchronized (mLock) { |
+ // Need to operate in a separate thread as it involves queries to our provider. |
+ new SyncEnabledStatusUpdatedTask(enabled, getFolderId(mCurrentFolder)).execute(); |
+ } |
+ } |
+ |
+ @Override |
+ public void onThumbnailUpdated(String url) { |
+ synchronized (mLock) { |
+ if (mCurrentFolder == null) return; |
+ |
+ for (BookmarkNode child : mCurrentFolder.children()) { |
+ if (child.isUrl() && url.equals(child.url())) { |
+ refreshWidget(); |
+ break; |
+ } |
+ } |
+ } |
+ } |
+ |
+ void refreshWidget() { |
+ mContext.sendBroadcast(new Intent( |
+ BookmarkThumbnailWidgetProviderBase.getBookmarkAppWidgetUpdateAction(mContext), |
+ null, mContext, BookmarkThumbnailWidgetProvider.class) |
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId)); |
+ } |
+ |
+ void requestFolderChange(long folderId) { |
+ mContext.sendBroadcast(new Intent(getChangeFolderAction(mContext)) |
+ .setClass(mContext, BookmarkWidgetProxy.class) |
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) |
+ .putExtra(BookmarkColumns._ID, folderId)); |
+ } |
+ |
+ // Performs the required checks to trigger an update of the widget after changing the sync |
+ // enable settings. The required provider methods cannot be accessed in the UI thread. |
+ private class SyncEnabledStatusUpdatedTask extends AsyncTask<Void, Void, Void> { |
+ private final boolean mEnabled; |
+ private final long mCurrentFolderId; |
+ |
+ public SyncEnabledStatusUpdatedTask(boolean enabled, long currentFolderId) { |
+ mEnabled = enabled; |
+ mCurrentFolderId = currentFolderId; |
+ } |
+ |
+ @Override |
+ protected Void doInBackground(Void... params) { |
+ // If we're in the Mobile Bookmarks folder the icon to go up the hierarchy |
+ // will either appear or disappear. Need to refresh. |
+ long mobileBookmarksFolderId = |
+ ChromeBrowserProviderClient.getMobileBookmarksFolderId(mContext); |
+ if (mCurrentFolderId == mobileBookmarksFolderId) { |
+ refreshWidget(); |
+ return null; |
+ } |
+ |
+ // If disabling sync, we need to move to the Mobile Bookmarks folder if we're |
+ // not inside that branch of the bookmark hierarchy (will become not accessible). |
+ if (!mEnabled && !ChromeBrowserProviderClient.isBookmarkInMobileBookmarksBranch( |
+ mContext, mCurrentFolderId)) { |
+ requestFolderChange(mobileBookmarksFolderId); |
+ } |
+ |
+ return null; |
+ } |
+ } |
+ |
+ // ---------------------------------------------------------------- // |
+ // ------- Methods below this line run in different thread -------- // |
+ // ---------------------------------------------------------------- // |
+ |
+ private void syncState() { |
+ long currentFolderId = mPreferences.getLong(STATE_CURRENT_FOLDER, |
+ ChromeBrowserProviderClient.INVALID_BOOKMARK_ID); |
+ |
+ // Keep outside the synchronized block to avoid deadlocks in case loading the folder |
+ // triggers an update that locks when trying to read mCurrentFolder. |
+ BookmarkNode newFolder = loadBookmarkFolder(currentFolderId); |
+ |
+ synchronized (mLock) { |
+ mCurrentFolder = |
+ getFolderId(newFolder) != ChromeBrowserProviderClient.INVALID_BOOKMARK_ID |
+ ? newFolder : null; |
+ } |
+ |
+ mPreferences.edit() |
+ .putLong(STATE_CURRENT_FOLDER, getFolderId(mCurrentFolder)) |
+ .apply(); |
+ } |
+ |
+ private BookmarkNode loadBookmarkFolder(long folderId) { |
+ if (ThreadUtils.runningOnUiThread()) { |
+ Log.e(TAG, "Trying to load bookmark folder from the UI thread."); |
+ return null; |
+ } |
+ |
+ // If the current folder id doesn't exist (it was deleted) try the current parent. |
+ // If this fails too then fallback to Mobile Bookmarks. |
+ if (!ChromeBrowserProviderClient.bookmarkNodeExists(mContext, folderId)) { |
+ folderId = mCurrentFolder != null ? getFolderId(mCurrentFolder.parent()) |
+ : ChromeBrowserProviderClient.INVALID_BOOKMARK_ID; |
+ if (!ChromeBrowserProviderClient.bookmarkNodeExists(mContext, folderId)) { |
+ folderId = ChromeBrowserProviderClient.INVALID_BOOKMARK_ID; |
+ } |
+ } |
+ |
+ // Need to verify this always because the package data might be cleared while the |
+ // widget is in the Mobile Bookmarks folder with sync enabled. In that case the |
+ // hierarchy up folder would still work (we can't update the widget) but the parent |
+ // folders should not be accessible because sync has been reset when clearing data. |
+ if (folderId != ChromeBrowserProviderClient.INVALID_BOOKMARK_ID |
+ && !AndroidSyncSettings.isSyncEnabled(mContext) |
+ && !ChromeBrowserProviderClient.isBookmarkInMobileBookmarksBranch( |
+ mContext, folderId)) { |
+ folderId = ChromeBrowserProviderClient.INVALID_BOOKMARK_ID; |
+ } |
+ |
+ // Use the Mobile Bookmarks folder by default. |
+ if (folderId < 0 || folderId == ChromeBrowserProviderClient.INVALID_BOOKMARK_ID) { |
+ folderId = ChromeBrowserProviderClient.getMobileBookmarksFolderId(mContext); |
+ if (folderId == ChromeBrowserProviderClient.INVALID_BOOKMARK_ID) return null; |
+ } |
+ |
+ return ChromeBrowserProviderClient.getBookmarkNode(mContext, folderId, |
+ ChromeBrowserProviderClient.GET_PARENT |
+ | ChromeBrowserProviderClient.GET_CHILDREN |
+ | ChromeBrowserProviderClient.GET_FAVICONS |
+ | ChromeBrowserProviderClient.GET_THUMBNAILS); |
+ } |
+ |
+ private BookmarkNode getBookmarkForPosition(int position) { |
+ if (mCurrentFolder == null) return null; |
+ |
+ // The position 0 is saved for an entry of the current folder used to go up. |
+ // This is not the case when the current node has no parent (it's the root node). |
+ return (mCurrentFolder.parent() == null) |
+ ? mCurrentFolder.children().get(position) |
+ : (position == 0 |
+ ? mCurrentFolder : mCurrentFolder.children().get(position - 1)); |
+ } |
+ |
+ @Override |
+ public void onDataSetChanged() { |
+ long token = Binder.clearCallingIdentity(); |
+ syncState(); |
+ Binder.restoreCallingIdentity(token); |
+ } |
+ |
+ @Override |
+ public int getViewTypeCount() { |
+ return 2; |
+ } |
+ |
+ @Override |
+ public boolean hasStableIds() { |
+ return false; |
+ } |
+ |
+ @Override |
+ public int getCount() { |
+ if (mCurrentFolder == null) return 0; |
+ return mCurrentFolder.children().size() + (mCurrentFolder.parent() != null ? 1 : 0); |
+ } |
+ |
+ @Override |
+ public long getItemId(int position) { |
+ return getFolderId(getBookmarkForPosition(position)); |
+ } |
+ |
+ @Override |
+ public RemoteViews getLoadingView() { |
+ return new RemoteViews(mContext.getPackageName(), |
+ R.layout.bookmark_thumbnail_widget_item); |
+ } |
+ |
+ @Override |
+ public RemoteViews getViewAt(int position) { |
+ if (mCurrentFolder == null) { |
+ Log.w(TAG, "No current folder data available."); |
+ return null; |
+ } |
+ |
+ BookmarkNode bookmark = getBookmarkForPosition(position); |
+ if (bookmark == null) { |
+ Log.w(TAG, "Couldn't get bookmark for position " + position); |
+ return null; |
+ } |
+ |
+ if (bookmark == mCurrentFolder && bookmark.parent() == null) { |
+ Log.w(TAG, "Invalid bookmark data: loop detected."); |
+ return null; |
+ } |
+ |
+ String title = bookmark.name(); |
+ String url = bookmark.url(); |
+ long id = (bookmark == mCurrentFolder) ? bookmark.parent().id() : bookmark.id(); |
+ |
+ // Two layouts are needed because RemoteView does not supporting changing the scale type |
+ // of an ImageView: boomarks crop their thumbnails, while folders stretch their icon. |
+ RemoteViews views = !bookmark.isUrl() |
+ ? new RemoteViews(mContext.getPackageName(), |
+ R.layout.bookmark_thumbnail_widget_item_folder) |
+ : new RemoteViews(mContext.getPackageName(), |
+ R.layout.bookmark_thumbnail_widget_item); |
+ |
+ // Set the title of the bookmark. Use the url as a backup. |
+ views.setTextViewText(R.id.label, TextUtils.isEmpty(title) ? url : title); |
+ |
+ if (!bookmark.isUrl()) { |
+ int thumbId = (bookmark == mCurrentFolder) |
+ ? R.drawable.thumb_bookmark_widget_folder_back_holo |
+ : R.drawable.thumb_bookmark_widget_folder_holo; |
+ views.setImageViewResource(R.id.thumb, thumbId); |
+ views.setImageViewResource(R.id.favicon, |
+ R.drawable.ic_bookmark_widget_bookmark_holo_dark); |
+ } else { |
+ // RemoteViews require a valid bitmap config. |
+ Options options = new Options(); |
+ options.inPreferredConfig = Config.ARGB_8888; |
+ |
+ byte[] favicon = bookmark.favicon(); |
+ if (favicon != null && favicon.length > 0) { |
+ views.setImageViewBitmap(R.id.favicon, |
+ BitmapFactory.decodeByteArray(favicon, 0, favicon.length, options)); |
+ } else { |
+ views.setImageViewResource(R.id.favicon, |
+ org.chromium.chrome.R.drawable.globe_favicon); |
+ } |
+ |
+ byte[] thumbnail = bookmark.thumbnail(); |
+ if (thumbnail != null && thumbnail.length > 0) { |
+ views.setImageViewBitmap(R.id.thumb, |
+ BitmapFactory.decodeByteArray(thumbnail, 0, thumbnail.length, options)); |
+ } else { |
+ views.setImageViewResource(R.id.thumb, R.drawable.browser_thumbnail); |
+ } |
+ } |
+ |
+ Intent fillIn; |
+ if (!bookmark.isUrl()) { |
+ fillIn = new Intent(getChangeFolderAction(mContext)) |
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) |
+ .putExtra(BookmarkColumns._ID, id); |
+ } else { |
+ fillIn = new Intent(Intent.ACTION_VIEW); |
+ if (!TextUtils.isEmpty(url)) { |
+ fillIn = fillIn.addCategory(Intent.CATEGORY_BROWSABLE) |
+ .setData(Uri.parse(url)); |
+ } else { |
+ fillIn = fillIn.addCategory(Intent.CATEGORY_LAUNCHER); |
+ } |
+ } |
+ views.setOnClickFillInIntent(R.id.list_item, fillIn); |
+ return views; |
+ } |
+ } |
+} |