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
onGetViewFactory(Intent intent)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 
getWidgetState(Context context, int widgetId)85     static SharedPreferences getWidgetState(Context context, int widgetId) {
86         return context.getSharedPreferences(
87                 String.format("widgetState-%d", widgetId),
88                 Context.MODE_PRIVATE);
89     }
90 
deleteWidgetState(Context context, int widgetId)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 
changeFolder(Context context, Intent intent)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 
setupWidgetState(Context context, int widgetId, long rootFolder)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      */
removeOrphanedStates(Context context, int[] widgetIds)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 
StateFilter(int[] ids)141         StateFilter(int[] ids) {
142             mWidgetIds = new HashSet<Integer>();
143             for (int id : ids) {
144                 mWidgetIds.add(id);
145             }
146         }
147 
148         @Override
accept(File dir, String filename)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 
BookmarkFactory(Context context, int widgetId)170         public BookmarkFactory(Context context, int widgetId) {
171             mContext = context.getApplicationContext();
172             mWidgetId = widgetId;
173         }
174 
syncState()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 
saveState()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
getCount()198         public int getCount() {
199             if (mBookmarks == null)
200                 return 0;
201             return mBookmarks.getCount();
202         }
203 
204         @Override
getItemId(int position)205         public long getItemId(int position) {
206             return position;
207         }
208 
209         @Override
getLoadingView()210         public RemoteViews getLoadingView() {
211             return new RemoteViews(
212                     mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item);
213         }
214 
215         @Override
getViewAt(int position)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
getViewTypeCount()295         public int getViewTypeCount() {
296             return 2;
297         }
298 
299         @Override
hasStableIds()300         public boolean hasStableIds() {
301             return false;
302         }
303 
304         @Override
onCreate()305         public void onCreate() {
306         }
307 
308         @Override
onDestroy()309         public void onDestroy() {
310             if (mBookmarks != null) {
311                 mBookmarks.close();
312                 mBookmarks = null;
313             }
314             deleteWidgetState(mContext, mWidgetId);
315         }
316 
317         @Override
onDataSetChanged()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 
resetBookmarks()331         private void resetBookmarks() {
332             if (mBookmarks != null) {
333                 mBookmarks.close();
334                 mBookmarks = null;
335             }
336         }
337 
loadBookmarks()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