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 
17 package com.android.providers.partnerbookmarks;
18 
19 import android.content.ContentProvider;
20 import android.content.ContentUris;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.SharedPreferences;
24 import android.content.SharedPreferences.Editor;
25 import android.content.UriMatcher;
26 import android.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.content.res.TypedArray;
29 import android.database.Cursor;
30 import android.database.DatabaseUtils;
31 import android.database.MatrixCursor;
32 import android.database.sqlite.SQLiteDatabase;
33 import android.database.sqlite.SQLiteOpenHelper;
34 import android.database.sqlite.SQLiteQueryBuilder;
35 import android.net.Uri;
36 import android.text.TextUtils;
37 import android.util.Log;
38 
39 import java.io.ByteArrayOutputStream;
40 import java.io.File;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.Iterator;
46 import java.util.Map;
47 import 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  */
72 public 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;
matcher.addURI(authority, "bookmarks", URI_MATCH_BOOKMARKS)102         matcher.addURI(authority, "bookmarks", URI_MATCH_BOOKMARKS);
matcher.addURI(authority, "bookmarks/#", URI_MATCH_BOOKMARKS_ID)103         matcher.addURI(authority, "bookmarks/#", URI_MATCH_BOOKMARKS_ID);
matcher.addURI(authority, "bookmarks/folder", URI_MATCH_BOOKMARKS_FOLDER)104         matcher.addURI(authority, "bookmarks/folder", URI_MATCH_BOOKMARKS_FOLDER);
matcher.addURI(authority, "bookmarks/folder/#", URI_MATCH_BOOKMARKS_FOLDER_ID)105         matcher.addURI(authority, "bookmarks/folder/#", URI_MATCH_BOOKMARKS_FOLDER_ID);
matcher.addURI(authority, "bookmarks/folder/id", URI_MATCH_BOOKMARKS_PARTNER_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;
map.put(PartnerBookmarksContract.Bookmarks.ID, PartnerBookmarksContract.Bookmarks.ID)110         map.put(PartnerBookmarksContract.Bookmarks.ID,
111                 PartnerBookmarksContract.Bookmarks.ID);
map.put(PartnerBookmarksContract.Bookmarks.TITLE, PartnerBookmarksContract.Bookmarks.TITLE)112         map.put(PartnerBookmarksContract.Bookmarks.TITLE,
113                 PartnerBookmarksContract.Bookmarks.TITLE);
map.put(PartnerBookmarksContract.Bookmarks.URL, PartnerBookmarksContract.Bookmarks.URL)114         map.put(PartnerBookmarksContract.Bookmarks.URL,
115                 PartnerBookmarksContract.Bookmarks.URL);
map.put(PartnerBookmarksContract.Bookmarks.TYPE, PartnerBookmarksContract.Bookmarks.TYPE)116         map.put(PartnerBookmarksContract.Bookmarks.TYPE,
117                 PartnerBookmarksContract.Bookmarks.TYPE);
map.put(PartnerBookmarksContract.Bookmarks.PARENT, PartnerBookmarksContract.Bookmarks.PARENT)118         map.put(PartnerBookmarksContract.Bookmarks.PARENT,
119                 PartnerBookmarksContract.Bookmarks.PARENT);
map.put(PartnerBookmarksContract.Bookmarks.FAVICON, PartnerBookmarksContract.Bookmarks.FAVICON)120         map.put(PartnerBookmarksContract.Bookmarks.FAVICON,
121                 PartnerBookmarksContract.Bookmarks.FAVICON);
map.put(PartnerBookmarksContract.Bookmarks.TOUCHICON, PartnerBookmarksContract.Bookmarks.TOUCHICON)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 
DatabaseHelper(Context context)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 
getConfigSignature(Configuration config)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 
prepareForConfiguration(Configuration config)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 
setActiveConfiguration(Configuration config)159         private void setActiveConfiguration(Configuration config) {
160             Editor editor = sharedPreferences.edit();
161             editor.putString(ACTIVE_CONFIGURATION_PREFNAME, getConfigSignature(config));
162             editor.apply();
163         }
164 
createTable(SQLiteDatabase db)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 
dropTable(SQLiteDatabase db)183         private void dropTable(SQLiteDatabase db) {
184             db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS);
185         }
186 
187         @Override
onCreate(SQLiteDatabase db)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
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)200         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
201             dropTable(db);
202             onCreate(db);
203         }
204 
205         @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)206         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
207             dropTable(db);
208             onCreate(db);
209         }
210 
createDefaultBookmarks(SQLiteDatabase db)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 
addRootFolder(SQLiteDatabase db, long id, String bookmarksFolderName)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 
addDefaultBookmarks(SQLiteDatabase db, long parentId, long firstBookmarkId)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 
readRaw(Resources res, int id)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
onCreate()347     public boolean onCreate() {
348         mOpenHelper = new DatabaseHelper(getContext());
349         return true;
350     }
351 
352     @Override
onConfigurationChanged(Configuration newConfig)353     public void onConfigurationChanged(Configuration newConfig) {
354         mOpenHelper.prepareForConfiguration(getContext().getResources().getConfiguration());
355     }
356 
357     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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
getType(Uri uri)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
insert(Uri uri, ContentValues values)437     public Uri insert(Uri uri, ContentValues values) {
438         throw new UnsupportedOperationException();
439     }
440 
441     @Override
delete(Uri uri, String selection, String[] selectionArgs)442     public int delete(Uri uri, String selection, String[] selectionArgs) {
443         throw new UnsupportedOperationException();
444     }
445 
446     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)447     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
448         throw new UnsupportedOperationException();
449     }
450 }
451