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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package org.mozilla.gecko.sync.repositories.android; 6 7 import android.content.ContentProviderClient; 8 import android.content.Context; 9 import android.database.Cursor; 10 import android.database.sqlite.SQLiteDatabase; 11 import android.net.Uri; 12 import android.os.RemoteException; 13 14 import org.json.simple.JSONArray; 15 import org.mozilla.gecko.background.common.log.Logger; 16 import org.mozilla.gecko.db.BrowserContract; 17 import org.mozilla.gecko.sync.ExtendedJSONObject; 18 import org.mozilla.gecko.sync.NonArrayJSONException; 19 import org.mozilla.gecko.sync.repositories.NullCursorException; 20 import org.mozilla.gecko.sync.repositories.domain.ClientRecord; 21 import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; 22 23 import java.io.IOException; 24 25 public class RepoUtils { 26 27 private static final String LOG_TAG = "RepoUtils"; 28 29 /** 30 * A helper class for monotonous SQL querying. Does timing and logging, 31 * offers a utility to throw on a null cursor. 32 * 33 * @author rnewman 34 * 35 */ 36 public static class QueryHelper { 37 private final Context context; 38 private final Uri uri; 39 private final String tag; 40 QueryHelper(Context context, Uri uri, String tag)41 public QueryHelper(Context context, Uri uri, String tag) { 42 this.context = context; 43 this.uri = uri; 44 this.tag = tag; 45 } 46 47 // For ContentProvider queries. safeQuery(String label, String[] projection, String selection, String[] selectionArgs, String sortOrder)48 public Cursor safeQuery(String label, String[] projection, 49 String selection, String[] selectionArgs, String sortOrder) throws NullCursorException { 50 long queryStart = android.os.SystemClock.uptimeMillis(); 51 Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder); 52 return checkAndLogCursor(label, queryStart, c); 53 } 54 safeQuery(String[] projection, String selection, String[] selectionArgs, String sortOrder)55 public Cursor safeQuery(String[] projection, String selection, String[] selectionArgs, String sortOrder) throws NullCursorException { 56 return this.safeQuery(null, projection, selection, selectionArgs, sortOrder); 57 } 58 59 // For ContentProviderClient queries. safeQuery(ContentProviderClient client, String label, String[] projection, String selection, String[] selectionArgs, String sortOrder)60 public Cursor safeQuery(ContentProviderClient client, String label, String[] projection, 61 String selection, String[] selectionArgs, String sortOrder) throws NullCursorException, RemoteException { 62 long queryStart = android.os.SystemClock.uptimeMillis(); 63 Cursor c = client.query(uri, projection, selection, selectionArgs, sortOrder); 64 return checkAndLogCursor(label, queryStart, c); 65 } 66 67 // For SQLiteOpenHelper queries. safeQuery(SQLiteDatabase db, String label, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)68 public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns, 69 String selection, String[] selectionArgs, 70 String groupBy, String having, String orderBy, String limit) throws NullCursorException { 71 long queryStart = android.os.SystemClock.uptimeMillis(); 72 Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); 73 return checkAndLogCursor(label, queryStart, c); 74 } 75 safeQuery(SQLiteDatabase db, String label, String table, String[] columns, String selection, String[] selectionArgs)76 public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns, 77 String selection, String[] selectionArgs) throws NullCursorException { 78 return safeQuery(db, label, table, columns, selection, selectionArgs, null, null, null, null); 79 } 80 checkAndLogCursor(String label, long queryStart, Cursor c)81 private Cursor checkAndLogCursor(String label, long queryStart, Cursor c) throws NullCursorException { 82 long queryEnd = android.os.SystemClock.uptimeMillis(); 83 String logLabel = (label == null) ? tag : (tag + label); 84 RepoUtils.queryTimeLogger(logLabel, queryStart, queryEnd); 85 return checkNullCursor(logLabel, c); 86 } 87 checkNullCursor(String logLabel, Cursor cursor)88 public Cursor checkNullCursor(String logLabel, Cursor cursor) throws NullCursorException { 89 if (cursor == null) { 90 Logger.error(tag, "Got null cursor exception in " + logLabel); 91 throw new NullCursorException(null); 92 } 93 return cursor; 94 } 95 } 96 97 /** 98 * This method exists because the behavior of <code>cur.getString()</code> is undefined 99 * when the value in the database is <code>NULL</code>. 100 * This method will return <code>null</code> in that case. 101 */ optStringFromCursor(final Cursor cur, final String colId)102 public static String optStringFromCursor(final Cursor cur, final String colId) { 103 final int col = cur.getColumnIndex(colId); 104 if (cur.isNull(col)) { 105 return null; 106 } 107 return cur.getString(col); 108 } 109 110 /** 111 * The behavior of this method when the value in the database is <code>NULL</code> is 112 * determined by the implementation of the {@link Cursor}. 113 */ getStringFromCursor(final Cursor cur, final String colId)114 public static String getStringFromCursor(final Cursor cur, final String colId) { 115 // TODO: getColumnIndexOrThrow? 116 // TODO: don't look up columns by name! 117 return cur.getString(cur.getColumnIndex(colId)); 118 } 119 getLongFromCursor(Cursor cur, String colId)120 public static long getLongFromCursor(Cursor cur, String colId) { 121 return cur.getLong(cur.getColumnIndex(colId)); 122 } 123 getIntFromCursor(Cursor cur, String colId)124 public static int getIntFromCursor(Cursor cur, String colId) { 125 return cur.getInt(cur.getColumnIndex(colId)); 126 } 127 getJSONArrayFromCursor(Cursor cur, String colId)128 public static JSONArray getJSONArrayFromCursor(Cursor cur, String colId) { 129 String jsonArrayAsString = getStringFromCursor(cur, colId); 130 if (jsonArrayAsString == null) { 131 return new JSONArray(); 132 } 133 try { 134 return ExtendedJSONObject.parseJSONArray(getStringFromCursor(cur, colId)); 135 } catch (NonArrayJSONException e) { 136 Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); 137 return null; 138 } catch (IOException e) { 139 Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); 140 return null; 141 } 142 } 143 144 /** 145 * Return true if the provided URI is non-empty and acceptable to Fennec 146 * (i.e., not an undesirable scheme). 147 * 148 * This code is pilfered from Fennec, which pilfered from Places. 149 */ isValidHistoryURI(String uri)150 public static boolean isValidHistoryURI(String uri) { 151 if (uri == null || uri.length() == 0) { 152 return false; 153 } 154 155 // First, check the most common cases (HTTP, HTTPS) to avoid most of the work. 156 if (uri.startsWith("http:") || uri.startsWith("https:")) { 157 return true; 158 } 159 160 String scheme = Uri.parse(uri).getScheme(); 161 if (scheme == null) { 162 return false; 163 } 164 165 // Now check for all bad things. 166 if (scheme.equals("about") || 167 scheme.equals("imap") || 168 scheme.equals("news") || 169 scheme.equals("mailbox") || 170 scheme.equals("moz-anno") || 171 scheme.equals("view-source") || 172 scheme.equals("chrome") || 173 scheme.equals("resource") || 174 scheme.equals("data") || 175 scheme.equals("wyciwyg") || 176 scheme.equals("javascript")) { 177 return false; 178 } 179 180 return true; 181 } 182 183 /** 184 * Create a HistoryRecord object from a cursor row. 185 * 186 * @return a HistoryRecord, or null if this row would produce 187 * an invalid record (e.g., with a null URI or no visits). 188 */ historyFromMirrorCursor(Cursor cur)189 public static HistoryRecord historyFromMirrorCursor(Cursor cur) { 190 final String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); 191 if (guid == null) { 192 Logger.debug(LOG_TAG, "Skipping history record with null GUID."); 193 return null; 194 } 195 196 final String historyURI = getStringFromCursor(cur, BrowserContract.History.URL); 197 if (!isValidHistoryURI(historyURI)) { 198 Logger.debug(LOG_TAG, "Skipping history record " + guid + " with unwanted/invalid URI " + historyURI); 199 return null; 200 } 201 202 final long visitCount = getLongFromCursor(cur, BrowserContract.History.VISITS); 203 if (visitCount <= 0) { 204 Logger.debug(LOG_TAG, "Skipping history record " + guid + " with <= 0 visit count."); 205 return null; 206 } 207 208 final String collection = "history"; 209 final long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED); 210 final boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1; 211 212 final HistoryRecord rec = new HistoryRecord(guid, collection, lastModified, deleted); 213 214 rec.androidID = getLongFromCursor(cur, BrowserContract.History._ID); 215 rec.fennecDateVisited = getLongFromCursor(cur, BrowserContract.History.DATE_LAST_VISITED); 216 rec.fennecVisitCount = visitCount; 217 rec.histURI = historyURI; 218 rec.title = getStringFromCursor(cur, BrowserContract.History.TITLE); 219 220 return logHistory(rec); 221 } 222 logHistory(HistoryRecord rec)223 private static HistoryRecord logHistory(HistoryRecord rec) { 224 try { 225 Logger.debug(LOG_TAG, "Returning history record " + rec.guid + " (" + rec.androidID + ")"); 226 Logger.debug(LOG_TAG, "> Visited: " + rec.fennecDateVisited); 227 Logger.debug(LOG_TAG, "> Visits: " + rec.fennecVisitCount); 228 if (Logger.LOG_PERSONAL_INFORMATION) { 229 Logger.pii(LOG_TAG, "> Title: " + rec.title); 230 Logger.pii(LOG_TAG, "> URI: " + rec.histURI); 231 } 232 } catch (Exception e) { 233 Logger.debug(LOG_TAG, "Exception logging history record " + rec, e); 234 } 235 return rec; 236 } 237 logClient(ClientRecord rec)238 public static void logClient(ClientRecord rec) { 239 if (Logger.shouldLogVerbose(LOG_TAG)) { 240 Logger.trace(LOG_TAG, "Returning client record " + rec.guid + " (" + rec.androidID + ")"); 241 Logger.trace(LOG_TAG, "Client Name: " + rec.name); 242 Logger.trace(LOG_TAG, "Client Type: " + rec.type); 243 Logger.trace(LOG_TAG, "Last Modified: " + rec.lastModified); 244 Logger.trace(LOG_TAG, "Deleted: " + rec.deleted); 245 } 246 } 247 queryTimeLogger(String methodCallingQuery, long queryStart, long queryEnd)248 public static void queryTimeLogger(String methodCallingQuery, long queryStart, long queryEnd) { 249 long elapsedTime = queryEnd - queryStart; 250 Logger.debug(LOG_TAG, "Query timer: " + methodCallingQuery + " took " + elapsedTime + "ms."); 251 } 252 stringsEqual(String a, String b)253 public static boolean stringsEqual(String a, String b) { 254 // Check for nulls 255 if (a == b) return true; 256 if (a == null && b != null) return false; 257 if (a != null && b == null) return false; 258 259 return a.equals(b); 260 } 261 computeSQLLongInClause(long[] items, String field)262 public static String computeSQLLongInClause(long[] items, String field) { 263 final StringBuilder builder = new StringBuilder(field); 264 builder.append(" IN ("); 265 int i = 0; 266 for (; i < items.length - 1; ++i) { 267 builder.append(items[i]); 268 builder.append(", "); 269 } 270 if (i < items.length) { 271 builder.append(items[i]); 272 } 273 builder.append(")"); 274 return builder.toString(); 275 } 276 computeSQLInClause(int items, String field)277 public static String computeSQLInClause(int items, String field) { 278 final StringBuilder builder = new StringBuilder(field); 279 builder.append(" IN ("); 280 int i = 0; 281 for (; i < items - 1; ++i) { 282 builder.append("?, "); 283 } 284 if (i < items) { 285 builder.append("?"); 286 } 287 builder.append(")"); 288 return builder.toString(); 289 } 290 } 291