1/*
2 * Copyright (C) 2012 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.providers.partnerbookmarks;
18
19import android.content.ContentProvider;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.SharedPreferences;
24import android.content.SharedPreferences.Editor;
25import android.content.UriMatcher;
26import android.content.res.Configuration;
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.database.Cursor;
30import android.database.DatabaseUtils;
31import android.database.MatrixCursor;
32import android.database.sqlite.SQLiteDatabase;
33import android.database.sqlite.SQLiteOpenHelper;
34import android.database.sqlite.SQLiteQueryBuilder;
35import android.net.Uri;
36import android.text.TextUtils;
37import android.util.Log;
38
39import java.io.ByteArrayOutputStream;
40import java.io.File;
41import java.io.IOException;
42import java.io.InputStream;
43import java.util.ArrayList;
44import java.util.HashMap;
45import java.util.Iterator;
46import java.util.Map;
47import java.util.Set;
48
49/**
50 * Default partner bookmarks provider implementation of {@link PartnerBookmarksContract} API.
51 * It reads the flat list of bookmarks and the name of the root partner
52 * bookmarks folder using getResources() API.
53 *
54 * Sample resources structure:
55 *     res/
56 *         values/
57 *             strings.xml
58 *                  string name="bookmarks_folder_name"
59 *                  string-array name="bookmarks"
60 *                      item TITLE1
61 *                      item URL1
62 *                      item TITLE2
63 *                      item URL2...
64 *             bookmarks_icons.xml
65 *                  array name="bookmark_preloads"
66 *                      item @raw/favicon1
67 *                      item @raw/touchicon1
68 *                      item @raw/favicon2
69 *                      item @raw/touchicon2
70 *                      ...
71 */
72public class PartnerBookmarksProvider extends ContentProvider {
73    private static final String TAG = "PartnerBookmarksProvider";
74
75    // URI matcher
76    private static final int URI_MATCH_BOOKMARKS = 1000;
77    private static final int URI_MATCH_BOOKMARKS_ID = 1001;
78    private static final int URI_MATCH_BOOKMARKS_FOLDER = 1002;
79    private static final int URI_MATCH_BOOKMARKS_FOLDER_ID = 1003;
80    private static final int URI_MATCH_BOOKMARKS_PARTNER_BOOKMARKS_FOLDER_ID = 1004;
81
82    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
83    private static final Map<String, String> BOOKMARKS_PROJECTION_MAP
84            = new HashMap<String, String>();
85
86    // Default sort order for unsync'd bookmarks
87    private static final String DEFAULT_BOOKMARKS_SORT_ORDER =
88            PartnerBookmarksContract.Bookmarks.ID + " DESC, "
89                    + PartnerBookmarksContract.Bookmarks.ID + " ASC";
90
91    // Initial bookmark id when for getResources() importing
92    // Make sure to fix tests if you are changing this
93    private static final long FIXED_ID_PARTNER_BOOKMARKS_ROOT =
94            PartnerBookmarksContract.Bookmarks.BOOKMARK_PARENT_ROOT_ID + 1;
95
96    // DB table name
97    private static final String TABLE_BOOKMARKS = "bookmarks";
98
99    static {
100        final UriMatcher matcher = URI_MATCHER;
101        final String authority = PartnerBookmarksContract.AUTHORITY;
102        matcher.addURI(authority, "bookmarks", URI_MATCH_BOOKMARKS);
103        matcher.addURI(authority, "bookmarks/#", URI_MATCH_BOOKMARKS_ID);
104        matcher.addURI(authority, "bookmarks/folder", URI_MATCH_BOOKMARKS_FOLDER);
105        matcher.addURI(authority, "bookmarks/folder/#", URI_MATCH_BOOKMARKS_FOLDER_ID);
106        matcher.addURI(authority, "bookmarks/folder/id",
107                URI_MATCH_BOOKMARKS_PARTNER_BOOKMARKS_FOLDER_ID);
108        // Projection maps
109        Map<String, String> map = BOOKMARKS_PROJECTION_MAP;
110        map.put(PartnerBookmarksContract.Bookmarks.ID,
111                PartnerBookmarksContract.Bookmarks.ID);
112        map.put(PartnerBookmarksContract.Bookmarks.TITLE,
113                PartnerBookmarksContract.Bookmarks.TITLE);
114        map.put(PartnerBookmarksContract.Bookmarks.URL,
115                PartnerBookmarksContract.Bookmarks.URL);
116        map.put(PartnerBookmarksContract.Bookmarks.TYPE,
117                PartnerBookmarksContract.Bookmarks.TYPE);
118        map.put(PartnerBookmarksContract.Bookmarks.PARENT,
119                PartnerBookmarksContract.Bookmarks.PARENT);
120        map.put(PartnerBookmarksContract.Bookmarks.FAVICON,
121                PartnerBookmarksContract.Bookmarks.FAVICON);
122        map.put(PartnerBookmarksContract.Bookmarks.TOUCHICON,
123                PartnerBookmarksContract.Bookmarks.TOUCHICON);
124    }
125
126    private final class DatabaseHelper extends SQLiteOpenHelper {
127        private static final String DATABASE_FILENAME = "partnerBookmarks.db";
128        private static final int DATABASE_VERSION = 1;
129        private static final String PREFERENCES_FILENAME = "pbppref";
130        private static final String ACTIVE_CONFIGURATION_PREFNAME = "config";
131        private final SharedPreferences sharedPreferences;
132
133        public DatabaseHelper(Context context) {
134            super(context, DATABASE_FILENAME, null, DATABASE_VERSION);
135            sharedPreferences = context.getSharedPreferences(
136                    PREFERENCES_FILENAME, Context.MODE_PRIVATE);
137        }
138
139        private String getConfigSignature(Configuration config) {
140            return "mmc=" + Integer.toString(config.mcc)
141                    + "-mnc=" + Integer.toString(config.mnc)
142                    + "-loc=" + config.locale.toString();
143        }
144
145        public synchronized void prepareForConfiguration(Configuration config) {
146            final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
147            String newSignature = getConfigSignature(config);
148            String activeSignature =
149                    sharedPreferences.getString(ACTIVE_CONFIGURATION_PREFNAME, null);
150            if (activeSignature == null || !activeSignature.equals(newSignature)) {
151                db.delete(TABLE_BOOKMARKS, null, null);
152                if (!createDefaultBookmarks(db)) {
153                    // Failure to read/insert bookmarks should be treated as "no bookmarks"
154                    db.delete(TABLE_BOOKMARKS, null, null);
155                }
156            }
157        }
158
159        private void setActiveConfiguration(Configuration config) {
160            Editor editor = sharedPreferences.edit();
161            editor.putString(ACTIVE_CONFIGURATION_PREFNAME, getConfigSignature(config));
162            editor.apply();
163        }
164
165        private void createTable(SQLiteDatabase db) {
166            db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
167                    PartnerBookmarksContract.Bookmarks.ID +
168                    " INTEGER NOT NULL DEFAULT 0," +
169                    PartnerBookmarksContract.Bookmarks.TITLE +
170                    " TEXT," +
171                    PartnerBookmarksContract.Bookmarks.URL +
172                    " TEXT," +
173                    PartnerBookmarksContract.Bookmarks.TYPE +
174                    " INTEGER NOT NULL DEFAULT 0," +
175                    PartnerBookmarksContract.Bookmarks.PARENT +
176                    " INTEGER," +
177                    PartnerBookmarksContract.Bookmarks.FAVICON +
178                    " BLOB," +
179                    PartnerBookmarksContract.Bookmarks.TOUCHICON +
180                    " BLOB" + ");");
181        }
182
183        private void dropTable(SQLiteDatabase db) {
184            db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS);
185        }
186
187        @Override
188        public void onCreate(SQLiteDatabase db) {
189            synchronized (this) {
190                createTable(db);
191                if (!createDefaultBookmarks(db)) {
192                    // Failure to read/insert bookmarks should be treated as "no bookmarks"
193                    dropTable(db);
194                    createTable(db);
195                }
196            }
197        }
198
199        @Override
200        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
201            dropTable(db);
202            onCreate(db);
203        }
204
205        @Override
206        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
207            dropTable(db);
208            onCreate(db);
209        }
210
211        private boolean createDefaultBookmarks(SQLiteDatabase db) {
212            Resources res = getContext().getResources();
213            try {
214                CharSequence bookmarksFolderName = res.getText(R.string.bookmarks_folder_name);
215                final CharSequence[] bookmarks = res.getTextArray(R.array.bookmarks);
216                if (bookmarks.length >= 1) {
217                    if (bookmarksFolderName.length() < 1) {
218                        Log.i(TAG, "bookmarks_folder_name was not specified; bailing out");
219                        return false;
220                    }
221                    if (!addRootFolder(db,
222                            FIXED_ID_PARTNER_BOOKMARKS_ROOT, bookmarksFolderName.toString())) {
223                        Log.i(TAG, "failed to insert root folder; bailing out");
224                        return false;
225                    }
226                    if (!addDefaultBookmarks(db,
227                            FIXED_ID_PARTNER_BOOKMARKS_ROOT, FIXED_ID_PARTNER_BOOKMARKS_ROOT + 1)) {
228                        Log.i(TAG, "failed to insert bookmarks; bailing out");
229                        return false;
230                    }
231                }
232                setActiveConfiguration(res.getConfiguration());
233            } catch (android.content.res.Resources.NotFoundException e) {
234                Log.i(TAG, "failed to fetch resources; bailing out");
235                return false;
236            }
237            return true;
238        }
239
240        private boolean addRootFolder(SQLiteDatabase db, long id, String bookmarksFolderName) {
241            ContentValues values = new ContentValues();
242            values.put(PartnerBookmarksContract.Bookmarks.ID, id);
243            values.put(PartnerBookmarksContract.Bookmarks.TITLE,
244                    bookmarksFolderName);
245            values.put(PartnerBookmarksContract.Bookmarks.PARENT,
246                    PartnerBookmarksContract.Bookmarks.BOOKMARK_PARENT_ROOT_ID);
247            values.put(PartnerBookmarksContract.Bookmarks.TYPE,
248                    PartnerBookmarksContract.Bookmarks.BOOKMARK_TYPE_FOLDER);
249            return db.insertOrThrow(TABLE_BOOKMARKS, null, values) != -1;
250        }
251
252        private boolean addDefaultBookmarks(SQLiteDatabase db, long parentId, long firstBookmarkId) {
253            long bookmarkId = firstBookmarkId;
254            Resources res = getContext().getResources();
255            final CharSequence[] bookmarks = res.getTextArray(R.array.bookmarks);
256            int size = bookmarks.length;
257            TypedArray preloads = res.obtainTypedArray(R.array.bookmark_preloads);
258            DatabaseUtils.InsertHelper insertHelper = null;
259            try {
260                insertHelper = new DatabaseUtils.InsertHelper(db, TABLE_BOOKMARKS);
261                final int idColumn = insertHelper.getColumnIndex(
262                        PartnerBookmarksContract.Bookmarks.ID);
263                final int titleColumn = insertHelper.getColumnIndex(
264                        PartnerBookmarksContract.Bookmarks.TITLE);
265                final int urlColumn = insertHelper.getColumnIndex(
266                        PartnerBookmarksContract.Bookmarks.URL);
267                final int typeColumn = insertHelper.getColumnIndex(
268                        PartnerBookmarksContract.Bookmarks.TYPE);
269                final int parentColumn = insertHelper.getColumnIndex(
270                        PartnerBookmarksContract.Bookmarks.PARENT);
271                final int faviconColumn = insertHelper.getColumnIndex(
272                        PartnerBookmarksContract.Bookmarks.FAVICON);
273                final int touchiconColumn = insertHelper.getColumnIndex(
274                        PartnerBookmarksContract.Bookmarks.TOUCHICON);
275
276                for (int i = 0; i + 1 < size; i = i + 2) {
277                    CharSequence bookmarkDestination = bookmarks[i + 1];
278
279                    String bookmarkTitle = bookmarks[i].toString();
280                    String bookmarkUrl = bookmarkDestination.toString();
281                    byte[] favicon = null;
282                    if (i < preloads.length()) {
283                        int faviconId = preloads.getResourceId(i, 0);
284                        try {
285                            favicon = readRaw(res, faviconId);
286                        } catch (IOException e) {
287                            Log.i(TAG, "Failed to read favicon for " + bookmarkTitle, e);
288                        }
289                    }
290                    byte[] touchicon = null;
291                    if (i + 1 < preloads.length()) {
292                        int touchiconId = preloads.getResourceId(i + 1, 0);
293                        try {
294                            touchicon = readRaw(res, touchiconId);
295                        } catch (IOException e) {
296                            Log.i(TAG, "Failed to read touchicon for " + bookmarkTitle, e);
297                        }
298                    }
299                    insertHelper.prepareForInsert();
300                    insertHelper.bind(idColumn, bookmarkId);
301                    insertHelper.bind(titleColumn, bookmarkTitle);
302                    insertHelper.bind(urlColumn, bookmarkUrl);
303                    insertHelper.bind(typeColumn,
304                            PartnerBookmarksContract.Bookmarks.BOOKMARK_TYPE_BOOKMARK);
305                    insertHelper.bind(parentColumn, parentId);
306                    if (favicon != null) {
307                        insertHelper.bind(faviconColumn, favicon);
308                    }
309                    if (touchicon != null) {
310                        insertHelper.bind(touchiconColumn, touchicon);
311                    }
312                    bookmarkId++;
313                    if (insertHelper.execute() == -1) {
314                        Log.i(TAG, "Failed to insert bookmark " + bookmarkTitle);
315                        return false;
316                    }
317                }
318            } finally {
319                preloads.recycle();
320                insertHelper.close();
321            }
322            return true;
323        }
324
325        private byte[] readRaw(Resources res, int id) throws IOException {
326            if (id == 0) return null;
327            InputStream is = res.openRawResource(id);
328            ByteArrayOutputStream bos = new ByteArrayOutputStream();
329            try {
330                byte[] buf = new byte[4096];
331                int read;
332                while ((read = is.read(buf)) > 0) {
333                    bos.write(buf, 0, read);
334                }
335                bos.flush();
336                return bos.toByteArray();
337            } finally {
338                is.close();
339                bos.close();
340            }
341        }
342    }
343
344    private DatabaseHelper mOpenHelper;
345
346    @Override
347    public boolean onCreate() {
348        mOpenHelper = new DatabaseHelper(getContext());
349        return true;
350    }
351
352    @Override
353    public void onConfigurationChanged(Configuration newConfig) {
354        mOpenHelper.prepareForConfiguration(getContext().getResources().getConfiguration());
355    }
356
357    @Override
358    public Cursor query(Uri uri, String[] projection,
359            String selection, String[] selectionArgs, String sortOrder) {
360        final int match = URI_MATCHER.match(uri);
361        mOpenHelper.prepareForConfiguration(getContext().getResources().getConfiguration());
362        final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
363        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
364        String limit = uri.getQueryParameter(PartnerBookmarksContract.PARAM_LIMIT);
365        String groupBy = uri.getQueryParameter(PartnerBookmarksContract.PARAM_GROUP_BY);
366        switch (match) {
367            case URI_MATCH_BOOKMARKS_FOLDER_ID:
368            case URI_MATCH_BOOKMARKS_ID:
369            case URI_MATCH_BOOKMARKS: {
370                if (match == URI_MATCH_BOOKMARKS_ID) {
371                    // Tack on the ID of the specific bookmark requested
372                    selection = DatabaseUtils.concatenateWhere(selection,
373                            TABLE_BOOKMARKS + "." +
374                                    PartnerBookmarksContract.Bookmarks.ID + "=?");
375                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
376                            new String[] { Long.toString(ContentUris.parseId(uri)) });
377                } else if (match == URI_MATCH_BOOKMARKS_FOLDER_ID) {
378                    // Tack on the ID of the specific folder requested
379                    selection = DatabaseUtils.concatenateWhere(selection,
380                            TABLE_BOOKMARKS + "." +
381                                    PartnerBookmarksContract.Bookmarks.PARENT + "=?");
382                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
383                            new String[] { Long.toString(ContentUris.parseId(uri)) });
384                }
385                // Set a default sort order if one isn't specified
386                if (TextUtils.isEmpty(sortOrder)) {
387                    sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
388                }
389                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
390                qb.setTables(TABLE_BOOKMARKS);
391                break;
392            }
393
394            case URI_MATCH_BOOKMARKS_FOLDER: {
395                qb.setTables(TABLE_BOOKMARKS);
396                String[] args;
397                String query;
398                // Set a default sort order if one isn't specified
399                if (TextUtils.isEmpty(sortOrder)) {
400                    sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
401                }
402                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
403                String where = PartnerBookmarksContract.Bookmarks.PARENT + "=?";
404                where = DatabaseUtils.concatenateWhere(where, selection);
405                args = new String[] { Long.toString(FIXED_ID_PARTNER_BOOKMARKS_ROOT) };
406                if (selectionArgs != null) {
407                    args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
408                }
409                query = qb.buildQuery(projection, where, null, null, sortOrder, null);
410                Cursor cursor = db.rawQuery(query, args);
411                return cursor;
412            }
413
414            case URI_MATCH_BOOKMARKS_PARTNER_BOOKMARKS_FOLDER_ID: {
415                MatrixCursor c = new MatrixCursor(
416                        new String[] {PartnerBookmarksContract.Bookmarks.ID});
417                c.newRow().add(FIXED_ID_PARTNER_BOOKMARKS_ROOT);
418                return c;
419            }
420
421            default: {
422                throw new UnsupportedOperationException("Unknown URL " + uri.toString());
423            }
424        }
425
426        return qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
427    }
428
429    @Override
430    public String getType(Uri uri) {
431        final int match = URI_MATCHER.match(uri);
432        if (match == UriMatcher.NO_MATCH) return null;
433        return PartnerBookmarksContract.Bookmarks.CONTENT_ITEM_TYPE;
434    }
435
436    @Override
437    public Uri insert(Uri uri, ContentValues values) {
438        throw new UnsupportedOperationException();
439    }
440
441    @Override
442    public int delete(Uri uri, String selection, String[] selectionArgs) {
443        throw new UnsupportedOperationException();
444    }
445
446    @Override
447    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
448        throw new UnsupportedOperationException();
449    }
450}
451