/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.browser.widget; import android.appwidget.AppWidgetManager; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.database.MergeCursor; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.net.Uri; import android.os.Binder; import android.provider.BrowserContract; import android.provider.BrowserContract.Bookmarks; import android.text.TextUtils; import android.util.Log; import android.widget.RemoteViews; import android.widget.RemoteViewsService; import com.android.browser.BrowserActivity; import com.android.browser.R; import com.android.browser.provider.BrowserProvider2; import java.io.File; import java.io.FilenameFilter; import java.util.HashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; public class BookmarkThumbnailWidgetService extends RemoteViewsService { static final String TAG = "BookmarkThumbnailWidgetService"; static final String ACTION_CHANGE_FOLDER = "com.android.browser.widget.CHANGE_FOLDER"; static final String STATE_CURRENT_FOLDER = "current_folder"; static final String STATE_ROOT_FOLDER = "root_folder"; private static final String[] PROJECTION = new String[] { BrowserContract.Bookmarks._ID, BrowserContract.Bookmarks.TITLE, BrowserContract.Bookmarks.URL, BrowserContract.Bookmarks.FAVICON, BrowserContract.Bookmarks.IS_FOLDER, BrowserContract.Bookmarks.POSITION, /* needed for order by */ BrowserContract.Bookmarks.THUMBNAIL, BrowserContract.Bookmarks.PARENT}; private static final int BOOKMARK_INDEX_ID = 0; private static final int BOOKMARK_INDEX_TITLE = 1; private static final int BOOKMARK_INDEX_URL = 2; private static final int BOOKMARK_INDEX_FAVICON = 3; private static final int BOOKMARK_INDEX_IS_FOLDER = 4; private static final int BOOKMARK_INDEX_THUMBNAIL = 6; private static final int BOOKMARK_INDEX_PARENT_ID = 7; @Override public RemoteViewsFactory onGetViewFactory(Intent intent) { int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); if (widgetId < 0) { Log.w(TAG, "Missing EXTRA_APPWIDGET_ID!"); return null; } return new BookmarkFactory(getApplicationContext(), widgetId); } 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) { File file = context.getSharedPrefsFile( String.format("widgetState-%d", widgetId)); if (file.exists()) { if (!file.delete()) { file.deleteOnExit(); } } } static void changeFolder(Context context, Intent intent) { int wid = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); long fid = intent.getLongExtra(Bookmarks._ID, -1); if (wid >= 0 && fid >= 0) { SharedPreferences prefs = getWidgetState(context, wid); prefs.edit().putLong(STATE_CURRENT_FOLDER, fid).commit(); AppWidgetManager.getInstance(context) .notifyAppWidgetViewDataChanged(wid, R.id.bookmarks_list); } } static void setupWidgetState(Context context, int widgetId, long rootFolder) { SharedPreferences pref = getWidgetState(context, widgetId); pref.edit() .putLong(STATE_CURRENT_FOLDER, rootFolder) .putLong(STATE_ROOT_FOLDER, rootFolder) .apply(); } /** * Checks for any state files that may have not received onDeleted */ static void removeOrphanedStates(Context context, int[] widgetIds) { File prefsDirectory = context.getSharedPrefsFile("null").getParentFile(); File[] widgetStates = prefsDirectory.listFiles(new StateFilter(widgetIds)); if (widgetStates != null) { for (File f : widgetStates) { Log.w(TAG, "Found orphaned state: " + f.getName()); if (!f.delete()) { f.deleteOnExit(); } } } } static class StateFilter implements FilenameFilter { static final Pattern sStatePattern = Pattern.compile("widgetState-(\\d+)\\.xml"); HashSet<Integer> mWidgetIds; StateFilter(int[] ids) { mWidgetIds = new HashSet<Integer>(); for (int id : ids) { mWidgetIds.add(id); } } @Override public boolean accept(File dir, String filename) { Matcher m = sStatePattern.matcher(filename); if (m.matches()) { int id = Integer.parseInt(m.group(1)); if (!mWidgetIds.contains(id)) { return true; } } return false; } } static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory { private Cursor mBookmarks; private Context mContext; private int mWidgetId; private long mCurrentFolder = -1; private long mRootFolder = -1; private SharedPreferences mPreferences = null; public BookmarkFactory(Context context, int widgetId) { mContext = context.getApplicationContext(); mWidgetId = widgetId; } void syncState() { if (mPreferences == null) { mPreferences = getWidgetState(mContext, mWidgetId); } long currentFolder = mPreferences.getLong(STATE_CURRENT_FOLDER, -1); mRootFolder = mPreferences.getLong(STATE_ROOT_FOLDER, -1); if (currentFolder != mCurrentFolder) { resetBookmarks(); mCurrentFolder = currentFolder; } } void saveState() { if (mPreferences == null) { mPreferences = getWidgetState(mContext, mWidgetId); } mPreferences.edit() .putLong(STATE_CURRENT_FOLDER, mCurrentFolder) .putLong(STATE_ROOT_FOLDER, mRootFolder) .commit(); } @Override public int getCount() { if (mBookmarks == null) return 0; return mBookmarks.getCount(); } @Override public long getItemId(int position) { return position; } @Override public RemoteViews getLoadingView() { return new RemoteViews( mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item); } @Override public RemoteViews getViewAt(int position) { if (!mBookmarks.moveToPosition(position)) { return null; } long id = mBookmarks.getLong(BOOKMARK_INDEX_ID); String title = mBookmarks.getString(BOOKMARK_INDEX_TITLE); String url = mBookmarks.getString(BOOKMARK_INDEX_URL); boolean isFolder = mBookmarks.getInt(BOOKMARK_INDEX_IS_FOLDER) != 0; RemoteViews views; // Two layouts are needed because of b/5387153 if (isFolder) { views = new RemoteViews(mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item_folder); } else { views = new RemoteViews(mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item); } // Set the title of the bookmark. Use the url as a backup. String displayTitle = title; if (TextUtils.isEmpty(displayTitle)) { // The browser always requires a title for bookmarks, but jic... displayTitle = url; } views.setTextViewText(R.id.label, displayTitle); if (isFolder) { if (id == mCurrentFolder) { id = mBookmarks.getLong(BOOKMARK_INDEX_PARENT_ID); views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_back_holo); } else { views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_holo); } views.setImageViewResource(R.id.favicon, R.drawable.ic_bookmark_widget_bookmark_holo_dark); views.setDrawableParameters(R.id.thumb, true, 0, -1, null, -1); } else { // RemoteViews require a valid bitmap config Options options = new Options(); options.inPreferredConfig = Config.ARGB_8888; Bitmap thumbnail = null, favicon = null; byte[] blob = mBookmarks.getBlob(BOOKMARK_INDEX_THUMBNAIL); views.setDrawableParameters(R.id.thumb, true, 255, -1, null, -1); if (blob != null && blob.length > 0) { thumbnail = BitmapFactory.decodeByteArray( blob, 0, blob.length, options); views.setImageViewBitmap(R.id.thumb, thumbnail); } else { views.setImageViewResource(R.id.thumb, R.drawable.browser_thumbnail); } blob = mBookmarks.getBlob(BOOKMARK_INDEX_FAVICON); if (blob != null && blob.length > 0) { favicon = BitmapFactory.decodeByteArray( blob, 0, blob.length, options); views.setImageViewBitmap(R.id.favicon, favicon); } else { views.setImageViewResource(R.id.favicon, R.drawable.app_web_browser_sm); } } Intent fillin; if (isFolder) { fillin = new Intent(ACTION_CHANGE_FOLDER) .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) .putExtra(Bookmarks._ID, id); } else { if (!TextUtils.isEmpty(url)) { fillin = new Intent(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) .setData(Uri.parse(url)); } else { fillin = new Intent(BrowserActivity.ACTION_SHOW_BROWSER); } } views.setOnClickFillInIntent(R.id.list_item, fillin); return views; } @Override public int getViewTypeCount() { return 2; } @Override public boolean hasStableIds() { return false; } @Override public void onCreate() { } @Override public void onDestroy() { if (mBookmarks != null) { mBookmarks.close(); mBookmarks = null; } deleteWidgetState(mContext, mWidgetId); } @Override public void onDataSetChanged() { long token = Binder.clearCallingIdentity(); syncState(); if (mRootFolder < 0 || mCurrentFolder < 0) { // This shouldn't happen, but JIC default to the local account mRootFolder = BrowserProvider2.FIXED_ID_ROOT; mCurrentFolder = mRootFolder; saveState(); } loadBookmarks(); Binder.restoreCallingIdentity(token); } private void resetBookmarks() { if (mBookmarks != null) { mBookmarks.close(); mBookmarks = null; } } void loadBookmarks() { resetBookmarks(); Uri uri = ContentUris.withAppendedId( BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER, mCurrentFolder); mBookmarks = mContext.getContentResolver().query(uri, PROJECTION, null, null, null); if (mCurrentFolder != mRootFolder) { uri = ContentUris.withAppendedId( BrowserContract.Bookmarks.CONTENT_URI, mCurrentFolder); Cursor c = mContext.getContentResolver().query(uri, PROJECTION, null, null, null); mBookmarks = new MergeCursor(new Cursor[] { c, mBookmarks }); } } } }