|
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.Context; |
|
17 import android.content.UriMatcher; |
|
18 import android.database.Cursor; |
|
19 import android.database.sqlite.SQLiteDatabase; |
|
20 import android.database.sqlite.SQLiteOpenHelper; |
|
21 import android.database.sqlite.SQLiteQueryBuilder; |
|
22 import android.net.Uri; |
|
23 import android.text.TextUtils; |
|
24 |
|
25 public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDatabaseHelper> { |
|
26 static final String DATABASE_NAME = "tabs.db"; |
|
27 |
|
28 static final int DATABASE_VERSION = 2; |
|
29 |
|
30 static final String TABLE_TABS = "tabs"; |
|
31 static final String TABLE_CLIENTS = "clients"; |
|
32 |
|
33 static final int TABS = 600; |
|
34 static final int TABS_ID = 601; |
|
35 static final int CLIENTS = 602; |
|
36 static final int CLIENTS_ID = 603; |
|
37 |
|
38 static final String DEFAULT_TABS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC, " + Tabs.LAST_USED + " DESC"; |
|
39 static final String DEFAULT_CLIENTS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC"; |
|
40 |
|
41 static final String INDEX_TABS_GUID = "tabs_guid_index"; |
|
42 static final String INDEX_TABS_POSITION = "tabs_position_index"; |
|
43 static final String INDEX_CLIENTS_GUID = "clients_guid_index"; |
|
44 |
|
45 static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); |
|
46 |
|
47 static final Map<String, String> TABS_PROJECTION_MAP; |
|
48 static final Map<String, String> CLIENTS_PROJECTION_MAP; |
|
49 |
|
50 static { |
|
51 URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs", TABS); |
|
52 URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs/#", TABS_ID); |
|
53 URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients", CLIENTS); |
|
54 URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients/#", CLIENTS_ID); |
|
55 |
|
56 HashMap<String, String> map; |
|
57 |
|
58 map = new HashMap<String, String>(); |
|
59 map.put(Tabs._ID, Tabs._ID); |
|
60 map.put(Tabs.TITLE, Tabs.TITLE); |
|
61 map.put(Tabs.URL, Tabs.URL); |
|
62 map.put(Tabs.HISTORY, Tabs.HISTORY); |
|
63 map.put(Tabs.FAVICON, Tabs.FAVICON); |
|
64 map.put(Tabs.LAST_USED, Tabs.LAST_USED); |
|
65 map.put(Tabs.POSITION, Tabs.POSITION); |
|
66 map.put(Clients.GUID, Clients.GUID); |
|
67 map.put(Clients.NAME, Clients.NAME); |
|
68 map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED); |
|
69 TABS_PROJECTION_MAP = Collections.unmodifiableMap(map); |
|
70 |
|
71 map = new HashMap<String, String>(); |
|
72 map.put(Clients.GUID, Clients.GUID); |
|
73 map.put(Clients.NAME, Clients.NAME); |
|
74 map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED); |
|
75 CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map); |
|
76 } |
|
77 |
|
78 private static final String selectColumn(String table, String column) { |
|
79 return table + "." + column + " = ?"; |
|
80 } |
|
81 |
|
82 final class TabsDatabaseHelper extends SQLiteOpenHelper { |
|
83 public TabsDatabaseHelper(Context context, String databasePath) { |
|
84 super(context, databasePath, null, DATABASE_VERSION); |
|
85 } |
|
86 |
|
87 @Override |
|
88 public void onCreate(SQLiteDatabase db) { |
|
89 debug("Creating tabs.db: " + db.getPath()); |
|
90 debug("Creating " + TABLE_TABS + " table"); |
|
91 |
|
92 // Table for each tab on any client. |
|
93 db.execSQL("CREATE TABLE " + TABLE_TABS + "(" + |
|
94 Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + |
|
95 Tabs.CLIENT_GUID + " TEXT," + |
|
96 Tabs.TITLE + " TEXT," + |
|
97 Tabs.URL + " TEXT," + |
|
98 Tabs.HISTORY + " TEXT," + |
|
99 Tabs.FAVICON + " TEXT," + |
|
100 Tabs.LAST_USED + " INTEGER," + |
|
101 Tabs.POSITION + " INTEGER" + |
|
102 ");"); |
|
103 |
|
104 // Indices on CLIENT_GUID and POSITION. |
|
105 db.execSQL("CREATE INDEX " + INDEX_TABS_GUID + |
|
106 " ON " + TABLE_TABS + "(" + Tabs.CLIENT_GUID + ")"); |
|
107 db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION + |
|
108 " ON " + TABLE_TABS + "(" + Tabs.POSITION + ")"); |
|
109 |
|
110 debug("Creating " + TABLE_CLIENTS + " table"); |
|
111 |
|
112 // Table for client's name-guid mapping. |
|
113 db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" + |
|
114 Clients.GUID + " TEXT PRIMARY KEY," + |
|
115 Clients.NAME + " TEXT," + |
|
116 Clients.LAST_MODIFIED + " INTEGER" + |
|
117 ");"); |
|
118 |
|
119 // Index on GUID. |
|
120 db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID + |
|
121 " ON " + TABLE_CLIENTS + "(" + Clients.GUID + ")"); |
|
122 |
|
123 createLocalClient(db); |
|
124 } |
|
125 |
|
126 // Insert a client row for our local Fennec client. |
|
127 private void createLocalClient(SQLiteDatabase db) { |
|
128 debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table"); |
|
129 |
|
130 ContentValues values = new ContentValues(); |
|
131 values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis()); |
|
132 db.insertOrThrow(TABLE_CLIENTS, null, values); |
|
133 } |
|
134 |
|
135 @Override |
|
136 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
|
137 debug("Upgrading tabs.db: " + db.getPath() + " from " + |
|
138 oldVersion + " to " + newVersion); |
|
139 |
|
140 // We have to do incremental upgrades until we reach the current |
|
141 // database schema version. |
|
142 for (int v = oldVersion + 1; v <= newVersion; v++) { |
|
143 switch(v) { |
|
144 case 2: |
|
145 createLocalClient(db); |
|
146 break; |
|
147 } |
|
148 } |
|
149 } |
|
150 |
|
151 @Override |
|
152 public void onOpen(SQLiteDatabase db) { |
|
153 debug("Opening tabs.db: " + db.getPath()); |
|
154 db.rawQuery("PRAGMA synchronous=OFF", null).close(); |
|
155 |
|
156 if (shouldUseTransactions()) { |
|
157 db.enableWriteAheadLogging(); |
|
158 db.setLockingEnabled(false); |
|
159 return; |
|
160 } |
|
161 |
|
162 // If we're not using transactions (in particular, prior to |
|
163 // Honeycomb), then we can do some lesser optimizations. |
|
164 db.rawQuery("PRAGMA journal_mode=PERSIST", null).close(); |
|
165 } |
|
166 } |
|
167 |
|
168 @Override |
|
169 public String getType(Uri uri) { |
|
170 final int match = URI_MATCHER.match(uri); |
|
171 |
|
172 trace("Getting URI type: " + uri); |
|
173 |
|
174 switch (match) { |
|
175 case TABS: |
|
176 trace("URI is TABS: " + uri); |
|
177 return Tabs.CONTENT_TYPE; |
|
178 |
|
179 case TABS_ID: |
|
180 trace("URI is TABS_ID: " + uri); |
|
181 return Tabs.CONTENT_ITEM_TYPE; |
|
182 |
|
183 case CLIENTS: |
|
184 trace("URI is CLIENTS: " + uri); |
|
185 return Clients.CONTENT_TYPE; |
|
186 |
|
187 case CLIENTS_ID: |
|
188 trace("URI is CLIENTS_ID: " + uri); |
|
189 return Clients.CONTENT_ITEM_TYPE; |
|
190 } |
|
191 |
|
192 debug("URI has unrecognized type: " + uri); |
|
193 |
|
194 return null; |
|
195 } |
|
196 |
|
197 @SuppressWarnings("fallthrough") |
|
198 public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { |
|
199 trace("Calling delete in transaction on URI: " + uri); |
|
200 |
|
201 final int match = URI_MATCHER.match(uri); |
|
202 int deleted = 0; |
|
203 |
|
204 switch (match) { |
|
205 case CLIENTS_ID: |
|
206 trace("Delete on CLIENTS_ID: " + uri); |
|
207 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID)); |
|
208 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, |
|
209 new String[] { Long.toString(ContentUris.parseId(uri)) }); |
|
210 // fall through |
|
211 case CLIENTS: |
|
212 trace("Delete on CLIENTS: " + uri); |
|
213 // Delete from both TABLE_TABS and TABLE_CLIENTS. |
|
214 deleteValues(uri, selection, selectionArgs, TABLE_TABS); |
|
215 deleted = deleteValues(uri, selection, selectionArgs, TABLE_CLIENTS); |
|
216 break; |
|
217 |
|
218 case TABS_ID: |
|
219 trace("Delete on TABS_ID: " + uri); |
|
220 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); |
|
221 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, |
|
222 new String[] { Long.toString(ContentUris.parseId(uri)) }); |
|
223 // fall through |
|
224 case TABS: |
|
225 trace("Deleting on TABS: " + uri); |
|
226 deleted = deleteValues(uri, selection, selectionArgs, TABLE_TABS); |
|
227 break; |
|
228 |
|
229 default: |
|
230 throw new UnsupportedOperationException("Unknown delete URI " + uri); |
|
231 } |
|
232 |
|
233 debug("Deleted " + deleted + " rows for URI: " + uri); |
|
234 |
|
235 return deleted; |
|
236 } |
|
237 |
|
238 public Uri insertInTransaction(Uri uri, ContentValues values) { |
|
239 trace("Calling insert in transaction on URI: " + uri); |
|
240 |
|
241 final SQLiteDatabase db = getWritableDatabase(uri); |
|
242 int match = URI_MATCHER.match(uri); |
|
243 long id = -1; |
|
244 |
|
245 switch (match) { |
|
246 case CLIENTS: |
|
247 String guid = values.getAsString(Clients.GUID); |
|
248 debug("Inserting client in database with GUID: " + guid); |
|
249 id = db.insertOrThrow(TABLE_CLIENTS, Clients.GUID, values); |
|
250 break; |
|
251 |
|
252 case TABS: |
|
253 String url = values.getAsString(Tabs.URL); |
|
254 debug("Inserting tab in database with URL: " + url); |
|
255 id = db.insertOrThrow(TABLE_TABS, Tabs.TITLE, values); |
|
256 break; |
|
257 |
|
258 default: |
|
259 throw new UnsupportedOperationException("Unknown insert URI " + uri); |
|
260 } |
|
261 |
|
262 debug("Inserted ID in database: " + id); |
|
263 |
|
264 if (id >= 0) |
|
265 return ContentUris.withAppendedId(uri, id); |
|
266 |
|
267 return null; |
|
268 } |
|
269 |
|
270 public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
|
271 trace("Calling update in transaction on URI: " + uri); |
|
272 |
|
273 int match = URI_MATCHER.match(uri); |
|
274 int updated = 0; |
|
275 |
|
276 switch (match) { |
|
277 case CLIENTS_ID: |
|
278 trace("Update on CLIENTS_ID: " + uri); |
|
279 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID)); |
|
280 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, |
|
281 new String[] { Long.toString(ContentUris.parseId(uri)) }); |
|
282 // fall through |
|
283 case CLIENTS: |
|
284 trace("Update on CLIENTS: " + uri); |
|
285 updated = updateValues(uri, values, selection, selectionArgs, TABLE_CLIENTS); |
|
286 break; |
|
287 |
|
288 case TABS_ID: |
|
289 trace("Update on TABS_ID: " + uri); |
|
290 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); |
|
291 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, |
|
292 new String[] { Long.toString(ContentUris.parseId(uri)) }); |
|
293 // fall through |
|
294 case TABS: |
|
295 trace("Update on TABS: " + uri); |
|
296 updated = updateValues(uri, values, selection, selectionArgs, TABLE_TABS); |
|
297 break; |
|
298 |
|
299 default: |
|
300 throw new UnsupportedOperationException("Unknown update URI " + uri); |
|
301 } |
|
302 |
|
303 debug("Updated " + updated + " rows for URI: " + uri); |
|
304 |
|
305 return updated; |
|
306 } |
|
307 |
|
308 @Override |
|
309 @SuppressWarnings("fallthrough") |
|
310 public Cursor query(Uri uri, String[] projection, String selection, |
|
311 String[] selectionArgs, String sortOrder) { |
|
312 SQLiteDatabase db = getReadableDatabase(uri); |
|
313 final int match = URI_MATCHER.match(uri); |
|
314 |
|
315 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); |
|
316 String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); |
|
317 |
|
318 switch (match) { |
|
319 case TABS_ID: |
|
320 trace("Query is on TABS_ID: " + uri); |
|
321 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_TABS, Tabs._ID)); |
|
322 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, |
|
323 new String[] { Long.toString(ContentUris.parseId(uri)) }); |
|
324 // fall through |
|
325 case TABS: |
|
326 trace("Query is on TABS: " + uri); |
|
327 if (TextUtils.isEmpty(sortOrder)) { |
|
328 sortOrder = DEFAULT_TABS_SORT_ORDER; |
|
329 } else { |
|
330 debug("Using sort order " + sortOrder + "."); |
|
331 } |
|
332 |
|
333 qb.setProjectionMap(TABS_PROJECTION_MAP); |
|
334 qb.setTables(TABLE_TABS + " LEFT OUTER JOIN " + TABLE_CLIENTS + " ON (" + TABLE_TABS + "." + Tabs.CLIENT_GUID + " = " + TABLE_CLIENTS + "." + Clients.GUID + ")"); |
|
335 break; |
|
336 |
|
337 case CLIENTS_ID: |
|
338 trace("Query is on CLIENTS_ID: " + uri); |
|
339 selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID)); |
|
340 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, |
|
341 new String[] { Long.toString(ContentUris.parseId(uri)) }); |
|
342 // fall through |
|
343 case CLIENTS: |
|
344 trace("Query is on CLIENTS: " + uri); |
|
345 if (TextUtils.isEmpty(sortOrder)) { |
|
346 sortOrder = DEFAULT_CLIENTS_SORT_ORDER; |
|
347 } else { |
|
348 debug("Using sort order " + sortOrder + "."); |
|
349 } |
|
350 |
|
351 qb.setProjectionMap(CLIENTS_PROJECTION_MAP); |
|
352 qb.setTables(TABLE_CLIENTS); |
|
353 break; |
|
354 |
|
355 default: |
|
356 throw new UnsupportedOperationException("Unknown query URI " + uri); |
|
357 } |
|
358 |
|
359 trace("Running built query."); |
|
360 final Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit); |
|
361 cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI); |
|
362 |
|
363 return cursor; |
|
364 } |
|
365 |
|
366 int updateValues(Uri uri, ContentValues values, String selection, String[] selectionArgs, String table) { |
|
367 trace("Updating tabs on URI: " + uri); |
|
368 |
|
369 final SQLiteDatabase db = getWritableDatabase(uri); |
|
370 beginWrite(db); |
|
371 return db.update(table, values, selection, selectionArgs); |
|
372 } |
|
373 |
|
374 int deleteValues(Uri uri, String selection, String[] selectionArgs, String table) { |
|
375 debug("Deleting tabs for URI: " + uri); |
|
376 |
|
377 final SQLiteDatabase db = getWritableDatabase(uri); |
|
378 beginWrite(db); |
|
379 return db.delete(table, selection, selectionArgs); |
|
380 } |
|
381 |
|
382 @Override |
|
383 protected TabsDatabaseHelper createDatabaseHelper(Context context, String databasePath) { |
|
384 return new TabsDatabaseHelper(context, databasePath); |
|
385 } |
|
386 |
|
387 @Override |
|
388 protected String getDatabaseName() { |
|
389 return DATABASE_NAME; |
|
390 } |
|
391 } |