/* * 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.app.PendingIntent; import android.app.Service; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.ParcelFileDescriptor; import android.provider.Browser; import android.provider.Browser.BookmarkColumns; import android.service.urlrenderer.UrlRenderer; import android.service.urlrenderer.UrlRendererService; import android.util.Log; import android.view.View; import android.widget.RemoteViews; import com.android.browser.R; import java.io.InputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; public class BookmarkWidgetService extends Service implements UrlRenderer.Callback { private static final String TAG = "BookmarkWidgetService"; /** Force the bookmarks to be re-renderer. */ public static final String UPDATE = "com.android.browser.widget.UPDATE"; /** Change the widget to the next bookmark. */ private static final String NEXT = "com.android.browser.widget.NEXT"; /** Change the widget to the previous bookmark. */ private static final String PREV = "com.android.browser.widget.PREV"; /** Id of the current item displayed in the widget. */ private static final String EXTRA_ID = "com.android.browser.widget.extra.ID"; // XXX: Remove these magic numbers once the dimensions of the widget can be // queried. private static final int WIDTH = 306; private static final int HEIGHT = 386; // Limit the number of connection attempts. private static final int MAX_SERVICE_RETRY_COUNT = 5; // No id specified. private static final int NO_ID = -1; private static final int MSG_UPDATE = 0; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE: if (mRenderer != null) { queryCursorAndRender(); } else { if (++mServiceRetryCount <= MAX_SERVICE_RETRY_COUNT) { // Service is not connected, try again in a second. mHandler.sendEmptyMessageDelayed(MSG_UPDATE, 1000); } } break; default: break; } } }; private final ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { mRenderer = new UrlRenderer(service); } public void onServiceDisconnected(ComponentName className) { mRenderer = null; } }; // Id -> information map storing db ids and their result. private final HashMap mIdsToResults = new HashMap(); // List of ids in order private final ArrayList mIdList = new ArrayList(); // Map of urls to ids for when a url is complete. private final HashMap mUrlsToIds = new HashMap(); // The current id used by the widget during an update. private int mCurrentId = NO_ID; // Class that contacts the service on the phone to render bookmarks. private UrlRenderer mRenderer; // Number of service retries. Stop trying to connect after // MAX_SERVICE_RETRY_COUNT private int mServiceRetryCount; @Override public void onCreate() { bindService(new Intent(UrlRendererService.SERVICE_INTERFACE), mConnection, Context.BIND_AUTO_CREATE); } @Override public void onDestroy() { unbindService(mConnection); } @Override public android.os.IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { final String action = intent.getAction(); if (UPDATE.equals(action)) { mHandler.sendEmptyMessage(MSG_UPDATE); } else if (PREV.equals(action) && mIdList.size() > 1) { int prev = getPreviousId(intent); if (prev == NO_ID) { Log.d(TAG, "Could not determine previous id"); return START_NOT_STICKY; } RenderResult res = mIdsToResults.get(prev); if (res != null) { updateWidget(res); } } else if (NEXT.equals(action) && mIdList.size() > 1) { int next = getNextId(intent); if (next == NO_ID) { Log.d(TAG, "Could not determine next id"); return START_NOT_STICKY; } RenderResult res = mIdsToResults.get(next); if (res != null) { updateWidget(res); } } return START_STICKY; } private int getPreviousId(Intent intent) { int listSize = mIdList.size(); // If the list contains 1 or fewer entries, return NO_ID so that the // widget does not update. if (listSize <= 1) { return NO_ID; } int curr = intent.getIntExtra(EXTRA_ID, NO_ID); if (curr == NO_ID) { return NO_ID; } // Check if the current id is the beginning of the list so we can skip // iterating through. if (mIdList.get(0) == curr) { return mIdList.get(listSize - 1); } // Search for the current id and remember the previous id. int prev = NO_ID; for (int id : mIdList) { if (id == curr) { break; } prev = id; } return prev; } private int getNextId(Intent intent) { int listSize = mIdList.size(); // If the list contains 1 or fewer entries, return NO_ID so that the // widget does not update. if (listSize <= 1) { return NO_ID; } int curr = intent.getIntExtra(EXTRA_ID, NO_ID); if (curr == NO_ID) { return NO_ID; } // Check if the current id is at the end of the list so we can skip // iterating through. if (mIdList.get(listSize - 1) == curr) { return mIdList.get(0); } // Iterate through the ids. i is set to the current index + 1. int i = 1; for (int id : mIdList) { if (id == curr) { break; } i++; } return mIdList.get(i); } private void updateWidget(RenderResult res) { RemoteViews views = new RemoteViews(getPackageName(), R.layout.bookmarkwidget); Intent prev = new Intent(PREV, null, this, BookmarkWidgetService.class); prev.putExtra(EXTRA_ID, res.mId); views.setOnClickPendingIntent(R.id.previous, PendingIntent.getService(this, 0, prev, PendingIntent.FLAG_CANCEL_CURRENT)); Intent next = new Intent(NEXT, null, this, BookmarkWidgetService.class); next.putExtra(EXTRA_ID, res.mId); views.setOnClickPendingIntent(R.id.next, PendingIntent.getService(this, 0, next, PendingIntent.FLAG_CANCEL_CURRENT)); // Set the title of the bookmark. Use the url as a backup. String displayTitle = res.mTitle; if (displayTitle == null) { displayTitle = res.mUrl; } views.setTextViewText(R.id.title, displayTitle); // Set the image or revert to the progress indicator. if (res.mBitmap != null) { views.setImageViewBitmap(R.id.image, res.mBitmap); views.setViewVisibility(R.id.image, View.VISIBLE); views.setViewVisibility(R.id.progress, View.GONE); } else { views.setViewVisibility(R.id.progress, View.VISIBLE); views.setViewVisibility(R.id.image, View.GONE); } // Update the current id. mCurrentId = res.mId; AppWidgetManager.getInstance(this).updateAppWidget( new ComponentName(this, BookmarkWidgetProvider.class), views); } // Default WHERE clause is all bookmarks. private static final String QUERY_WHERE = BookmarkColumns.BOOKMARK + " == 1"; private static final String[] PROJECTION = new String[] { BookmarkColumns._ID, BookmarkColumns.TITLE, BookmarkColumns.URL }; // Class containing the rendering information for a specific bookmark. private static class RenderResult { final int mId; final String mTitle; final String mUrl; Bitmap mBitmap; RenderResult(int id, String title, String url) { mId = id; mTitle = title; mUrl = url; } } private void queryCursorAndRender() { // Clear the ordered list of ids and the map of ids to bitmaps. mIdList.clear(); mIdsToResults.clear(); // Look up all the bookmarks Cursor c = getContentResolver().query(Browser.BOOKMARKS_URI, PROJECTION, QUERY_WHERE, null, null); if (c != null) { if (c.moveToFirst()) { ArrayList urls = new ArrayList(c.getCount()); boolean sawCurrentId = false; do { int id = c.getInt(0); String title = c.getString(1); String url = c.getString(2); // Linear list of ids to obtain the previous and next. mIdList.add(id); // Map the url to its db id for lookup when complete. mUrlsToIds.put(url, id); // Is this the current id? if (mCurrentId == id) { sawCurrentId = true; } // Store the current information to at least display the // title. RenderResult res = new RenderResult(id, title, url); mIdsToResults.put(id, res); // Add the url to our list to render. urls.add(url); } while (c.moveToNext()); // Request a rendering of the urls. XXX: Hard-coded dimensions // until the view's orientation and size can be determined. Or // in the future the image will be a picture that can be // scaled/zoomed arbitrarily. mRenderer.render(urls, WIDTH, HEIGHT, this); // Set the current id to the very first id if we did not see // the current id in the list (the bookmark could have been // deleted or this is the first update). if (!sawCurrentId) { mCurrentId = mIdList.get(0); } } c.close(); } } // UrlRenderer.Callback implementation public void complete(String url, ParcelFileDescriptor result) { int id = mUrlsToIds.get(url); if (id == NO_ID) { Log.d(TAG, "No matching id found during completion of " + url); return; } RenderResult res = mIdsToResults.get(id); if (res == null) { Log.d(TAG, "No result found during completion of " + url); return; } // Set the result. if (result != null) { InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(result); Bitmap orig = BitmapFactory.decodeStream(input, null, null); // XXX: Hard-coded scaled bitmap until I can query the image // dimensions. res.mBitmap = Bitmap.createScaledBitmap(orig, WIDTH, HEIGHT, true); try { input.close(); } catch (IOException e) { // oh well... } } // If we are currently looking at the bookmark that just finished, // update the widget. if (mCurrentId == id) { updateWidget(res); } } }