/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.sync.repositories.android; import org.json.simple.JSONArray; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.sync.Logger; import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.domain.ClientRecord; import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; import android.content.ContentProviderClient; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.RemoteException; public class RepoUtils { private static final String LOG_TAG = "RepoUtils"; /** * A helper class for monotonous SQL querying. Does timing and logging, * offers a utility to throw on a null cursor. * * @author rnewman * */ public static class QueryHelper { private final Context context; private final Uri uri; private final String tag; public QueryHelper(Context context, Uri uri, String tag) { this.context = context; this.uri = uri; this.tag = tag; } // For ContentProvider queries. public Cursor safeQuery(String label, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws NullCursorException { long queryStart = android.os.SystemClock.uptimeMillis(); Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder); return checkAndLogCursor(label, queryStart, c); } public Cursor safeQuery(String[] projection, String selection, String[] selectionArgs, String sortOrder) throws NullCursorException { return this.safeQuery(null, projection, selection, selectionArgs, sortOrder); } // For ContentProviderClient queries. public Cursor safeQuery(ContentProviderClient client, String label, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws NullCursorException, RemoteException { long queryStart = android.os.SystemClock.uptimeMillis(); Cursor c = client.query(uri, projection, selection, selectionArgs, sortOrder); return checkAndLogCursor(label, queryStart, c); } // For SQLiteOpenHelper queries. public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) throws NullCursorException { long queryStart = android.os.SystemClock.uptimeMillis(); Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); return checkAndLogCursor(label, queryStart, c); } public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns, String selection, String[] selectionArgs) throws NullCursorException { return safeQuery(db, label, table, columns, selection, selectionArgs, null, null, null, null); } private Cursor checkAndLogCursor(String label, long queryStart, Cursor c) throws NullCursorException { long queryEnd = android.os.SystemClock.uptimeMillis(); String logLabel = (label == null) ? tag : (tag + label); RepoUtils.queryTimeLogger(logLabel, queryStart, queryEnd); return checkNullCursor(logLabel, c); } public Cursor checkNullCursor(String logLabel, Cursor cursor) throws NullCursorException { if (cursor == null) { Logger.error(tag, "Got null cursor exception in " + logLabel); throw new NullCursorException(null); } return cursor; } } public static String getStringFromCursor(Cursor cur, String colId) { // TODO: getColumnIndexOrThrow? // TODO: don't look up columns by name! return cur.getString(cur.getColumnIndex(colId)); } public static long getLongFromCursor(Cursor cur, String colId) { return cur.getLong(cur.getColumnIndex(colId)); } public static int getIntFromCursor(Cursor cur, String colId) { return cur.getInt(cur.getColumnIndex(colId)); } public static JSONArray getJSONArrayFromCursor(Cursor cur, String colId) { String jsonArrayAsString = getStringFromCursor(cur, colId); if (jsonArrayAsString == null) { return new JSONArray(); } try { return (JSONArray) new JSONParser().parse(getStringFromCursor(cur, colId)); } catch (ParseException e) { Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); return null; } } /** * Return true if the provided URI is non-empty and acceptable to Fennec * (i.e., not an undesirable scheme). * * This code is pilfered from Fennec, which pilfered from Places. */ public static boolean isValidHistoryURI(String uri) { if (uri == null || uri.length() == 0) { return false; } // First, check the most common cases (HTTP, HTTPS) to avoid most of the work. if (uri.startsWith("http:") || uri.startsWith("https:")) { return true; } String scheme = Uri.parse(uri).getScheme(); if (scheme == null) { return false; } // Now check for all bad things. if (scheme.equals("about") || scheme.equals("imap") || scheme.equals("news") || scheme.equals("mailbox") || scheme.equals("moz-anno") || scheme.equals("view-source") || scheme.equals("chrome") || scheme.equals("resource") || scheme.equals("data") || scheme.equals("wyciwyg") || scheme.equals("javascript")) { return false; } return true; } /** * Create a HistoryRecord object from a cursor row. * * @return a HistoryRecord, or null if this row would produce * an invalid record (e.g., with a null URI or no visits). */ public static HistoryRecord historyFromMirrorCursor(Cursor cur) { final String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); if (guid == null) { Logger.debug(LOG_TAG, "Skipping history record with null GUID."); return null; } final String historyURI = getStringFromCursor(cur, BrowserContract.History.URL); if (!isValidHistoryURI(historyURI)) { Logger.debug(LOG_TAG, "Skipping history record " + guid + " with unwanted/invalid URI " + historyURI); return null; } final long visitCount = getLongFromCursor(cur, BrowserContract.History.VISITS); if (visitCount <= 0) { Logger.debug(LOG_TAG, "Skipping history record " + guid + " with <= 0 visit count."); return null; } final String collection = "history"; final long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED); final boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1 ? true : false; final HistoryRecord rec = new HistoryRecord(guid, collection, lastModified, deleted); rec.androidID = getLongFromCursor(cur, BrowserContract.History._ID); rec.fennecDateVisited = getLongFromCursor(cur, BrowserContract.History.DATE_LAST_VISITED); rec.fennecVisitCount = visitCount; rec.histURI = historyURI; rec.title = getStringFromCursor(cur, BrowserContract.History.TITLE); return logHistory(rec); } private static HistoryRecord logHistory(HistoryRecord rec) { try { Logger.debug(LOG_TAG, "Returning history record " + rec.guid + " (" + rec.androidID + ")"); Logger.debug(LOG_TAG, "> Visited: " + rec.fennecDateVisited); Logger.debug(LOG_TAG, "> Visits: " + rec.fennecVisitCount); if (Logger.LOG_PERSONAL_INFORMATION) { Logger.pii(LOG_TAG, "> Title: " + rec.title); Logger.pii(LOG_TAG, "> URI: " + rec.histURI); } } catch (Exception e) { Logger.debug(LOG_TAG, "Exception logging history record " + rec, e); } return rec; } public static void logClient(ClientRecord rec) { if (Logger.shouldLogVerbose(LOG_TAG)) { Logger.trace(LOG_TAG, "Returning client record " + rec.guid + " (" + rec.androidID + ")"); Logger.trace(LOG_TAG, "Client Name: " + rec.name); Logger.trace(LOG_TAG, "Client Type: " + rec.type); Logger.trace(LOG_TAG, "Last Modified: " + rec.lastModified); Logger.trace(LOG_TAG, "Deleted: " + rec.deleted); } } public static void queryTimeLogger(String methodCallingQuery, long queryStart, long queryEnd) { long elapsedTime = queryEnd - queryStart; Logger.debug(LOG_TAG, "Query timer: " + methodCallingQuery + " took " + elapsedTime + "ms."); } public static boolean stringsEqual(String a, String b) { // Check for nulls if (a == b) return true; if (a == null && b != null) return false; if (a != null && b == null) return false; return a.equals(b); } private static String fixedWidth(int width, String s) { if (s == null) { return spaces(width); } int length = s.length(); if (width == length) { return s; } if (width > length) { return s + spaces(width - length); } return s.substring(0, width); } private static String spaces(int i) { return " ".substring(0, i); } private static String dashes(int i) { return "-------------------------------------".substring(0, i); } public static void dumpCursor(Cursor cur) { dumpCursor(cur, 18, "records"); } public static void dumpCursor(Cursor cur, int columnWidth, String tag) { int originalPosition = cur.getPosition(); try { String[] columnNames = cur.getColumnNames(); int columnCount = cur.getColumnCount(); for (int i = 0; i < columnCount; ++i) { System.out.print(fixedWidth(columnWidth, columnNames[i]) + " | "); } System.out.println("(" + cur.getCount() + " " + tag + ")"); for (int i = 0; i < columnCount; ++i) { System.out.print(dashes(columnWidth) + " | "); } System.out.println(""); if (!cur.moveToFirst()) { System.out.println("EMPTY"); return; } cur.moveToFirst(); while (!cur.isAfterLast()) { for (int i = 0; i < columnCount; ++i) { System.out.print(fixedWidth(columnWidth, cur.getString(i)) + " | "); } System.out.println(""); cur.moveToNext(); } for (int i = 0; i < columnCount-1; ++i) { System.out.print(dashes(columnWidth + 3)); } System.out.print(dashes(columnWidth + 3 - 1)); System.out.println(""); } finally { cur.moveToPosition(originalPosition); } } public static String computeSQLInClause(int items, String field) { StringBuilder builder = new StringBuilder(field); builder.append(" IN ("); int i = 0; for (; i < items - 1; ++i) { builder.append("?, "); } if (i < items) { builder.append("?"); } builder.append(")"); return builder.toString(); } }