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