/*
* 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 });
}
}
}
}