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
17package com.android.browser.widget;
18
19import android.appwidget.AppWidgetManager;
20import android.content.ContentUris;
21import android.content.Context;
22import android.content.Intent;
23import android.content.SharedPreferences;
24import android.database.Cursor;
25import android.database.MergeCursor;
26import android.graphics.Bitmap;
27import android.graphics.Bitmap.Config;
28import android.graphics.BitmapFactory;
29import android.graphics.BitmapFactory.Options;
30import android.net.Uri;
31import android.os.Binder;
32import android.provider.BrowserContract;
33import android.provider.BrowserContract.Bookmarks;
34import android.text.TextUtils;
35import android.util.Log;
36import android.widget.RemoteViews;
37import android.widget.RemoteViewsService;
38
39import com.android.browser.BrowserActivity;
40import com.android.browser.R;
41import com.android.browser.provider.BrowserProvider2;
42
43import java.io.File;
44import java.io.FilenameFilter;
45import java.util.HashSet;
46import java.util.regex.Matcher;
47import java.util.regex.Pattern;
48
49public 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