author | Wes Johnston <wjohnston@mozilla.com> |
Fri, 16 Dec 2011 15:11:09 -0800 | |
changeset 89414 | 85346ae0ea9942f1ac9cff829f2f588fd8deff19 |
parent 89413 | 8fa903fa888740143d0537fb51abca5061c6e01e |
child 89415 | aca408a1d17b2f35dddf6d4ab25398b78d749d70 |
push id | 783 |
push user | lsblakk@mozilla.com |
push date | Tue, 24 Apr 2012 17:33:42 +0000 |
treeherder | mozilla-beta@11faed19f136 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | blassey, gpascutto |
bugs | 704682 |
milestone | 13.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -158,15 +158,21 @@ android:theme="@style/Gecko.TitleBar" android:label="@string/settings_title" android:excludeFromRecents="true"/> <provider android:name="@ANDROID_PACKAGE_NAME@.db.BrowserProvider" android:authorities="@ANDROID_PACKAGE_NAME@.db.browser" android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/> + <provider android:name="@ANDROID_PACKAGE_NAME@.db.PasswordsProvider" + android:authorities="@ANDROID_PACKAGE_NAME@.db.passwords" + android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER" + android:protectionLevel="signature"/> + + #include ../sync/manifests/SyncAndroidManifest_services.xml.in </application> <permission android:name="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER" android:protectionLevel="signature"/> </manifest>
--- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -67,16 +67,17 @@ FENNEC_JAVA_FILES = \ AwesomeBar.java \ AwesomeBarTabs.java \ BrowserToolbar.java \ ConfirmPreference.java \ SyncPreference.java \ db/AndroidBrowserDB.java \ db/BrowserDB.java \ db/LocalBrowserDB.java \ + db/DBUtils.java \ DoorHanger.java \ DoorHangerPopup.java \ Favicons.java \ FloatUtils.java \ GeckoActionBar.java \ GeckoApp.java \ GeckoAppShell.java \ GeckoAsyncTask.java \ @@ -144,16 +145,17 @@ endif FENNEC_PP_JAVA_FILES = \ App.java \ LauncherShortcuts.java \ NotificationHandler.java \ Restarter.java \ db/BrowserContract.java \ db/BrowserProvider.java \ + db/PasswordsProvider.java \ SmsManager.java \ $(NULL) ifneq (,$(findstring -march=armv7,$(OS_CFLAGS))) MIN_CPU_VERSION=7 else MIN_CPU_VERSION=5
--- a/mobile/android/base/db/BrowserContract.java.in +++ b/mobile/android/base/db/BrowserContract.java.in @@ -38,17 +38,25 @@ #filter substitution package org.mozilla.gecko.db; import android.net.Uri; public class BrowserContract { public static final String AUTHORITY = "@ANDROID_PACKAGE_NAME@.db.browser"; + public static final String PASSWORDS_AUTHORITY = "@ANDROID_PACKAGE_NAME@.db.passwords"; + + public static final String DELETED_PASSWORDS_AUTHORITY = "@ANDROID_PACKAGE_NAME@.db.deleted-passwords"; + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + public static final Uri PASSWORDS_AUTHORITY_URI = Uri.parse("content://" + PASSWORDS_AUTHORITY); + + public static final Uri DELETED_PASSWORDS_AUTHORITY_URI = Uri.parse("content://" + DELETED_PASSWORDS_AUTHORITY); public static final String DEFAULT_PROFILE = "default"; public static final String PARAM_PROFILE = "profile"; public static final String PARAM_LIMIT = "limit"; public static final String PARAM_IS_SYNC = "sync"; @@ -136,9 +144,57 @@ public class BrowserContract { public static final class Schema { private Schema() {} public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "schema"); public static final String VERSION = "version"; } + + public static final class Passwords implements CommonColumns, SyncColumns { + private Passwords() {} + + public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "passwords"); + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/passwords"; + + public static final String ID = "id"; + + public static final String HOSTNAME = "hostname"; + + public static final String HTTP_REALM = "httpRealm"; + + public static final String FORM_SUBMIT_URL = "formSubmitURL"; + + public static final String USERNAME_FIELD = "usernameField"; + + public static final String PASSWORD_FIELD = "passwordField"; + + public static final String ENCRYPTED_USERNAME = "encryptedUsername"; + + public static final String ENCRYPTED_PASSWORD = "encryptedPassword"; + + public static final String ENC_TYPE = "encType"; + + public static final String TIME_CREATED = "timeCreated"; + + public static final String TIME_LAST_USED = "timeLastUsed"; + + public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged"; + + public static final String TIMES_USED = "timesUsed"; + } + + public static final class DeletedPasswords implements CommonColumns, SyncColumns { + private DeletedPasswords() {} + + public static final Uri CONTENT_URI = Uri.withAppendedPath(DELETED_PASSWORDS_AUTHORITY_URI, "deleted-passwords"); + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-passwords"; + + public static final String ID = "id"; + + public static final String GUID = "guid"; + + public static final String TIME_DELETED = "timeDeleted"; + } }
--- a/mobile/android/base/db/BrowserProvider.java.in +++ b/mobile/android/base/db/BrowserProvider.java.in @@ -51,16 +51,18 @@ import org.mozilla.gecko.R; import org.mozilla.gecko.db.BrowserContract.Bookmarks; import org.mozilla.gecko.db.BrowserContract.CommonColumns; import org.mozilla.gecko.db.BrowserContract.History; import org.mozilla.gecko.db.BrowserContract.Images; import org.mozilla.gecko.db.BrowserContract.Schema; import org.mozilla.gecko.db.BrowserContract.SyncColumns; import org.mozilla.gecko.db.BrowserContract.URLColumns; import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.DBUtils; +import org.mozilla.gecko.sync.Utils; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; @@ -203,62 +205,16 @@ public class BrowserProvider extends Con } private HashMap<String, DatabaseHelper> mDatabasePerProfile; static final String qualifyColumn(String table, String column) { return table + "." + column; } - public static String generateGuid() { - byte[] encodedBytes = GeckoAppShell.encodeBase64(generateRandomBytes(9), GeckoAppShell.BASE64_URL_SAFE); - return new String(encodedBytes); - } - - private static byte[] generateRandomBytes(int length) { - byte[] bytes = new byte[length]; - - Random random = new Random(System.nanoTime()); - random.nextBytes(bytes); - - return bytes; - } - - // This is available in Android >= 11. Implemented locally to be - // compatible with older versions. - public static String concatenateWhere(String a, String b) { - if (TextUtils.isEmpty(a)) { - return b; - } - - if (TextUtils.isEmpty(b)) { - return a; - } - - return "(" + a + ") AND (" + b + ")"; - } - - // This is available in Android >= 11. Implemented locally to be - // compatible with older versions. - public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { - if (originalValues == null || originalValues.length == 0) { - return newValues; - } - - if (newValues == null || newValues.length == 0) { - return originalValues; - } - - String[] result = new String[originalValues.length + newValues.length]; - System.arraycopy(originalValues, 0, result, 0, originalValues.length); - System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); - - return result; - } - private static boolean hasImagesInProjection(String[] projection) { if (projection == null) return true; for (int i = 0; i < projection.length; ++i) { if (projection[i].equals(Images.FAVICON) || projection[i].equals(Images.THUMBNAIL)) return true; } @@ -614,46 +570,46 @@ public class BrowserProvider extends Con final int match = URI_MATCHER.match(uri); int deleted = 0; switch (match) { case BOOKMARKS_ID: Log.d(LOGTAG, "Delete on BOOKMARKS_ID: " + uri); - selection = concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); // fall through case BOOKMARKS: { Log.d(LOGTAG, "Deleting bookmarks: " + uri); deleted = deleteBookmarks(uri, selection, selectionArgs); deleteUnusedImages(uri); break; } case HISTORY_ID: Log.d(LOGTAG, "Delete on HISTORY_ID: " + uri); - selection = concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); // fall through case HISTORY: { Log.d(LOGTAG, "Deleting history: " + uri); deleted = deleteHistory(uri, selection, selectionArgs); deleteUnusedImages(uri); break; } case IMAGES_ID: Log.d(LOGTAG, "Delete on IMAGES_ID: " + uri); - selection = concatenateWhere(selection, TABLE_IMAGES + "._id = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, TABLE_IMAGES + "._id = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); // fall through case IMAGES: { Log.d(LOGTAG, "Deleting images: " + uri); deleted = deleteImages(uri, selection, selectionArgs); break; } @@ -702,17 +658,17 @@ public class BrowserProvider extends Con Log.d(LOGTAG, "Insert on BOOKMARKS: " + uri); long now = System.currentTimeMillis(); values.put(Bookmarks.DATE_CREATED, now); values.put(Bookmarks.DATE_MODIFIED, now); // Generate GUID for new bookmark. Don't override specified GUIDs. if (!values.containsKey(Bookmarks.GUID)) { - values.put(Bookmarks.GUID, generateGuid()); + values.put(Bookmarks.GUID, Utils.generateGuid()); } if (!values.containsKey(Bookmarks.POSITION)) { Log.d(LOGTAG, "Inserting bookmark with no position for URI"); values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE)); } String url = values.getAsString(Bookmarks.URL); @@ -735,17 +691,17 @@ public class BrowserProvider extends Con Log.d(LOGTAG, "Insert on HISTORY: " + uri); long now = System.currentTimeMillis(); values.put(History.DATE_CREATED, now); values.put(History.DATE_MODIFIED, now); // Generate GUID for new history entry. Don't override specified GUIDs. if (!values.containsKey(History.GUID)) { - values.put(History.GUID, generateGuid()); + values.put(History.GUID, Utils.generateGuid()); } String url = values.getAsString(History.URL); ContentValues imageValues = extractImageValues(values, values.getAsString(History.URL)); if (imageValues != null) { @@ -762,17 +718,17 @@ public class BrowserProvider extends Con case IMAGES: { Log.d(LOGTAG, "Insert on IMAGES: " + uri); long now = System.currentTimeMillis(); values.put(History.DATE_CREATED, now); values.put(History.DATE_MODIFIED, now); // Generate GUID for new history entry - values.put(History.GUID, generateGuid()); + values.put(History.GUID, Utils.generateGuid()); String url = values.getAsString(Images.URL); Log.d(LOGTAG, "Inserting image in database with URL: " + url); id = db.insertOrThrow(TABLE_IMAGES, Images.URL, values); break; } @@ -820,31 +776,31 @@ public class BrowserProvider extends Con int match = URI_MATCHER.match(uri); int updated = 0; switch (match) { case BOOKMARKS_ID: Log.d(LOGTAG, "Update on BOOKMARKS_ID: " + uri); - selection = concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); // fall through case BOOKMARKS: { Log.d(LOGTAG, "Updating bookmark: " + uri); updated = updateBookmarks(uri, values, selection, selectionArgs); break; } case HISTORY_ID: Log.d(LOGTAG, "Update on HISTORY_ID: " + uri); - selection = concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); // fall through case HISTORY: { Log.d(LOGTAG, "Updating history: " + uri); updated = updateHistory(uri, values, selection, selectionArgs); break; } @@ -882,27 +838,27 @@ public class BrowserProvider extends Con switch (match) { case BOOKMARKS_FOLDER_ID: case BOOKMARKS_ID: case BOOKMARKS: { Log.d(LOGTAG, "Query is on bookmarks: " + uri); if (match == BOOKMARKS_ID) { - selection = concatenateWhere(selection, Bookmarks._ID + " = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); } else if (match == BOOKMARKS_FOLDER_ID) { - selection = concatenateWhere(selection, Bookmarks.PARENT + " = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); } if (!shouldShowDeleted(uri)) - selection = concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection); + selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection); if (TextUtils.isEmpty(sortOrder)) { sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; } else { Log.d(LOGTAG, "Using sort order " + sortOrder + "."); } qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); @@ -915,23 +871,23 @@ public class BrowserProvider extends Con break; } case HISTORY_ID: case HISTORY: { Log.d(LOGTAG, "Query is on history: " + uri); if (match == HISTORY_ID) { - selection = concatenateWhere(selection, History._ID + " = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, History._ID + " = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); } if (!shouldShowDeleted(uri)) - selection = concatenateWhere(History.IS_DELETED + " = 0", selection); + selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection); if (TextUtils.isEmpty(sortOrder)) sortOrder = DEFAULT_HISTORY_SORT_ORDER; qb.setProjectionMap(HISTORY_PROJECTION_MAP); if (hasImagesInProjection(projection)) qb.setTables(VIEW_HISTORY_WITH_IMAGES); @@ -941,23 +897,23 @@ public class BrowserProvider extends Con break; } case IMAGES_ID: case IMAGES: { Log.d(LOGTAG, "Query is on images: " + uri); if (match == IMAGES_ID) { - selection = concatenateWhere(selection, Images._ID + " = ?"); - selectionArgs = appendSelectionArgs(selectionArgs, + selection = DBUtils.concatenateWhere(selection, Images._ID + " = ?"); + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { Long.toString(ContentUris.parseId(uri)) }); } if (!shouldShowDeleted(uri)) - selection = concatenateWhere(Images.IS_DELETED + " = 0", selection); + selection = DBUtils.concatenateWhere(Images.IS_DELETED + " = 0", selection); qb.setProjectionMap(IMAGES_PROJECTION_MAP); qb.setTables(TABLE_IMAGES); break; } case SCHEMA: { @@ -1176,17 +1132,17 @@ public class BrowserProvider extends Con values.put(Images.IS_DELETED, 0); Log.d(LOGTAG, "Trying to update image for URL: " + url); int updated = db.update(TABLE_IMAGES, values, selection, selectionArgs); if (updated == 0 && insertIfNeeded) { // Generate GUID for new image, if one is not already provided. if (!values.containsKey(Images.GUID)) { - values.put(Images.GUID, generateGuid()); + values.put(Images.GUID, Utils.generateGuid()); } values.put(Images.DATE_CREATED, now); values.put(Images.DATE_MODIFIED, now); Log.d(LOGTAG, "No update, inserting image for URL: " + url); db.insert(TABLE_IMAGES, Images.FAVICON, values); updated = 1;
new file mode 100644 --- /dev/null +++ b/mobile/android/base/db/DBUtils.java @@ -0,0 +1,66 @@ +/* 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.db; + +import org.mozilla.gecko.sync.Utils; + +import android.content.ContentValues; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; +import java.util.UUID; + +import java.util.Random; + +public class DBUtils { + public static final String qualifyColumn(String table, String column) { + return table + "." + column; + } + + // This is available in Android >= 11. Implemented locally to be + // compatible with older versions. + public static String concatenateWhere(String a, String b) { + if (TextUtils.isEmpty(a)) { + return b; + } + + if (TextUtils.isEmpty(b)) { + return a; + } + + return "(" + a + ") AND (" + b + ")"; + } + + // This is available in Android >= 11. Implemented locally to be + // compatible with older versions. + public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { + if (originalValues == null || originalValues.length == 0) { + return newValues; + } + + if (newValues == null || newValues.length == 0) { + return originalValues; + } + + String[] result = new String[originalValues.length + newValues.length]; + System.arraycopy(originalValues, 0, result, 0, originalValues.length); + System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); + + return result; + } + + public static void replaceKey(ContentValues aValues, String aOriginalKey, + String aNewKey, String aDefault) { + String value = aDefault; + if (aOriginalKey != null && aValues.containsKey(aOriginalKey)) { + value = aValues.get(aOriginalKey).toString(); + aValues.remove(aOriginalKey); + } + + if (!aValues.containsKey(aOriginalKey)) { + aValues.put(aNewKey, value); + } + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/base/db/PasswordsProvider.java.in @@ -0,0 +1,412 @@ +/* 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/. */ + +#filter substitution +package @ANDROID_PACKAGE_NAME@.db; + +import java.io.File; +import java.io.IOException; +import java.lang.IllegalArgumentException; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Random; + +import org.mozilla.gecko.GeckoApp; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoDirProvider; +import org.mozilla.gecko.GeckoEvent; +import org.mozilla.gecko.GeckoEventListener; +import org.mozilla.gecko.db.BrowserContract.CommonColumns; +import org.mozilla.gecko.db.DBUtils; +import org.mozilla.gecko.db.BrowserContract.Passwords; +import org.mozilla.gecko.db.BrowserContract.DeletedPasswords; +import org.mozilla.gecko.db.BrowserContract.SyncColumns; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.sqlite.SQLiteBridge; +import org.mozilla.gecko.sqlite.SQLiteBridgeException; +import org.mozilla.gecko.sync.Utils; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +public class PasswordsProvider extends ContentProvider { + private Context mContext = null; + private static final String LOGTAG = "GeckoPasswordsProvider"; + + static final String DATABASE_NAME = "signons.sqlite"; + + static final int DATABASE_VERSION = 5; + + static final String TABLE_PASSWORDS = "moz_logins"; + static final String TABLE_DELETED_PASSWORDS = "deleted_logins"; + + private static final int PASSWORDS = 500; + private static final int DELETED_PASSWORDS = 502; + + static final String DEFAULT_PASSWORDS_SORT_ORDER = Passwords.HOSTNAME + " ASC"; + static final String DEFAULT_DELETED_PASSWORDS_SORT_ORDER = DeletedPasswords.TIME_DELETED + " ASC"; + + private static final UriMatcher URI_MATCHER; + + private static HashMap<String, String> PASSWORDS_PROJECTION_MAP; + private static HashMap<String, String> DELETED_PASSWORDS_PROJECTION_MAP; + + private HashMap<String, SQLiteBridge> mDatabasePerProfile; + + private static ArrayList<String> mSyncColumns; + + static { + URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + + // content://org.mozilla.gecko.providers.browser/passwords/# + URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "passwords", PASSWORDS); + + PASSWORDS_PROJECTION_MAP = new HashMap<String, String>(); + PASSWORDS_PROJECTION_MAP.put(Passwords.ID, Passwords.ID); + PASSWORDS_PROJECTION_MAP.put(Passwords.HOSTNAME, Passwords.HOSTNAME); + PASSWORDS_PROJECTION_MAP.put(Passwords.HTTP_REALM, Passwords.HTTP_REALM); + PASSWORDS_PROJECTION_MAP.put(Passwords.FORM_SUBMIT_URL, Passwords.FORM_SUBMIT_URL); + PASSWORDS_PROJECTION_MAP.put(Passwords.USERNAME_FIELD, Passwords.USERNAME_FIELD); + PASSWORDS_PROJECTION_MAP.put(Passwords.PASSWORD_FIELD, Passwords.PASSWORD_FIELD); + PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_USERNAME, Passwords.ENCRYPTED_USERNAME); + PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_PASSWORD, Passwords.ENCRYPTED_PASSWORD); + PASSWORDS_PROJECTION_MAP.put(Passwords.GUID, Passwords.GUID); + PASSWORDS_PROJECTION_MAP.put(Passwords.ENC_TYPE, Passwords.ENC_TYPE); + PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_CREATED, Passwords.TIME_CREATED); + PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_LAST_USED, Passwords.TIME_LAST_USED); + PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_PASSWORD_CHANGED, Passwords.TIME_PASSWORD_CHANGED); + PASSWORDS_PROJECTION_MAP.put(Passwords.TIMES_USED, Passwords.TIMES_USED); + + URI_MATCHER.addURI(BrowserContract.DELETED_PASSWORDS_AUTHORITY, "deleted-passwords", DELETED_PASSWORDS); + + mSyncColumns = new ArrayList<String>(); + mSyncColumns.add(Passwords._ID); + mSyncColumns.add(Passwords.DATE_CREATED); + mSyncColumns.add(Passwords.DATE_MODIFIED); + + DELETED_PASSWORDS_PROJECTION_MAP = new HashMap<String, String>(); + DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.ID, DeletedPasswords.ID); + DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.GUID, DeletedPasswords.GUID); + DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.TIME_DELETED, DeletedPasswords.TIME_DELETED); + } + + private SQLiteBridge getDB(Context context, final String databasePath) { + SQLiteBridge mBridge = null; + try { + mBridge = new SQLiteBridge(databasePath); + + boolean dbNeedsSetup = true; + try { + int version = mBridge.getVersion(); + Log.i(LOGTAG, version + " == " + DATABASE_VERSION); + dbNeedsSetup = version != DATABASE_VERSION; + } catch(Exception ex) { + Log.e(LOGTAG, "Error getting version ", ex); + // if Gecko is not running, we should bail out. Otherwise we try to + // let Gecko build the database for us + if (!GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning)) { + mBridge = null; + throw new UnsupportedOperationException("Need to launch Gecko to set password database up"); + } + } + if (dbNeedsSetup) { + Log.i(LOGTAG, "Sending init to gecko"); + mBridge = null; + GeckoAppShell.sendEventToGecko(new GeckoEvent("Passwords:Init", databasePath)); + } + } catch(SQLiteBridgeException ex) { + Log.e(LOGTAG, "Error getting database", ex); + } + return mBridge; + } + + private SQLiteBridge getDatabaseForProfile(String profile) { + // Each profile has a separate signons.sqlite database. The target + // profile is provided using a URI query argument in each request + // to our content provider. + + if (TextUtils.isEmpty(profile)) { + Log.d(LOGTAG, "No profile provided, using default"); + profile = BrowserContract.DEFAULT_PROFILE; + } + + SQLiteBridge db = mDatabasePerProfile.get(profile); + + if (db == null) { + synchronized (this) { + try { + db = getDB(getContext(), getDatabasePath(profile)); + } catch(UnsupportedOperationException ex) { + Log.i(LOGTAG, "Gecko has not set the database up yet"); + return db; + } + mDatabasePerProfile.put(profile, db); + } + } + + Log.d(LOGTAG, "Successfully created database helper for profile: " + profile); + + return db; + } + + private String getDatabasePath(String profile) { + File profileDir = null; + try { + profileDir = GeckoDirProvider.getProfileDir(mContext, profile); + } catch (IOException ex) { + Log.e(LOGTAG, "Error getting profile dir", ex); + } + + if (profileDir == null) { + Log.d(LOGTAG, "Couldn't find directory for profile: " + profile); + return null; + } + + String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath(); + return databasePath; + } + + private SQLiteBridge getDatabase(Uri uri) { + String profile = null; + + if (uri != null) + profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE); + + return getDatabaseForProfile(profile); + } + + @Override + public boolean onCreate() { + mContext = getContext(); + mDatabasePerProfile = new HashMap<String, SQLiteBridge>(); + + return true; + } + + @Override + public String getType(Uri uri) { + final int match = URI_MATCHER.match(uri); + + switch (match) { + case PASSWORDS: + return Passwords.CONTENT_TYPE; + case DELETED_PASSWORDS: + return DeletedPasswords.CONTENT_TYPE; + } + + Log.d(LOGTAG, "URI has unrecognized type: " + uri); + return null; + } + + private String getTable(Uri uri) { + final int match = URI_MATCHER.match(uri); + switch (match) { + case DELETED_PASSWORDS: + return TABLE_DELETED_PASSWORDS; + case PASSWORDS: { + return TABLE_PASSWORDS; + } + default: + throw new UnsupportedOperationException("Unknown table " + uri); + } + } + + private String getSortOrder(Uri uri, String aRequested) { + if (!TextUtils.isEmpty(aRequested)) + return aRequested; + + final int match = URI_MATCHER.match(uri); + switch (match) { + case DELETED_PASSWORDS: + return DEFAULT_DELETED_PASSWORDS_SORT_ORDER; + case PASSWORDS: { + return DEFAULT_PASSWORDS_SORT_ORDER; + } + default: + throw new UnsupportedOperationException("Unknown delete URI " + uri); + } + } + + private Uri getAuthUri(Uri uri) { + final int match = URI_MATCHER.match(uri); + switch (match) { + case DELETED_PASSWORDS: + return BrowserContract.DELETED_PASSWORDS_AUTHORITY_URI; + case PASSWORDS: { + return BrowserContract.PASSWORDS_AUTHORITY_URI; + } + default: + throw new UnsupportedOperationException("Unknown delete URI " + uri); + } + } + + private void setupDefaults(Uri uri, ContentValues values) + throws IllegalArgumentException { + int match = URI_MATCHER.match(uri); + long now = System.currentTimeMillis(); + switch (match) { + case DELETED_PASSWORDS: + values.put(DeletedPasswords.TIME_DELETED, now); + + // Deleted passwords must contain a guid + if (!values.containsKey(Passwords.GUID)) { + throw new IllegalArgumentException("Must provide a GUID for a deleted password"); + } + break; + case PASSWORDS: { + values.put(Passwords.TIME_CREATED, now); + + // Generate GUID for new password. Don't override specified GUIDs. + if (!values.containsKey(Passwords.GUID)) { + String guid = Utils.generateGuid(); + values.put(Passwords.GUID, guid); + } + String nowString = new Long(now).toString(); + DBUtils.replaceKey(values, CommonColumns._ID, Passwords.ID, ""); + DBUtils.replaceKey(values, SyncColumns.DATE_CREATED, Passwords.TIME_CREATED, nowString); + DBUtils.replaceKey(values, SyncColumns.DATE_MODIFIED, Passwords.TIME_PASSWORD_CHANGED, nowString); + DBUtils.replaceKey(values, null, Passwords.HOSTNAME, ""); + DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, ""); + DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, ""); + DBUtils.replaceKey(values, null, Passwords.USERNAME_FIELD, ""); + DBUtils.replaceKey(values, null, Passwords.PASSWORD_FIELD, ""); + DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_USERNAME, ""); + DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_PASSWORD, ""); + DBUtils.replaceKey(values, null, Passwords.ENC_TYPE, "0"); + DBUtils.replaceKey(values, null, Passwords.TIME_LAST_USED, nowString); + DBUtils.replaceKey(values, null, Passwords.TIME_PASSWORD_CHANGED, nowString); + DBUtils.replaceKey(values, null, Passwords.TIMES_USED, "0"); + break; + } + + default: + throw new UnsupportedOperationException("Unknown insert URI " + uri); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int deleted = 0; + final SQLiteBridge db = getDatabase(uri); + if (db == null) + return deleted; + + String table = getTable(uri); + if (table.equals("")) + return deleted; + + if (selection != null) { + for (String item : mSyncColumns) + selection = selection.replace(item, translateColumn(item)); + } + + try { + deleted = db.delete(table, selection, selectionArgs); + } catch (SQLiteBridgeException ex) { + Log.e(LOGTAG, "Error deleting record", ex); + } + + return deleted; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + long id = -1; + final SQLiteBridge db = getDatabase(uri); + if (db == null) + return null; + + String table = getTable(uri); + if (table.equals("")) + return null; + + try { + setupDefaults(uri, values); + } catch(Exception ex) { + Log.e(LOGTAG, "Error setting up defaults", ex); + return null; + } + + try { + id = db.insert(table, "", values); + } catch(SQLiteBridgeException ex) { + Log.e(LOGTAG, "Error inserting in db", ex); + } + + return ContentUris.withAppendedId(uri, id); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + int updated = 0; + final SQLiteBridge db = getDatabase(uri); + if (db == null) + return updated; + + String table = getTable(uri); + if (TextUtils.isEmpty(table)) + return updated; + + if (selection != null) { + for (String item : mSyncColumns) + selection = selection.replace(item, translateColumn(item)); + } + + try { + return db.update(table, values, selection, selectionArgs); + } catch(SQLiteBridgeException ex) { + Log.e(LOGTAG, "Error updating table", ex); + } + return 0; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + Cursor cursor = null; + final SQLiteBridge db = getDatabase(uri); + if (db == null) + return cursor; + + String table = getTable(uri); + if (table.equals("")) + return cursor; + + sortOrder = getSortOrder(uri, sortOrder); + if (TextUtils.isEmpty(sortOrder)) + return cursor; + + try { + cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null); + cursor.setNotificationUri(getContext().getContentResolver(), getAuthUri(uri)); + } catch (SQLiteBridgeException ex) { + Log.e(LOGTAG, "Error querying database", ex); + } + + return cursor; + } + + private String translateColumn(String column) { + if (column.equals(SyncColumns.DATE_CREATED)) + return Passwords.TIME_CREATED; + else if (column.equals(SyncColumns.DATE_MODIFIED)) + return Passwords.TIME_PASSWORD_CHANGED; + else if (column.equals(CommonColumns._ID)) + return Passwords.ID; + else + return column; + } +}
--- a/mobile/android/base/sqlite/SQLiteBridge.java +++ b/mobile/android/base/sqlite/SQLiteBridge.java @@ -1,53 +1,28 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Mozilla Android code. - * - * The Initial Developer of the Original Code is Mozilla Foundation. - * Portions created by the Initial Developer are Copyright (C) 2012 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Gian-Carlo Pascutto <gpascutto@mozilla.com> - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ +/* 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.sqlite; import org.mozilla.gecko.sqlite.SQLiteBridgeException; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.text.TextUtils; import android.util.Log; import java.lang.String; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; /* * This class allows using the mozsqlite3 library included with Firefox * to read SQLite databases, instead of the Android SQLiteDataBase API, * which might use whatever outdated DB is present on the Android system. */ public class SQLiteBridge { private static final String LOGTAG = "SQLiteBridge"; @@ -63,16 +38,227 @@ public class SQLiteBridge { ArrayList<Object[]> aRes) throws SQLiteBridgeException; // Takes the path to the database we want to access. public SQLiteBridge(String aDb) throws SQLiteBridgeException { mDb = aDb; } + // Executes a simple line of sql. + public void execSQL(String sql) + throws SQLiteBridgeException { + try { + query(sql, null); + } catch(SQLiteBridgeException ex) { + throw ex; + } + } + + // Executes a simple line of sql. Allow you to bind arguments + public void execSQL(String sql, String[] bindArgs) + throws SQLiteBridgeException { + try { + query(sql, bindArgs); + } catch(SQLiteBridgeException ex) { + throw ex; + } + } + + // Executes a DELETE statement on the database + public int delete(String table, String whereClause, String[] whereArgs) + throws SQLiteBridgeException { + StringBuilder sb = new StringBuilder("DELETE from "); + sb.append(table); + if (whereClause != null) { + sb.append(" WHERE " + whereClause); + } + + try { + return getIntResult(sb.toString(), whereArgs, 1); + } catch(SQLiteBridgeException ex) { + throw ex; + } + } + + public Cursor query(String table, + String[] columns, + String selection, + String[] selectionArgs, + String groupBy, + String having, + String orderBy, + String limit) + throws SQLiteBridgeException { + StringBuilder sb = new StringBuilder("SELECT "); + if (columns != null) + sb.append(TextUtils.join(", ", columns)); + else + sb.append(" * "); + + sb.append(" FROM "); + sb.append(table); + + if (selection != null) { + sb.append(" WHERE " + selection); + } + + if (groupBy != null) { + sb.append(" GROUP BY " + groupBy); + } + + if (having != null) { + sb.append(" HAVING " + having); + } + + if (orderBy != null) { + sb.append(" ORDER BY " + orderBy); + } + + if (limit != null) { + sb.append(" " + limit); + } + + ArrayList<Object[]> results; + try { + mColumns = null; + + results = query(sb.toString(), selectionArgs); + + } catch(SQLiteBridgeException ex) { + throw ex; + } + + MatrixCursor cursor = new MatrixCursor(mColumns.toArray(new String[0])); + try { + for (Object resultRow: results) { + Object[] resultColumns = (Object[])resultRow; + if (resultColumns.length == mColumns.size()) + cursor.addRow(resultColumns); + } + } catch(IllegalArgumentException ex) { + Log.e(LOGTAG, "Error getting rows", ex); + } + + return cursor; + } + + public long insert(String table, String nullColumnHack, ContentValues values) + throws SQLiteBridgeException { + Set<Entry<String, Object>> valueSet = values.valueSet(); + Iterator<Entry<String, Object>> valueIterator = valueSet.iterator(); + ArrayList<String> valueNames = new ArrayList<String>(); + ArrayList<String> valueBinds = new ArrayList<String>(); + ArrayList<String> keyNames = new ArrayList<String>(); + + while(valueIterator.hasNext()) { + Entry<String, Object> value = valueIterator.next(); + keyNames.add(value.getKey()); + valueNames.add("?"); + valueBinds.add(value.getValue().toString()); + } + + StringBuilder sb = new StringBuilder("INSERT into "); + sb.append(table); + + sb.append(" ("); + sb.append(TextUtils.join(", ", keyNames)); + sb.append(")"); + + // XXX - Do we need to bind these values? + sb.append(" VALUES ("); + sb.append(TextUtils.join(", ", valueNames)); + sb.append(") "); + + String[] binds = new String[valueBinds.size()]; + valueBinds.toArray(binds); + try { + return getIntResult(sb.toString(), binds, 0); + } catch (SQLiteBridgeException ex) { + throw ex; + } + } + + public int update(String table, ContentValues values, String whereClause, String[] whereArgs) + throws SQLiteBridgeException { + Set<Entry<String, Object>> valueSet = values.valueSet(); + Iterator<Entry<String, Object>> valueIterator = valueSet.iterator(); + ArrayList<String> valueNames = new ArrayList<String>(); + + StringBuilder sb = new StringBuilder("UPDATE "); + sb.append(table); + + sb.append(" SET "); + while(valueIterator.hasNext()) { + Entry<String, Object> value = valueIterator.next(); + sb.append(value.getKey() + " = ?"); + valueNames.add(value.getValue().toString()); + if (valueIterator.hasNext()) + sb.append(", "); + else + sb.append(" "); + } + + if (whereClause != null) { + sb.append(" WHERE "); + sb.append(whereClause); + for (int i = 0; i < whereArgs.length; i++) { + valueNames.add(whereArgs[i]); + } + } + + String[] binds = new String[valueNames.size()]; + valueNames.toArray(binds); + try { + return getIntResult(sb.toString(), binds, 1); + } catch (SQLiteBridgeException ex) { + throw ex; + } + } + + private int getIntResult(String query, String[] params, int resultIndex) + throws SQLiteBridgeException { + ArrayList<Object[]> results = null; + try { + mColumns = null; + results = query(query, params); + } catch(SQLiteBridgeException ex) { + throw ex; + } + + if (results != null) { + for (Object resultRow: results) { + Object[] resultColumns = (Object[])resultRow; + return ((Number)resultColumns[resultIndex]).intValue(); + } + } + return -1; + } + + public int getVersion() + throws SQLiteBridgeException { + ArrayList<Object[]> results = null; + try { + mColumns = null; + results = query("PRAGMA user_version"); + } catch(SQLiteBridgeException ex) { + throw ex; + } + int ret = -1; + if (results != null) { + for (Object resultRow: results) { + Object[] resultColumns = (Object[])resultRow; + String version = (String)resultColumns[0]; + ret = Integer.parseInt(version); + } + } + return ret; + } + + // Do an SQL query without parameters public ArrayList<Object[]> query(String aQuery) throws SQLiteBridgeException { String[] params = new String[0]; return query(aQuery, params); } // Do an SQL query, substituting the parameters in the query with the passed // parameters. The parameters are subsituded in order, so named parameters @@ -90,12 +276,11 @@ public class SQLiteBridge { } // Gets the index in the row Object[] for the given column name. // Returns -1 if not found. public int getColumnIndex(String aColumnName) { return mColumns.lastIndexOf(aColumnName); } - public void close() { - // nop, provided for API compatibility with SQLiteDatabase. - } + // nop, provided for API compatibility with SQLiteDatabase. + public void close() { } } \ No newline at end of file
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -216,16 +216,17 @@ var BrowserApp = { Services.obs.addObserver(this, "Preferences:Get", false); Services.obs.addObserver(this, "Preferences:Set", false); Services.obs.addObserver(this, "ScrollTo:FocusedInput", false); Services.obs.addObserver(this, "Sanitize:ClearAll", false); Services.obs.addObserver(this, "PanZoom:PanZoom", false); Services.obs.addObserver(this, "FullScreen:Exit", false); Services.obs.addObserver(this, "Viewport:Change", false); Services.obs.addObserver(this, "SearchEngines:Get", false); + Services.obs.addObserver(this, "Passwords:Init", false); function showFullScreenWarning() { NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short"); } window.addEventListener("fullscreen", function() { sendMessageToJava({ gecko: { @@ -965,16 +966,22 @@ var BrowserApp = { Sanitizer.sanitize(); } else if (aTopic == "FullScreen:Exit") { browser.contentDocument.mozCancelFullScreen(); } else if (aTopic == "Viewport:Change") { this.selectedTab.viewport = JSON.parse(aData); ViewportHandler.onResize(); } else if (aTopic == "SearchEngines:Get") { this.getSearchEngines(); + } else if (aTopic == "Passwords:Init") { + var storage = Components.classes["@mozilla.org/login-manager/storage/mozStorage;1"]. + getService(Components.interfaces.nsILoginManagerStorage); + storage.init(); + + sendMessageToJava({gecko: { type: "Passwords:Init:Return" }}); } }, get defaultBrowserWidth() { delete this.defaultBrowserWidth; let width = Services.prefs.getIntPref("browser.viewport.desktopWidth"); return this.defaultBrowserWidth = width; }
--- a/mozglue/android/SQLiteBridge.cpp +++ b/mozglue/android/SQLiteBridge.cpp @@ -30,16 +30,17 @@ * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include <stdlib.h> +#include <stdio.h> #include <jni.h> #include <android/log.h> #include "dlfcn.h" #include "APKOpen.h" #ifndef MOZ_OLD_LINKER #include "ElfLoader.h" #endif #include "SQLiteBridge.h" @@ -61,16 +62,18 @@ SQLITE_WRAPPER_INT(sqlite3_step) SQLITE_WRAPPER_INT(sqlite3_column_count) SQLITE_WRAPPER_INT(sqlite3_finalize) SQLITE_WRAPPER_INT(sqlite3_close) SQLITE_WRAPPER_INT(sqlite3_column_name) SQLITE_WRAPPER_INT(sqlite3_column_type) SQLITE_WRAPPER_INT(sqlite3_column_blob) SQLITE_WRAPPER_INT(sqlite3_column_bytes) SQLITE_WRAPPER_INT(sqlite3_column_text) +SQLITE_WRAPPER_INT(sqlite3_changes) +SQLITE_WRAPPER_INT(sqlite3_last_insert_rowid) void setup_sqlite_functions(void *sqlite_handle) { #define GETFUNC(name) f_ ## name = (name ## _t) __wrap_dlsym(sqlite_handle, #name) GETFUNC(sqlite3_open); GETFUNC(sqlite3_errmsg); GETFUNC(sqlite3_prepare_v2); GETFUNC(sqlite3_bind_parameter_count); @@ -79,16 +82,18 @@ void setup_sqlite_functions(void *sqlite GETFUNC(sqlite3_column_count); GETFUNC(sqlite3_finalize); GETFUNC(sqlite3_close); GETFUNC(sqlite3_column_name); GETFUNC(sqlite3_column_type); GETFUNC(sqlite3_column_blob); GETFUNC(sqlite3_column_bytes); GETFUNC(sqlite3_column_text); + GETFUNC(sqlite3_changes); + GETFUNC(sqlite3_last_insert_rowid); #undef GETFUNC } static bool initialized = false; static jclass stringClass; static jclass objectClass; static jclass byteBufferClass; static jclass arrayListClass; @@ -152,102 +157,134 @@ Java_org_mozilla_gecko_sqlite_SQLiteBrid jstring jDb, jstring jQuery, jobjectArray jParams, jobject jColumns, jobject jArrayList) { JNI_Setup(jenv); + char* errorMsg; + jsize numPars = 0; + const char* queryStr; queryStr = jenv->GetStringUTFChars(jQuery, NULL); const char* dbPath; dbPath = jenv->GetStringUTFChars(jDb, NULL); const char *pzTail; sqlite3_stmt *ppStmt; sqlite3 *db; int rc; rc = f_sqlite3_open(dbPath, &db); jenv->ReleaseStringUTFChars(jDb, dbPath); if (rc != SQLITE_OK) { - LOG("Can't open database: %s\n", f_sqlite3_errmsg(db)); + asprintf(&errorMsg, "Can't open database: %s\n", f_sqlite3_errmsg(db)); goto error_close; } rc = f_sqlite3_prepare_v2(db, queryStr, -1, &ppStmt, &pzTail); if (rc != SQLITE_OK || ppStmt == NULL) { - LOG("Can't prepare statement: %s\n", f_sqlite3_errmsg(db)); + asprintf(&errorMsg, "Can't prepare statement: %s\n", f_sqlite3_errmsg(db)); goto error_close; } jenv->ReleaseStringUTFChars(jQuery, queryStr); // Check if number of parameters matches - jsize numPars; - numPars = jenv->GetArrayLength(jParams); + if (jParams != NULL) { + numPars = jenv->GetArrayLength(jParams); + } int sqlNumPars; sqlNumPars = f_sqlite3_bind_parameter_count(ppStmt); if (numPars != sqlNumPars) { - LOG("Passed parameter count (%d) doesn't match SQL parameter count (%d)\n", + asprintf(&errorMsg, "Passed parameter count (%d) doesn't match SQL parameter count (%d)\n", numPars, sqlNumPars); goto error_close; } - // Bind parameters, if any - if (numPars > 0) { - for (int i = 0; i < numPars; i++) { - jobject jObjectParam = jenv->GetObjectArrayElement(jParams, i); - // IsInstanceOf or isAssignableFrom? String is final, so IsInstanceOf - // should be OK. - jboolean isString = jenv->IsInstanceOf(jObjectParam, stringClass); - if (isString != JNI_TRUE) { - LOG("Parameter is not of String type"); - goto error_close; - } - jstring jStringParam = (jstring)jObjectParam; - const char* paramStr = jenv->GetStringUTFChars(jStringParam, NULL); - // SQLite parameters index from 1. - rc = f_sqlite3_bind_text(ppStmt, i + 1, paramStr, -1, SQLITE_TRANSIENT); - jenv->ReleaseStringUTFChars(jStringParam, paramStr); - if (rc != SQLITE_OK) { - LOG("Error binding query parameter"); - goto error_close; + + if (jParams != NULL) { + // Bind parameters, if any + if (numPars > 0) { + for (int i = 0; i < numPars; i++) { + jobject jObjectParam = jenv->GetObjectArrayElement(jParams, i); + // IsInstanceOf or isAssignableFrom? String is final, so IsInstanceOf + // should be OK. + jboolean isString = jenv->IsInstanceOf(jObjectParam, stringClass); + if (isString != JNI_TRUE) { + asprintf(&errorMsg, "Parameter is not of String type"); + goto error_close; + } + jstring jStringParam = (jstring)jObjectParam; + const char* paramStr = jenv->GetStringUTFChars(jStringParam, NULL); + // SQLite parameters index from 1. + rc = f_sqlite3_bind_text(ppStmt, i + 1, paramStr, -1, SQLITE_TRANSIENT); + jenv->ReleaseStringUTFChars(jStringParam, paramStr); + if (rc != SQLITE_OK) { + asprintf(&errorMsg, "Error binding query parameter"); + goto error_close; + } } } } // Execute the query and step through the results rc = f_sqlite3_step(ppStmt); if (rc != SQLITE_ROW && rc != SQLITE_DONE) { - LOG("Can't step statement: (%d) %s\n", rc, f_sqlite3_errmsg(db)); + asprintf(&errorMsg, "Can't step statement: (%d) %s\n", rc, f_sqlite3_errmsg(db)); goto error_close; } // Get the column names int cols; cols = f_sqlite3_column_count(ppStmt); for (int i = 0; i < cols; i++) { const char* colName = f_sqlite3_column_name(ppStmt, i); jstring jStr = jenv->NewStringUTF(colName); jenv->CallBooleanMethod(jColumns, jArrayListAdd, jStr); jenv->DeleteLocalRef(jStr); } + // if the statement doesn't return any results, instead return the id and number of changed rows + if (rc == SQLITE_DONE) { + jclass integerClass = jenv->FindClass("java/lang/Integer"); + jmethodID intConstructor = jenv->GetMethodID(integerClass, "<init>", "(I)V"); + + jobjectArray jRow = jenv->NewObjectArray(2, objectClass, NULL); + if (jRow == NULL) { + asprintf(&errorMsg, "Can't allocate jRow Object[]\n"); + goto error_close; + } + + int id = f_sqlite3_last_insert_rowid(db); + jobject jId = jenv->NewObject(integerClass, intConstructor, id); + jenv->SetObjectArrayElement(jRow, 0, jId); + jenv->DeleteLocalRef(jId); + + int changed = f_sqlite3_changes(db); + jobject jChanged = jenv->NewObject(integerClass, intConstructor, changed); + jenv->SetObjectArrayElement(jRow, 1, jChanged); + jenv->DeleteLocalRef(jChanged); + + jenv->CallBooleanMethod(jArrayList, jArrayListAdd, jRow); + jenv->DeleteLocalRef(jRow); + } + // For each row, add an Object[] to the passed ArrayList, // with that containing either String or ByteArray objects // containing the columns while (rc != SQLITE_DONE) { // Process row // Construct Object[] jobjectArray jRow = jenv->NewObjectArray(cols, objectClass, NULL); if (jRow == NULL) { - LOG("Can't allocate jRow Object[]\n"); + asprintf(&errorMsg, "Can't allocate jRow Object[]\n"); goto error_close; } for (int i = 0; i < cols; i++) { int colType = f_sqlite3_column_type(ppStmt, i); if (colType == SQLITE_BLOB) { // Treat as blob const void* blob = f_sqlite3_column_blob(ppStmt, i); @@ -260,17 +297,17 @@ Java_org_mozilla_gecko_sqlite_SQLiteBrid colLen); if (jByteBuffer == NULL) { goto error_close; } // Get its backing array void* bufferArray = jenv->GetDirectBufferAddress(jByteBuffer); if (bufferArray == NULL) { - LOG("Failure calling GetDirectBufferAddress\n"); + asprintf(&errorMsg, "Failure calling GetDirectBufferAddress\n"); goto error_close; } memcpy(bufferArray, blob, colLen); jenv->SetObjectArrayElement(jRow, i, jByteBuffer); jenv->DeleteLocalRef(jByteBuffer); } else if (colType == SQLITE_NULL) { jenv->SetObjectArrayElement(jRow, i, jNull); @@ -289,27 +326,29 @@ Java_org_mozilla_gecko_sqlite_SQLiteBrid // Clean up jenv->DeleteLocalRef(jRow); // Get next row rc = f_sqlite3_step(ppStmt); // Real error? if (rc != SQLITE_ROW && rc != SQLITE_DONE) { - LOG("Can't re-step statement:(%d) %s\n", rc, f_sqlite3_errmsg(db)); + asprintf(&errorMsg, "Can't re-step statement:(%d) %s\n", rc, f_sqlite3_errmsg(db)); goto error_close; } } rc = f_sqlite3_finalize(ppStmt); if (rc != SQLITE_OK) { - LOG("Can't finalize statement: %s\n", f_sqlite3_errmsg(db)); + asprintf(&errorMsg, "Can't finalize statement: %s\n", f_sqlite3_errmsg(db)); goto error_close; } f_sqlite3_close(db); return; error_close: f_sqlite3_close(db); - JNI_Throw(jenv, "org/mozilla/gecko/sqlite/SQLiteBridgeException", "SQLite error"); + LOG("Error in SQLiteBridge: %s\n", errorMsg); + JNI_Throw(jenv, "org/mozilla/gecko/sqlite/SQLiteBridgeException", errorMsg); + free(errorMsg); return; }
--- a/mozglue/android/SQLiteBridge.h +++ b/mozglue/android/SQLiteBridge.h @@ -54,10 +54,12 @@ SQLITE_WRAPPER(sqlite3_step, int, sqlite SQLITE_WRAPPER(sqlite3_column_count, int, sqlite3_stmt*) SQLITE_WRAPPER(sqlite3_finalize, int, sqlite3_stmt*) SQLITE_WRAPPER(sqlite3_close, int, sqlite3*) SQLITE_WRAPPER(sqlite3_column_name, const char*, sqlite3_stmt*, int) SQLITE_WRAPPER(sqlite3_column_type, int, sqlite3_stmt*, int) SQLITE_WRAPPER(sqlite3_column_blob, const void*, sqlite3_stmt*, int) SQLITE_WRAPPER(sqlite3_column_bytes, int, sqlite3_stmt*, int) SQLITE_WRAPPER(sqlite3_column_text, const unsigned char*, sqlite3_stmt*, int) +SQLITE_WRAPPER(sqlite3_changes, int, sqlite3*) +SQLITE_WRAPPER(sqlite3_last_insert_rowid, sqlite3_int64, sqlite3*) #endif /* SQLiteBridge_h */