1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 package org.mozilla.gecko.db;
6 
7 import java.util.Collections;
8 import java.util.HashMap;
9 import java.util.Map;
10 
11 import org.mozilla.gecko.db.BrowserContract.Clients;
12 import org.mozilla.gecko.db.BrowserContract.Tabs;
13 
14 import android.content.ContentUris;
15 import android.content.ContentValues;
16 import android.content.UriMatcher;
17 import android.database.Cursor;
18 import android.database.sqlite.SQLiteDatabase;
19 import android.database.sqlite.SQLiteQueryBuilder;
20 import android.net.Uri;
21 import android.text.TextUtils;
22 
23 public class TabsProvider extends SharedBrowserDatabaseProvider {
u8_check(const uint8_t * s,size_t n)24     private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
25     private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS;
26     private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS;
27 
28     static final String TABLE_TABS = "tabs";
29     static final String TABLE_CLIENTS = "clients";
30 
31     static final int TABS = 600;
32     static final int TABS_ID = 601;
33     static final int CLIENTS = 602;
34     static final int CLIENTS_ID = 603;
35     static final int CLIENTS_RECENCY = 604;
36 
37     // Exclude clients that are more than three weeks old and also any duplicates that are older than one week old.
38     static final String EXCLUDE_STALE_CLIENTS_SUBQUERY =
39     "(SELECT " + Clients.GUID +
40     ", " + Clients.NAME +
41     ", " + Clients.LAST_MODIFIED +
42     ", " + Clients.DEVICE_TYPE +
43     "  FROM " + TABLE_CLIENTS +
44     "  WHERE " + Clients.LAST_MODIFIED + " > %1$s " +
45     " GROUP BY " + Clients.NAME +
46     " UNION ALL " +
47     " SELECT c." +  Clients.GUID + " AS " + Clients.GUID +
48     ", c." + Clients.NAME + " AS " + Clients.NAME +
49     ", c." + Clients.LAST_MODIFIED + " AS " + Clients.LAST_MODIFIED +
50     ", c." + Clients.DEVICE_TYPE + " AS " + Clients.DEVICE_TYPE +
51     " FROM " + TABLE_CLIENTS + " AS c " +
52     " JOIN (" +
53         " SELECT " + Clients.GUID +
54         ", " + "MAX( " + Clients.LAST_MODIFIED + ") AS " + Clients.LAST_MODIFIED +
55         " FROM " + TABLE_CLIENTS +
56         " WHERE (" + Clients.LAST_MODIFIED + " < %1$s" + " AND " + Clients.LAST_MODIFIED + " > %2$s) AND " +
57         Clients.NAME + " NOT IN " + "( SELECT " + Clients.NAME + " FROM " + TABLE_CLIENTS + " WHERE " + Clients.LAST_MODIFIED + " > %1$s)" +
58         " GROUP BY " + Clients.NAME +
59     ") AS c2" +
60     " ON c." + Clients.GUID + " = c2." + Clients.GUID + ")";
61 
62     static final String DEFAULT_TABS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC, " + Tabs.LAST_USED + " DESC";
63     static final String DEFAULT_CLIENTS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC";
64     static final String DEFAULT_CLIENTS_RECENCY_SORT_ORDER = "COALESCE(MAX(" + Tabs.LAST_USED + "), " + Clients.LAST_MODIFIED + ") DESC";
65 
66     static final String INDEX_TABS_GUID = "tabs_guid_index";
67     static final String INDEX_TABS_POSITION = "tabs_position_index";
68 
69     static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
70 
71     static final Map<String, String> TABS_PROJECTION_MAP;
72     static final Map<String, String> CLIENTS_PROJECTION_MAP;
73     static final Map<String, String> CLIENTS_RECENCY_PROJECTION_MAP;
74 
75     static {
76         URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs", TABS);
77         URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs/#", TABS_ID);
78         URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients", CLIENTS);
79         URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients/#", CLIENTS_ID);
80         URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients_recency", CLIENTS_RECENCY);
81 
82         HashMap<String, String> map;
83 
84         map = new HashMap<String, String>();
85         map.put(Tabs._ID, Tabs._ID);
86         map.put(Tabs.TITLE, Tabs.TITLE);
87         map.put(Tabs.URL, Tabs.URL);
88         map.put(Tabs.HISTORY, Tabs.HISTORY);
89         map.put(Tabs.FAVICON, Tabs.FAVICON);
90         map.put(Tabs.LAST_USED, Tabs.LAST_USED);
91         map.put(Tabs.POSITION, Tabs.POSITION);
92         map.put(Clients.GUID, Clients.GUID);
93         map.put(Clients.NAME, Clients.NAME);
94         map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED);
95         map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE);
96         TABS_PROJECTION_MAP = Collections.unmodifiableMap(map);
97 
98         map = new HashMap<String, String>();
99         map.put(Clients.GUID, Clients.GUID);
100         map.put(Clients.NAME, Clients.NAME);
101         map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED);
102         map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE);
103         CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map);
104 
105         map = new HashMap<>();
106         map.put(Clients.GUID, projectColumn(TABLE_CLIENTS, Clients.GUID) + " AS guid");
107         map.put(Clients.NAME, projectColumn(TABLE_CLIENTS, Clients.NAME) + " AS name");
108         map.put(Clients.LAST_MODIFIED, projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + " AS last_modified");
109         map.put(Clients.DEVICE_TYPE, projectColumn(TABLE_CLIENTS, Clients.DEVICE_TYPE) + " AS device_type");
110         // last_used is the max of the tab last_used times, or if there are no tabs,
111         // the client's last_modified time.
112         map.put(Tabs.LAST_USED, "COALESCE(MAX(" + projectColumn(TABLE_TABS, Tabs.LAST_USED) + "), " + projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + ") AS last_used");
113         CLIENTS_RECENCY_PROJECTION_MAP = Collections.unmodifiableMap(map);
114     }
115 
116     private static final String projectColumn(String table, String column) {
117         return table + "." + column;
118     }
119 
120     private static final String selectColumn(String table, String column) {
121         return projectColumn(table, column) + " = ?";
122     }
123 
124     @Override
125     public String getType(Uri uri) {
126         final int match = URI_MATCHER.match(uri);
127 
128         trace("Getting URI type: " + uri);
129 
130         switch (match) {
131             case TABS:
132                 trace("URI is TABS: " + uri);
133                 return Tabs.CONTENT_TYPE;
134 
135             case TABS_ID:
136                 trace("URI is TABS_ID: " + uri);
137                 return Tabs.CONTENT_ITEM_TYPE;
138 
139             case CLIENTS:
140                 trace("URI is CLIENTS: " + uri);
141                 return Clients.CONTENT_TYPE;
142 
143             case CLIENTS_ID:
144                 trace("URI is CLIENTS_ID: " + uri);
145                 return Clients.CONTENT_ITEM_TYPE;
146         }
147 
148         debug("URI has unrecognized type: " + uri);
149 
150         return null;
151     }
152 
153     @Override
154     @SuppressWarnings("fallthrough")
155     public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
156         trace("Calling delete in transaction on URI: " + uri);
157 
158         final int match = URI_MATCHER.match(uri);
159         int deleted = 0;
160 
161         switch (match) {
162             case CLIENTS_ID:
163                 trace("Delete on CLIENTS_ID: " + uri);
164                 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
165                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
166                         new String[] { Long.toString(ContentUris.parseId(uri)) });
167                 // fall through
168             case CLIENTS:
169                 trace("Delete on CLIENTS: " + uri);
170                 deleted = deleteValues(uri, selection, selectionArgs, TABLE_CLIENTS);
171                 break;
172 
173             case TABS_ID:
174                 trace("Delete on TABS_ID: " + uri);
175                 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID));
176                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
177                         new String[] { Long.toString(ContentUris.parseId(uri)) });
178                 // fall through
179             case TABS:
180                 trace("Deleting on TABS: " + uri);
181                 deleted = deleteValues(uri, selection, selectionArgs, TABLE_TABS);
182                 break;
183 
184             default:
185                 throw new UnsupportedOperationException("Unknown delete URI " + uri);
186         }
187 
188         debug("Deleted " + deleted + " rows for URI: " + uri);
189 
190         return deleted;
191     }
192 
193     @Override
194     public Uri insertInTransaction(Uri uri, ContentValues values) {
195         trace("Calling insert in transaction on URI: " + uri);
196 
197         final SQLiteDatabase db = getWritableDatabase(uri);
198         int match = URI_MATCHER.match(uri);
199         long id = -1;
200 
201         switch (match) {
202             case CLIENTS:
203                 String guid = values.getAsString(Clients.GUID);
204                 debug("Inserting client in database with GUID: " + guid);
205                 id = db.insertOrThrow(TABLE_CLIENTS, Clients.GUID, values);
206                 break;
207 
208             case TABS:
209                 String url = values.getAsString(Tabs.URL);
210                 debug("Inserting tab in database with URL: " + url);
211                 id = db.insertOrThrow(TABLE_TABS, Tabs.TITLE, values);
212                 break;
213 
214             default:
215                 throw new UnsupportedOperationException("Unknown insert URI " + uri);
216         }
217 
218         debug("Inserted ID in database: " + id);
219 
220         if (id >= 0)
221             return ContentUris.withAppendedId(uri, id);
222 
223         return null;
224     }
225 
226     @Override
227     public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
228         trace("Calling update in transaction on URI: " + uri);
229 
230         int match = URI_MATCHER.match(uri);
231         int updated = 0;
232 
233         switch (match) {
234             case CLIENTS_ID:
235                 trace("Update on CLIENTS_ID: " + uri);
236                 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
237                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
238                         new String[] { Long.toString(ContentUris.parseId(uri)) });
239                 // fall through
240             case CLIENTS:
241                 trace("Update on CLIENTS: " + uri);
242                 updated = updateValues(uri, values, selection, selectionArgs, TABLE_CLIENTS);
243                 break;
244 
245             case TABS_ID:
246                 trace("Update on TABS_ID: " + uri);
247                 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID));
248                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
249                         new String[] { Long.toString(ContentUris.parseId(uri)) });
250                 // fall through
251             case TABS:
252                 trace("Update on TABS: " + uri);
253                 updated = updateValues(uri, values, selection, selectionArgs, TABLE_TABS);
254                 break;
255 
256             default:
257                 throw new UnsupportedOperationException("Unknown update URI " + uri);
258         }
259 
260         debug("Updated " + updated + " rows for URI: " + uri);
261 
262         return updated;
263     }
264 
265     @Override
266     @SuppressWarnings("fallthrough")
267     public Cursor query(Uri uri, String[] projection, String selection,
268             String[] selectionArgs, String sortOrder) {
269         SQLiteDatabase db = getReadableDatabase(uri);
270         final int match = URI_MATCHER.match(uri);
271 
272         String groupBy = null;
273         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
274         String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
275 
276         switch (match) {
277             case TABS_ID:
278                 trace("Query is on TABS_ID: " + uri);
279                 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID));
280                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
281                         new String[] { Long.toString(ContentUris.parseId(uri)) });
282                 // fall through
283             case TABS:
284                 trace("Query is on TABS: " + uri);
285                 if (TextUtils.isEmpty(sortOrder)) {
286                     sortOrder = DEFAULT_TABS_SORT_ORDER;
287                 } else {
288                     debug("Using sort order " + sortOrder + ".");
289                 }
290 
291                 qb.setProjectionMap(TABS_PROJECTION_MAP);
292                 qb.setTables(TABLE_TABS + " LEFT OUTER JOIN " + TABLE_CLIENTS + " ON (" + TABLE_TABS + "." + Tabs.CLIENT_GUID + " = " + TABLE_CLIENTS + "." + Clients.GUID + ")");
293                 break;
294 
295             case CLIENTS_ID:
296                 trace("Query is on CLIENTS_ID: " + uri);
297                 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
298                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
299                         new String[] { Long.toString(ContentUris.parseId(uri)) });
300                 // fall through
301             case CLIENTS:
302                 trace("Query is on CLIENTS: " + uri);
303                 if (TextUtils.isEmpty(sortOrder)) {
304                     sortOrder = DEFAULT_CLIENTS_SORT_ORDER;
305                 } else {
306                     debug("Using sort order " + sortOrder + ".");
307                 }
308 
309                 qb.setProjectionMap(CLIENTS_PROJECTION_MAP);
310                 qb.setTables(TABLE_CLIENTS);
311                 break;
312 
313             case CLIENTS_RECENCY:
314                 trace("Query is on CLIENTS_RECENCY: " + uri);
315                 if (TextUtils.isEmpty(sortOrder)) {
316                     sortOrder = DEFAULT_CLIENTS_RECENCY_SORT_ORDER;
317                 } else {
318                     debug("Using sort order " + sortOrder + ".");
319                 }
320 
321                 final long oneWeekAgo = System.currentTimeMillis() - ONE_WEEK_IN_MILLISECONDS;
322                 final long threeWeeksAgo = System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS;
323 
324                 final String excludeStaleClientsTable = String.format(EXCLUDE_STALE_CLIENTS_SUBQUERY, oneWeekAgo, threeWeeksAgo);
325 
326                 qb.setProjectionMap(CLIENTS_RECENCY_PROJECTION_MAP);
327 
328                 // Use a subquery to quietly exclude stale duplicate client records.
329                 qb.setTables(excludeStaleClientsTable + " AS " + TABLE_CLIENTS + " LEFT OUTER JOIN " + TABLE_TABS +
330                         " ON (" + projectColumn(TABLE_CLIENTS, Clients.GUID) +
331                         " = " + projectColumn(TABLE_TABS, Tabs.CLIENT_GUID) + ")");
332                 groupBy = projectColumn(TABLE_CLIENTS, Clients.GUID);
333                 break;
334 
335             default:
336                 throw new UnsupportedOperationException("Unknown query URI " + uri);
337         }
338 
339         trace("Running built query.");
340         final Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
341         cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI);
342 
343         return cursor;
344     }
345 
346     int updateValues(Uri uri, ContentValues values, String selection, String[] selectionArgs, String table) {
347         trace("Updating tabs on URI: " + uri);
348 
349         final SQLiteDatabase db = getWritableDatabase(uri);
350         beginWrite(db);
351         return db.update(table, values, selection, selectionArgs);
352     }
353 
354     int deleteValues(Uri uri, String selection, String[] selectionArgs, String table) {
355         debug("Deleting tabs for URI: " + uri);
356 
357         final SQLiteDatabase db = getWritableDatabase(uri);
358         beginWrite(db);
359         return db.delete(table, selection, selectionArgs);
360     }
361 }
362