Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.browser.widget;
     18 
     19 import android.appwidget.AppWidgetManager;
     20 import android.content.ContentUris;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.SharedPreferences;
     24 import android.database.Cursor;
     25 import android.database.MergeCursor;
     26 import android.graphics.Bitmap;
     27 import android.graphics.Bitmap.Config;
     28 import android.graphics.BitmapFactory;
     29 import android.graphics.BitmapFactory.Options;
     30 import android.net.Uri;
     31 import android.os.Binder;
     32 import android.provider.BrowserContract;
     33 import android.provider.BrowserContract.Bookmarks;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 import android.widget.RemoteViews;
     37 import android.widget.RemoteViewsService;
     38 
     39 import com.android.browser.BrowserActivity;
     40 import com.android.browser.R;
     41 import com.android.browser.provider.BrowserProvider2;
     42 
     43 import java.io.File;
     44 import java.io.FilenameFilter;
     45 import java.util.HashSet;
     46 import java.util.regex.Matcher;
     47 import java.util.regex.Pattern;
     48 
     49 public class BookmarkThumbnailWidgetService extends RemoteViewsService {
     50 
     51     static final String TAG = "BookmarkThumbnailWidgetService";
     52     static final String ACTION_CHANGE_FOLDER
     53             = "com.android.browser.widget.CHANGE_FOLDER";
     54 
     55     static final String STATE_CURRENT_FOLDER = "current_folder";
     56     static final String STATE_ROOT_FOLDER = "root_folder";
     57 
     58     private static final String[] PROJECTION = new String[] {
     59             BrowserContract.Bookmarks._ID,
     60             BrowserContract.Bookmarks.TITLE,
     61             BrowserContract.Bookmarks.URL,
     62             BrowserContract.Bookmarks.FAVICON,
     63             BrowserContract.Bookmarks.IS_FOLDER,
     64             BrowserContract.Bookmarks.POSITION, /* needed for order by */
     65             BrowserContract.Bookmarks.THUMBNAIL,
     66             BrowserContract.Bookmarks.PARENT};
     67     private static final int BOOKMARK_INDEX_ID = 0;
     68     private static final int BOOKMARK_INDEX_TITLE = 1;
     69     private static final int BOOKMARK_INDEX_URL = 2;
     70     private static final int BOOKMARK_INDEX_FAVICON = 3;
     71     private static final int BOOKMARK_INDEX_IS_FOLDER = 4;
     72     private static final int BOOKMARK_INDEX_THUMBNAIL = 6;
     73     private static final int BOOKMARK_INDEX_PARENT_ID = 7;
     74 
     75     @Override
     76     public RemoteViewsFactory onGetViewFactory(Intent intent) {
     77         int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
     78         if (widgetId < 0) {
     79             Log.w(TAG, "Missing EXTRA_APPWIDGET_ID!");
     80             return null;
     81         }
     82         return new BookmarkFactory(getApplicationContext(), widgetId);
     83     }
     84 
     85     static SharedPreferences getWidgetState(Context context, int widgetId) {
     86         return context.getSharedPreferences(
     87                 String.format("widgetState-%d", widgetId),
     88                 Context.MODE_PRIVATE);
     89     }
     90 
     91     static void deleteWidgetState(Context context, int widgetId) {
     92         File file = context.getSharedPrefsFile(
     93                 String.format("widgetState-%d", widgetId));
     94         if (file.exists()) {
     95             if (!file.delete()) {
     96                 file.deleteOnExit();
     97             }
     98         }
     99     }
    100 
    101     static void changeFolder(Context context, Intent intent) {
    102         int wid = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
    103         long fid = intent.getLongExtra(Bookmarks._ID, -1);
    104         if (wid >= 0 && fid >= 0) {
    105             SharedPreferences prefs = getWidgetState(context, wid);
    106             prefs.edit().putLong(STATE_CURRENT_FOLDER, fid).commit();
    107             AppWidgetManager.getInstance(context)
    108                     .notifyAppWidgetViewDataChanged(wid, R.id.bookmarks_list);
    109         }
    110     }
    111 
    112     static void setupWidgetState(Context context, int widgetId, long rootFolder) {
    113         SharedPreferences pref = getWidgetState(context, widgetId);
    114         pref.edit()
    115             .putLong(STATE_CURRENT_FOLDER, rootFolder)
    116             .putLong(STATE_ROOT_FOLDER, rootFolder)
    117             .apply();
    118     }
    119 
    120     /**
    121      *  Checks for any state files that may have not received onDeleted
    122      */
    123     static void removeOrphanedStates(Context context, int[] widgetIds) {
    124         File prefsDirectory = context.getSharedPrefsFile("null").getParentFile();
    125         File[] widgetStates = prefsDirectory.listFiles(new StateFilter(widgetIds));
    126         if (widgetStates != null) {
    127             for (File f : widgetStates) {
    128                 Log.w(TAG, "Found orphaned state: " + f.getName());
    129                 if (!f.delete()) {
    130                     f.deleteOnExit();
    131                 }
    132             }
    133         }
    134     }
    135 
    136     static class StateFilter implements FilenameFilter {
    137 
    138         static final Pattern sStatePattern = Pattern.compile("widgetState-(\\d+)\\.xml");
    139         HashSet<Integer> mWidgetIds;
    140 
    141         StateFilter(int[] ids) {
    142             mWidgetIds = new HashSet<Integer>();
    143             for (int id : ids) {
    144                 mWidgetIds.add(id);
    145             }
    146         }
    147 
    148         @Override
    149         public boolean accept(File dir, String filename) {
    150             Matcher m = sStatePattern.matcher(filename);
    151             if (m.matches()) {
    152                 int id = Integer.parseInt(m.group(1));
    153                 if (!mWidgetIds.contains(id)) {
    154                     return true;
    155                 }
    156             }
    157             return false;
    158         }
    159 
    160     }
    161 
    162     static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory {
    163         private Cursor mBookmarks;
    164         private Context mContext;
    165         private int mWidgetId;
    166         private long mCurrentFolder = -1;
    167         private long mRootFolder = -1;
    168         private SharedPreferences mPreferences = null;
    169 
    170         public BookmarkFactory(Context context, int widgetId) {
    171             mContext = context.getApplicationContext();
    172             mWidgetId = widgetId;
    173         }
    174 
    175         void syncState() {
    176             if (mPreferences == null) {
    177                 mPreferences = getWidgetState(mContext, mWidgetId);
    178             }
    179             long currentFolder = mPreferences.getLong(STATE_CURRENT_FOLDER, -1);
    180             mRootFolder = mPreferences.getLong(STATE_ROOT_FOLDER, -1);
    181             if (currentFolder != mCurrentFolder) {
    182                 resetBookmarks();
    183                 mCurrentFolder = currentFolder;
    184             }
    185         }
    186 
    187         void saveState() {
    188             if (mPreferences == null) {
    189                 mPreferences = getWidgetState(mContext, mWidgetId);
    190             }
    191             mPreferences.edit()
    192                 .putLong(STATE_CURRENT_FOLDER, mCurrentFolder)
    193                 .putLong(STATE_ROOT_FOLDER, mRootFolder)
    194                 .commit();
    195         }
    196 
    197         @Override
    198         public int getCount() {
    199             if (mBookmarks == null)
    200                 return 0;
    201             return mBookmarks.getCount();
    202         }
    203 
    204         @Override
    205         public long getItemId(int position) {
    206             return position;
    207         }
    208 
    209         @Override
    210         public RemoteViews getLoadingView() {
    211             return new RemoteViews(
    212                     mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item);
    213         }
    214 
    215         @Override
    216         public RemoteViews getViewAt(int position) {
    217             if (!mBookmarks.moveToPosition(position)) {
    218                 return null;
    219             }
    220 
    221             long id = mBookmarks.getLong(BOOKMARK_INDEX_ID);
    222             String title = mBookmarks.getString(BOOKMARK_INDEX_TITLE);
    223             String url = mBookmarks.getString(BOOKMARK_INDEX_URL);
    224             boolean isFolder = mBookmarks.getInt(BOOKMARK_INDEX_IS_FOLDER) != 0;
    225 
    226             RemoteViews views;
    227             // Two layouts are needed because of b/5387153
    228             if (isFolder) {
    229                 views = new RemoteViews(mContext.getPackageName(),
    230                         R.layout.bookmarkthumbnailwidget_item_folder);
    231             } else {
    232                 views = new RemoteViews(mContext.getPackageName(),
    233                         R.layout.bookmarkthumbnailwidget_item);
    234             }
    235             // Set the title of the bookmark. Use the url as a backup.
    236             String displayTitle = title;
    237             if (TextUtils.isEmpty(displayTitle)) {
    238                 // The browser always requires a title for bookmarks, but jic...
    239                 displayTitle = url;
    240             }
    241             views.setTextViewText(R.id.label, displayTitle);
    242             if (isFolder) {
    243                 if (id == mCurrentFolder) {
    244                     id = mBookmarks.getLong(BOOKMARK_INDEX_PARENT_ID);
    245                     views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_back_holo);
    246                 } else {
    247                     views.setImageViewResource(R.id.thumb, R.drawable.thumb_bookmark_widget_folder_holo);
    248                 }
    249                 views.setImageViewResource(R.id.favicon, R.drawable.ic_bookmark_widget_bookmark_holo_dark);
    250                 views.setDrawableParameters(R.id.thumb, true, 0, -1, null, -1);
    251             } else {
    252                 // RemoteViews require a valid bitmap config
    253                 Options options = new Options();
    254                 options.inPreferredConfig = Config.ARGB_8888;
    255                 Bitmap thumbnail = null, favicon = null;
    256                 byte[] blob = mBookmarks.getBlob(BOOKMARK_INDEX_THUMBNAIL);
    257                 views.setDrawableParameters(R.id.thumb, true, 255, -1, null, -1);
    258                 if (blob != null && blob.length > 0) {
    259                     thumbnail = BitmapFactory.decodeByteArray(
    260                             blob, 0, blob.length, options);
    261                     views.setImageViewBitmap(R.id.thumb, thumbnail);
    262                 } else {
    263                     views.setImageViewResource(R.id.thumb,
    264                             R.drawable.browser_thumbnail);
    265                 }
    266                 blob = mBookmarks.getBlob(BOOKMARK_INDEX_FAVICON);
    267                 if (blob != null && blob.length > 0) {
    268                     favicon = BitmapFactory.decodeByteArray(
    269                             blob, 0, blob.length, options);
    270                     views.setImageViewBitmap(R.id.favicon, favicon);
    271                 } else {
    272                     views.setImageViewResource(R.id.favicon,
    273                             R.drawable.app_web_browser_sm);
    274                 }
    275             }
    276             Intent fillin;
    277             if (isFolder) {
    278                 fillin = new Intent(ACTION_CHANGE_FOLDER)
    279                         .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId)
    280                         .putExtra(Bookmarks._ID, id);
    281             } else {
    282                 if (!TextUtils.isEmpty(url)) {
    283                     fillin = new Intent(Intent.ACTION_VIEW)
    284                             .addCategory(Intent.CATEGORY_BROWSABLE)
    285                             .setData(Uri.parse(url));
    286                 } else {
    287                     fillin = new Intent(BrowserActivity.ACTION_SHOW_BROWSER);
    288                 }
    289             }
    290             views.setOnClickFillInIntent(R.id.list_item, fillin);
    291             return views;
    292         }
    293 
    294         @Override
    295         public int getViewTypeCount() {
    296             return 2;
    297         }
    298 
    299         @Override
    300         public boolean hasStableIds() {
    301             return false;
    302         }
    303 
    304         @Override
    305         public void onCreate() {
    306         }
    307 
    308         @Override
    309         public void onDestroy() {
    310             if (mBookmarks != null) {
    311                 mBookmarks.close();
    312                 mBookmarks = null;
    313             }
    314             deleteWidgetState(mContext, mWidgetId);
    315         }
    316 
    317         @Override
    318         public void onDataSetChanged() {
    319             long token = Binder.clearCallingIdentity();
    320             syncState();
    321             if (mRootFolder < 0 || mCurrentFolder < 0) {
    322                 // This shouldn't happen, but JIC default to the local account
    323                 mRootFolder = BrowserProvider2.FIXED_ID_ROOT;
    324                 mCurrentFolder = mRootFolder;
    325                 saveState();
    326             }
    327             loadBookmarks();
    328             Binder.restoreCallingIdentity(token);
    329         }
    330 
    331         private void resetBookmarks() {
    332             if (mBookmarks != null) {
    333                 mBookmarks.close();
    334                 mBookmarks = null;
    335             }
    336         }
    337 
    338         void loadBookmarks() {
    339             resetBookmarks();
    340 
    341             Uri uri = ContentUris.withAppendedId(
    342                     BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER,
    343                     mCurrentFolder);
    344             mBookmarks = mContext.getContentResolver().query(uri, PROJECTION,
    345                     null, null, null);
    346             if (mCurrentFolder != mRootFolder) {
    347                 uri = ContentUris.withAppendedId(
    348                         BrowserContract.Bookmarks.CONTENT_URI,
    349                         mCurrentFolder);
    350                 Cursor c = mContext.getContentResolver().query(uri, PROJECTION,
    351                         null, null, null);
    352                 mBookmarks = new MergeCursor(new Cursor[] { c, mBookmarks });
    353             }
    354         }
    355     }
    356 
    357 }
    358