mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge m-c to s-c.
This commit is contained in:
commit
c0ed82d59e
@ -134,7 +134,7 @@ public class AwesomeBarTabs extends TabHost {
|
|||||||
ImageView favicon = (ImageView) childView.findViewById(R.id.favicon);
|
ImageView favicon = (ImageView) childView.findViewById(R.id.favicon);
|
||||||
|
|
||||||
if (b == null) {
|
if (b == null) {
|
||||||
favicon.setImageResource(android.R.id.empty);
|
favicon.setImageDrawable(null);
|
||||||
} else {
|
} else {
|
||||||
Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
|
Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
|
||||||
favicon.setImageBitmap(bitmap);
|
favicon.setImageBitmap(bitmap);
|
||||||
@ -150,7 +150,7 @@ public class AwesomeBarTabs extends TabHost {
|
|||||||
ImageView favicon = (ImageView) view;
|
ImageView favicon = (ImageView) view;
|
||||||
|
|
||||||
if (b == null) {
|
if (b == null) {
|
||||||
favicon.setImageResource(android.R.id.empty);
|
favicon.setImageDrawable(null);
|
||||||
} else {
|
} else {
|
||||||
Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
|
Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
|
||||||
favicon.setImageBitmap(bitmap);
|
favicon.setImageBitmap(bitmap);
|
||||||
|
@ -905,10 +905,11 @@ abstract public class GeckoApp
|
|||||||
ExtraMenuItem item = i.next();
|
ExtraMenuItem item = i.next();
|
||||||
if (item.id == id) {
|
if (item.id == id) {
|
||||||
sExtraMenuItems.remove(item);
|
sExtraMenuItems.remove(item);
|
||||||
|
if (sMenu == null)
|
||||||
|
return;
|
||||||
MenuItem menu = sMenu.findItem(id);
|
MenuItem menu = sMenu.findItem(id);
|
||||||
if (menu != null)
|
if (menu != null)
|
||||||
sMenu.removeItem(id);
|
sMenu.removeItem(id);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.equals("Toast:Show")) {
|
} else if (event.equals("Toast:Show")) {
|
||||||
|
@ -1900,7 +1900,7 @@ public class GeckoAppShell
|
|||||||
*/
|
*/
|
||||||
public static byte[] decodeBase64(byte[] in) {
|
public static byte[] decodeBase64(byte[] in) {
|
||||||
if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.FROYO)
|
if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.FROYO)
|
||||||
return Base64.decode(in, GUID_ENCODE_FLAGS);
|
return Base64.decode(in, Base64.DEFAULT);
|
||||||
int iOff = 0;
|
int iOff = 0;
|
||||||
int iLen = in.length;
|
int iLen = in.length;
|
||||||
if (iLen%4 != 0) throw new IllegalArgumentException ("Length of Base64 encoded input string is not a multiple of 4.");
|
if (iLen%4 != 0) throw new IllegalArgumentException ("Length of Base64 encoded input string is not a multiple of 4.");
|
||||||
|
@ -57,8 +57,8 @@ SYNC_RES_DRAWABLE_MDPI=$(shell cat $(topsrcdir)/mobile/android/sync/android-draw
|
|||||||
SYNC_RES_DRAWABLE_HDPI=$(shell cat $(topsrcdir)/mobile/android/sync/android-drawable-hdpi-resources.mn | tr '\n' ' ';)
|
SYNC_RES_DRAWABLE_HDPI=$(shell cat $(topsrcdir)/mobile/android/sync/android-drawable-hdpi-resources.mn | tr '\n' ' ';)
|
||||||
SYNC_RES_LAYOUT=$(shell cat $(topsrcdir)/mobile/android/sync/android-layout-resources.mn | tr '\n' ' ';)
|
SYNC_RES_LAYOUT=$(shell cat $(topsrcdir)/mobile/android/sync/android-layout-resources.mn | tr '\n' ' ';)
|
||||||
SYNC_RES_VALUES=$(shell cat $(topsrcdir)/mobile/android/sync/android-values-resources.mn | tr '\n' ' ';)
|
SYNC_RES_VALUES=$(shell cat $(topsrcdir)/mobile/android/sync/android-values-resources.mn | tr '\n' ' ';)
|
||||||
SYNC_RES_XML=res/xml/sync_authenticator.xml res/xml/sync_options.xml
|
SYNC_RES_XML=res/xml/sync_authenticator.xml
|
||||||
SYNC_PP_RES_XML=res/xml/sync_syncadapter.xml
|
SYNC_PP_RES_XML=res/xml/sync_syncadapter.xml res/xml/sync_options.xml
|
||||||
|
|
||||||
FENNEC_JAVA_FILES = \
|
FENNEC_JAVA_FILES = \
|
||||||
AboutHomeContent.java \
|
AboutHomeContent.java \
|
||||||
@ -597,9 +597,10 @@ classes.dex: $(FENNEC_JAVA_FILES) $(FENNEC_PP_JAVA_FILES) $(SYNC_JAVA_FILES) $(S
|
|||||||
|
|
||||||
PP_RES_XML=$(SYNC_PP_RES_XML)
|
PP_RES_XML=$(SYNC_PP_RES_XML)
|
||||||
|
|
||||||
$(PP_RES_XML): $(subst res/,$(srcdir)/resources/, $(PP_RES_XML).in)
|
# This is kinda awful; if any of the source files change, we remake them all.
|
||||||
|
$(PP_RES_XML): $(patsubst res/%,$(srcdir)/resources/%.in,$(PP_RES_XML))
|
||||||
$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
|
$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
|
||||||
$(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $< > $@
|
$(AUTOMATION_PPARGS) $(DEFINES) $(ACDEFINES) $(subst res,$(srcdir)/resources,$@).in > $@
|
||||||
|
|
||||||
# AndroidManifest.xml includes these files, so they need to be marked as dependencies.
|
# AndroidManifest.xml includes these files, so they need to be marked as dependencies.
|
||||||
SYNC_MANIFEST_FRAGMENTS = $(wildcard $(topsrcdir)/mobile/android/sync/manifests/*.in)
|
SYNC_MANIFEST_FRAGMENTS = $(wildcard $(topsrcdir)/mobile/android/sync/manifests/*.in)
|
||||||
@ -635,10 +636,10 @@ $(RESOURCES): $(RES_DIRS) $(subst res/,$(srcdir)/resources/,$(RESOURCES))
|
|||||||
$(NSINSTALL) $(subst res/,$(srcdir)/resources/,$@) $(dir $@)
|
$(NSINSTALL) $(subst res/,$(srcdir)/resources/,$@) $(dir $@)
|
||||||
|
|
||||||
|
|
||||||
R.java: $(MOZ_APP_ICON) $(RESOURCES) $(RES_DRAWABLE) $(RES_DRAWABLE_LDPI) $(RES_DRAWABLE_MDPI) $(RES_DRAWABLE_HDPI) $(PP_RES_XML) res/values/defaults.xml res/drawable/sync_icon.png res/drawable/icon.png res/drawable-hdpi/icon.png res/values/strings.xml AndroidManifest.xml FORCE
|
R.java: $(MOZ_APP_ICON) $(RESOURCES) $(RES_DRAWABLE) $(RES_DRAWABLE_LDPI) $(RES_DRAWABLE_MDPI) $(RES_DRAWABLE_HDPI) $(PP_RES_XML) res/values/defaults.xml res/drawable/sync_ic_launcher.png res/drawable/icon.png res/drawable-hdpi/icon.png res/values/strings.xml AndroidManifest.xml FORCE
|
||||||
$(AAPT) package -f -M AndroidManifest.xml -I $(ANDROID_SDK)/android.jar -S res -J . --custom-package org.mozilla.gecko
|
$(AAPT) package -f -M AndroidManifest.xml -I $(ANDROID_SDK)/android.jar -S res -J . --custom-package org.mozilla.gecko
|
||||||
|
|
||||||
gecko.ap_: AndroidManifest.xml res/drawable/sync_icon.png res/drawable/icon.png res/drawable-hdpi/icon.png $(RESOURCES) $(RES_DRAWABLE) $(RES_DRAWABLE_LDPI) $(RES_DRAWABLE_MDPI) $(RES_DRAWABLE_HDPI) $(PP_RES_XML) res/values/defaults.xml res/values/strings.xml FORCE
|
gecko.ap_: AndroidManifest.xml res/drawable/sync_ic_launcher.png res/drawable/icon.png res/drawable-hdpi/icon.png $(RESOURCES) $(RES_DRAWABLE) $(RES_DRAWABLE_LDPI) $(RES_DRAWABLE_MDPI) $(RES_DRAWABLE_HDPI) $(PP_RES_XML) res/values/defaults.xml res/values/strings.xml FORCE
|
||||||
$(AAPT) package -f -M AndroidManifest.xml -I $(ANDROID_SDK)/android.jar -S res -F $@
|
$(AAPT) package -f -M AndroidManifest.xml -I $(ANDROID_SDK)/android.jar -S res -F $@
|
||||||
|
|
||||||
libs:: classes.dex package-name.txt
|
libs:: classes.dex package-name.txt
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<!ENTITY sync.pin.default.label '...\n...\n...\n'>
|
<!ENTITY sync.pin.default.label '...\n...\n...\n'>
|
||||||
<!ENTITY sync.pin.oneline.label '...'>
|
<!ENTITY sync.pin.oneline.label '...'>
|
||||||
<!ENTITY sync.link.show.label 'Show me how.'>
|
<!ENTITY sync.link.show.label 'Show me how.'>
|
||||||
<!ENTITY sync.link.advancedsetup.label 'Advanced setup...'>
|
<!ENTITY sync.link.advancedsetup.label 'Advanced setup…'>
|
||||||
<!ENTITY sync.link.nodevice.label 'I don\'t have the device with me…'>
|
<!ENTITY sync.link.nodevice.label 'I don\'t have the device with me…'>
|
||||||
|
|
||||||
<!-- J-PAKE Waiting Screen -->
|
<!-- J-PAKE Waiting Screen -->
|
||||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
@ -11,7 +11,6 @@
|
|||||||
android:layout_centerVertical="true"
|
android:layout_centerVertical="true"
|
||||||
android:minWidth="32dip"
|
android:minWidth="32dip"
|
||||||
android:minHeight="32dip"
|
android:minHeight="32dip"
|
||||||
android:src="@android:id/empty"
|
|
||||||
android:scaleType="fitCenter"/>
|
android:scaleType="fitCenter"/>
|
||||||
|
|
||||||
<TextView android:id="@+id/title"
|
<TextView android:id="@+id/title"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
|
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:accountType="org.mozilla.firefox_sync"
|
android:accountType="org.mozilla.firefox_sync"
|
||||||
android:icon="@drawable/sync_icon"
|
android:icon="@drawable/sync_ic_launcher"
|
||||||
android:smallIcon="@drawable/sync_icon"
|
android:smallIcon="@drawable/sync_ic_launcher"
|
||||||
android:label="@string/sync_account_label"
|
android:label="@string/sync_account_label"
|
||||||
android:accountPreferences="@xml/sync_options" />
|
android:accountPreferences="@xml/sync_options" />
|
@ -1,3 +1,4 @@
|
|||||||
|
#filter substitution
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
@ -8,7 +9,7 @@
|
|||||||
android:summary="@string/sync_settings_summary_pair">
|
android:summary="@string/sync_settings_summary_pair">
|
||||||
<intent
|
<intent
|
||||||
android:action="android.intent.action.MAIN"
|
android:action="android.intent.action.MAIN"
|
||||||
android:targetPackage="org.mozilla.gecko"
|
android:targetPackage="@ANDROID_PACKAGE_NAME@"
|
||||||
android:targetClass="org.mozilla.gecko.sync.setup.activities.SetupSyncActivity">
|
android:targetClass="org.mozilla.gecko.sync.setup.activities.SetupSyncActivity">
|
||||||
<extra
|
<extra
|
||||||
android:name="isSetup"
|
android:name="isSetup"
|
@ -130,6 +130,17 @@ public class CryptoRecord extends Record {
|
|||||||
super(source.guid, source.collection, source.lastModified, source.deleted);
|
super(source.guid, source.collection, source.lastModified, source.deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Record copyWithIDs(String guid, long androidID) {
|
||||||
|
CryptoRecord out = new CryptoRecord(this);
|
||||||
|
out.guid = guid;
|
||||||
|
out.androidID = androidID;
|
||||||
|
out.sortIndex = this.sortIndex;
|
||||||
|
out.payload = (this.payload == null) ? null : new ExtendedJSONObject(this.payload.object);
|
||||||
|
out.keyBundle = this.keyBundle; // TODO: copy me?
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Take a whole record as JSON -- i.e., something like
|
* Take a whole record as JSON -- i.e., something like
|
||||||
*
|
*
|
||||||
|
@ -185,8 +185,7 @@ public class GlobalSession implements CredentialsSource, PrefsSource {
|
|||||||
config.syncKeyBundle = syncKeyBundle;
|
config.syncKeyBundle = syncKeyBundle;
|
||||||
// clusterURL and syncID are set through `persisted`, or fetched from the server.
|
// clusterURL and syncID are set through `persisted`, or fetched from the server.
|
||||||
|
|
||||||
// TODO: populate saved configurations. We'll amend these after processing meta/global.
|
assert(null == persisted);
|
||||||
this.synchronizerConfigurations = new SynchronizerConfigurations(persisted);
|
|
||||||
prepareStages();
|
prepareStages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,23 +695,4 @@ public class GlobalSession implements CredentialsSource, PrefsSource {
|
|||||||
}
|
}
|
||||||
return this.config.metaGlobal.engines.get(engineName) != null;
|
return this.config.metaGlobal.engines.get(engineName) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return enough information to be able to reconstruct a Synchronizer.
|
|
||||||
*
|
|
||||||
* @param engineName
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public SynchronizerConfiguration configForEngine(String engineName) {
|
|
||||||
// TODO: we need an altogether better way of handling empty configs.
|
|
||||||
SynchronizerConfiguration stored = this.getSynchronizerConfigurations().forEngine(engineName);
|
|
||||||
if (stored == null) {
|
|
||||||
return new SynchronizerConfiguration(engineName, new RepositorySessionBundle(0), new RepositorySessionBundle(0));
|
|
||||||
}
|
|
||||||
return stored;
|
|
||||||
}
|
|
||||||
private SynchronizerConfigurations synchronizerConfigurations;
|
|
||||||
private SynchronizerConfigurations getSynchronizerConfigurations() {
|
|
||||||
return this.synchronizerConfigurations;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -91,15 +91,6 @@ public class SynchronizerConfigurations {
|
|||||||
engines = new HashMap<String, SynchronizerConfiguration>();
|
engines = new HashMap<String, SynchronizerConfiguration>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fillBundle(Bundle bundle) {
|
|
||||||
Bundle contents = new Bundle();
|
|
||||||
for (Entry<String, SynchronizerConfiguration> entry : engines.entrySet()) {
|
|
||||||
contents.putStringArray(entry.getKey(), entry.getValue().toStringValues());
|
|
||||||
}
|
|
||||||
contents.putInt("version", CONFIGURATION_VERSION);
|
|
||||||
bundle.putBundle("engines", contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SynchronizerConfiguration forEngine(String engineName) {
|
public SynchronizerConfiguration forEngine(String engineName) {
|
||||||
return engines.get(engineName);
|
return engines.get(engineName);
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,29 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void error(String logTag, String message) {
|
||||||
|
logToStdout(logTag, " :: ERROR: ", message);
|
||||||
|
Log.i(logTag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void info(String logTag, String message) {
|
||||||
|
logToStdout(logTag, " :: INFO: ", message);
|
||||||
|
Log.i(logTag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void debug(String logTag, String message) {
|
||||||
|
logToStdout(logTag, " :: DEBUG: ", message);
|
||||||
|
Log.d(logTag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void trace(String logTag, String message) {
|
||||||
|
if (!ENABLE_TRACE_LOGGING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logToStdout(logTag, " :: TRACE: ", message);
|
||||||
|
Log.d(logTag, message);
|
||||||
|
}
|
||||||
|
|
||||||
public static String generateGuid() {
|
public static String generateGuid() {
|
||||||
byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false);
|
byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false);
|
||||||
return new String(encodedBytes).replace("+", "-").replace("/", "_");
|
return new String(encodedBytes).replace("+", "-").replace("/", "_");
|
||||||
|
@ -1,102 +1,66 @@
|
|||||||
/* ***** BEGIN LICENSE BLOCK *****
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
* 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/. */
|
||||||
* 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 Android Sync Client.
|
|
||||||
*
|
|
||||||
* The Initial Developer of the Original Code is
|
|
||||||
* the Mozilla Foundation.
|
|
||||||
* Portions created by the Initial Developer are Copyright (C) 2011
|
|
||||||
* the Initial Developer. All Rights Reserved.
|
|
||||||
*
|
|
||||||
* Contributor(s):
|
|
||||||
* Jason Voll
|
|
||||||
*
|
|
||||||
* 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 ***** */
|
|
||||||
|
|
||||||
package org.mozilla.gecko.sync.crypto;
|
package org.mozilla.gecko.sync.crypto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* All info in these objects should be decoded (i.e. not BaseXX encoded).
|
* All info in these objects should be decoded (i.e. not BaseXX encoded).
|
||||||
*/
|
*/
|
||||||
public class CryptoInfo {
|
public class CryptoInfo {
|
||||||
|
|
||||||
private byte[] message;
|
private byte[] message;
|
||||||
private byte[] iv;
|
private byte[] iv;
|
||||||
private byte[] hmac;
|
private byte[] hmac;
|
||||||
private KeyBundle keys;
|
private KeyBundle keys;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Constructor typically used when encrypting
|
* Constructor typically used when encrypting.
|
||||||
*/
|
*/
|
||||||
public CryptoInfo(byte[] message, KeyBundle keys) {
|
public CryptoInfo(byte[] message, KeyBundle keys) {
|
||||||
this.setMessage(message);
|
this.setMessage(message);
|
||||||
this.setKeys(keys);
|
this.setKeys(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Constructor typically used when decrypting
|
* Constructor typically used when decrypting.
|
||||||
*/
|
*/
|
||||||
public CryptoInfo(byte[] message, byte[] iv, byte[] hmac, KeyBundle keys) {
|
public CryptoInfo(byte[] message, byte[] iv, byte[] hmac, KeyBundle keys) {
|
||||||
this.setMessage(message);
|
this.setMessage(message);
|
||||||
this.setIV(iv);
|
this.setIV(iv);
|
||||||
this.setHMAC(hmac);
|
this.setHMAC(hmac);
|
||||||
this.setKeys(keys);
|
this.setKeys(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getMessage() {
|
public byte[] getMessage() {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMessage(byte[] message) {
|
public void setMessage(byte[] message) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getIV() {
|
public byte[] getIV() {
|
||||||
return iv;
|
return iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIV(byte[] iv) {
|
public void setIV(byte[] iv) {
|
||||||
this.iv = iv;
|
this.iv = iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getHMAC() {
|
public byte[] getHMAC() {
|
||||||
return hmac;
|
return hmac;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHMAC(byte[] hmac) {
|
public void setHMAC(byte[] hmac) {
|
||||||
this.hmac = hmac;
|
this.hmac = hmac;
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeyBundle getKeys() {
|
public KeyBundle getKeys() {
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setKeys(KeyBundle keys) {
|
|
||||||
this.keys = keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public void setKeys(KeyBundle keys) {
|
||||||
|
this.keys = keys;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ import javax.crypto.spec.SecretKeySpec;
|
|||||||
import org.mozilla.apache.commons.codec.binary.Base32;
|
import org.mozilla.apache.commons.codec.binary.Base32;
|
||||||
import org.mozilla.apache.commons.codec.binary.Base64;
|
import org.mozilla.apache.commons.codec.binary.Base64;
|
||||||
import org.mozilla.gecko.sync.Utils;
|
import org.mozilla.gecko.sync.Utils;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Implements the basic required cryptography options.
|
* Implements the basic required cryptography options.
|
||||||
@ -82,7 +83,6 @@ public class Cryptographer {
|
|||||||
cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(info.getIV()));
|
cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(info.getIV()));
|
||||||
}
|
}
|
||||||
} catch (GeneralSecurityException ex) {
|
} catch (GeneralSecurityException ex) {
|
||||||
ex.printStackTrace();
|
|
||||||
throw new CryptoException(ex);
|
throw new CryptoException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +94,13 @@ public class Cryptographer {
|
|||||||
info.setIV(cipher.getIV());
|
info.setIV(cipher.getIV());
|
||||||
|
|
||||||
// Generate HMAC.
|
// Generate HMAC.
|
||||||
info.setHMAC(generateHMAC(info));
|
try {
|
||||||
|
info.setHMAC(generateHMAC(info));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new CryptoException(e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new CryptoException(e);
|
||||||
|
}
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
|
|
||||||
@ -112,8 +118,14 @@ public class Cryptographer {
|
|||||||
public static byte[] decrypt(CryptoInfo info) throws CryptoException {
|
public static byte[] decrypt(CryptoInfo info) throws CryptoException {
|
||||||
|
|
||||||
// Check HMAC.
|
// Check HMAC.
|
||||||
if (!verifyHMAC(info)) {
|
try {
|
||||||
throw new HMACVerificationException();
|
if (!verifyHMAC(info)) {
|
||||||
|
throw new HMACVerificationException();
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new CryptoException(e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new CryptoException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cipher cipher = getCipher();
|
Cipher cipher = getCipher();
|
||||||
@ -190,7 +202,7 @@ public class Cryptographer {
|
|||||||
/*
|
/*
|
||||||
* Helper to verify HMAC Input: CryptoInfo Output: true if HMAC is correct
|
* Helper to verify HMAC Input: CryptoInfo Output: true if HMAC is correct
|
||||||
*/
|
*/
|
||||||
private static boolean verifyHMAC(CryptoInfo bundle) {
|
private static boolean verifyHMAC(CryptoInfo bundle) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
byte[] generatedHMAC = generateHMAC(bundle);
|
byte[] generatedHMAC = generateHMAC(bundle);
|
||||||
byte[] expectedHMAC = bundle.getHMAC();
|
byte[] expectedHMAC = bundle.getHMAC();
|
||||||
boolean eq = Arrays.equals(generatedHMAC, expectedHMAC);
|
boolean eq = Arrays.equals(generatedHMAC, expectedHMAC);
|
||||||
@ -206,7 +218,7 @@ public class Cryptographer {
|
|||||||
* Helper to generate HMAC Input: CryptoInfo Output: a generated HMAC for
|
* Helper to generate HMAC Input: CryptoInfo Output: a generated HMAC for
|
||||||
* given cipher text
|
* given cipher text
|
||||||
*/
|
*/
|
||||||
private static byte[] generateHMAC(CryptoInfo bundle) {
|
private static byte[] generateHMAC(CryptoInfo bundle) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
Mac hmacHasher = HKDF.makeHMACHasher(bundle.getKeys().getHMACKey());
|
Mac hmacHasher = HKDF.makeHMACHasher(bundle.getKeys().getHMACKey());
|
||||||
return hmacHasher.doFinal(Base64.encodeBase64(bundle.getMessage()));
|
return hmacHasher.doFinal(Base64.encodeBase64(bundle.getMessage()));
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,6 @@
|
|||||||
/* ***** BEGIN LICENSE BLOCK *****
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
* 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/. */
|
||||||
* 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 Android Sync Client.
|
|
||||||
*
|
|
||||||
* The Initial Developer of the Original Code is
|
|
||||||
* the Mozilla Foundation.
|
|
||||||
* Portions created by the Initial Developer are Copyright (C) 2011
|
|
||||||
* the Initial Developer. All Rights Reserved.
|
|
||||||
*
|
|
||||||
* Contributor(s):
|
|
||||||
* Jason Voll
|
|
||||||
*
|
|
||||||
* 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 ***** */
|
|
||||||
|
|
||||||
package org.mozilla.gecko.sync.crypto;
|
package org.mozilla.gecko.sync.crypto;
|
||||||
|
|
||||||
@ -46,105 +13,99 @@ import javax.crypto.spec.SecretKeySpec;
|
|||||||
|
|
||||||
import org.mozilla.gecko.sync.Utils;
|
import org.mozilla.gecko.sync.Utils;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A standards-compliant implementation of RFC 5869
|
* A standards-compliant implementation of RFC 5869
|
||||||
* for HMAC-based Key Derivation Function.
|
* for HMAC-based Key Derivation Function.
|
||||||
* HMAC uses HMAC SHA256 standard.
|
* HMAC uses HMAC SHA256 standard.
|
||||||
*/
|
*/
|
||||||
public class HKDF {
|
public class HKDF {
|
||||||
|
public static String HMAC_ALGORITHM = "hmacSHA256";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for conversion in cases in which you *know* the encoding exists.
|
* Used for conversion in cases in which you *know* the encoding exists.
|
||||||
*/
|
*/
|
||||||
public static final byte[] bytes(String in) {
|
public static final byte[] bytes(String in) {
|
||||||
try {
|
try {
|
||||||
return in.getBytes("UTF-8");
|
return in.getBytes("UTF-8");
|
||||||
} catch (java.io.UnsupportedEncodingException e) {
|
} catch (java.io.UnsupportedEncodingException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int BLOCKSIZE = 256 / 8;
|
||||||
|
public static final byte[] HMAC_INPUT = bytes("Sync-AES_256_CBC-HMAC256");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Step 1 of RFC 5869
|
||||||
|
* Get sha256HMAC Bytes
|
||||||
|
* Input: salt (message), IKM (input keyring material)
|
||||||
|
* Output: PRK (pseudorandom key)
|
||||||
|
*/
|
||||||
|
public static byte[] hkdfExtract(byte[] salt, byte[] IKM) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
return digestBytes(IKM, makeHMACHasher(salt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Step 2 of RFC 5869.
|
||||||
|
* Input: PRK from step 1, info, length.
|
||||||
|
* Output: OKM (output keyring material).
|
||||||
|
*/
|
||||||
|
public static byte[] hkdfExpand(byte[] prk, byte[] info, int len) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
Mac hmacHasher = makeHMACHasher(prk);
|
||||||
|
|
||||||
|
byte[] T = {};
|
||||||
|
byte[] Tn = {};
|
||||||
|
|
||||||
|
int iterations = (int) Math.ceil(((double)len) / ((double)BLOCKSIZE));
|
||||||
|
for (int i = 0; i < iterations; i++) {
|
||||||
|
Tn = digestBytes(Utils.concatAll(Tn, info, Utils.hex2Byte(Integer.toHexString(i + 1))),
|
||||||
|
hmacHasher);
|
||||||
|
T = Utils.concatAll(T, Tn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final int BLOCKSIZE = 256 / 8;
|
byte[] result = new byte[len];
|
||||||
public static final byte[] HMAC_INPUT = bytes("Sync-AES_256_CBC-HMAC256");
|
System.arraycopy(T, 0, result, 0, len);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Step 1 of RFC 5869
|
* Make HMAC key
|
||||||
* Get sha256HMAC Bytes
|
* Input: key (salt)
|
||||||
* Input: salt (message), IKM (input keyring material)
|
* Output: Key HMAC-Key
|
||||||
* Output: PRK (pseudorandom key)
|
*/
|
||||||
*/
|
public static Key makeHMACKey(byte[] key) {
|
||||||
public static byte[] hkdfExtract(byte[] salt, byte[] IKM) {
|
if (key.length == 0) {
|
||||||
return digestBytes(IKM, makeHMACHasher(salt));
|
key = new byte[BLOCKSIZE];
|
||||||
}
|
}
|
||||||
|
return new SecretKeySpec(key, HMAC_ALGORITHM);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Step 2 of RFC 5869.
|
* Make an HMAC hasher
|
||||||
* Input: PRK from step 1, info, length.
|
* Input: Key hmacKey
|
||||||
* Output: OKM (output keyring material).
|
* Ouput: An HMAC Hasher
|
||||||
*/
|
*/
|
||||||
public static byte[] hkdfExpand(byte[] prk, byte[] info, int len) {
|
public static Mac makeHMACHasher(byte[] key) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
Mac hmacHasher = null;
|
||||||
|
hmacHasher = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
|
||||||
Mac hmacHasher = makeHMACHasher(prk);
|
// If Mac.getInstance doesn't throw NoSuchAlgorithmException, hmacHasher is
|
||||||
|
// non-null.
|
||||||
|
assert(hmacHasher != null);
|
||||||
|
|
||||||
byte[] T = {};
|
hmacHasher.init(makeHMACKey(key));
|
||||||
byte[] Tn = {};
|
return hmacHasher;
|
||||||
|
}
|
||||||
|
|
||||||
int iterations = (int) Math.ceil(((double)len) / ((double)BLOCKSIZE));
|
/*
|
||||||
for (int i = 0; i < iterations; i++) {
|
* Hash bytes with given hasher
|
||||||
Tn = digestBytes(Utils.concatAll
|
* Input: message to hash, HMAC hasher
|
||||||
(Tn, info, Utils.hex2Byte(Integer.toHexString(i + 1))), hmacHasher);
|
* Output: hashed byte[].
|
||||||
T = Utils.concatAll(T, Tn);
|
*/
|
||||||
}
|
public static byte[] digestBytes(byte[] message, Mac hasher) {
|
||||||
|
hasher.update(message);
|
||||||
byte[] result = new byte[len];
|
byte[] ret = hasher.doFinal();
|
||||||
System.arraycopy(T, 0, result, 0, len);
|
hasher.reset();
|
||||||
return result;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Make HMAC key
|
|
||||||
* Input: key (salt)
|
|
||||||
* Output: Key HMAC-Key
|
|
||||||
*/
|
|
||||||
public static Key makeHMACKey(byte[] key) {
|
|
||||||
if (key.length == 0) {
|
|
||||||
key = new byte[BLOCKSIZE];
|
|
||||||
}
|
|
||||||
return new SecretKeySpec(key, "HmacSHA256");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Make an HMAC hasher
|
|
||||||
* Input: Key hmacKey
|
|
||||||
* Ouput: An HMAC Hasher
|
|
||||||
*/
|
|
||||||
public static Mac makeHMACHasher(byte[] key) {
|
|
||||||
Mac hmacHasher = null;
|
|
||||||
try {
|
|
||||||
hmacHasher = Mac.getInstance("hmacSHA256");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
hmacHasher.init(makeHMACKey(key));
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return hmacHasher;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Hash bytes with given hasher
|
|
||||||
* Input: message to hash, HMAC hasher
|
|
||||||
* Output: hashed byte[].
|
|
||||||
*/
|
|
||||||
public static byte[] digestBytes(byte[] message, Mac hasher) {
|
|
||||||
hasher.update(message);
|
|
||||||
byte[] ret = hasher.doFinal();
|
|
||||||
hasher.reset();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,8 @@ import javax.crypto.Mac;
|
|||||||
|
|
||||||
import org.mozilla.apache.commons.codec.binary.Base64;
|
import org.mozilla.apache.commons.codec.binary.Base64;
|
||||||
import org.mozilla.gecko.sync.Utils;
|
import org.mozilla.gecko.sync.Utils;
|
||||||
|
import org.mozilla.gecko.sync.crypto.CryptoException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
|
||||||
public class KeyBundle {
|
public class KeyBundle {
|
||||||
|
|
||||||
@ -86,7 +88,7 @@ public class KeyBundle {
|
|||||||
* encryption key and the second iteration the HMAC key.
|
* encryption key and the second iteration the HMAC key.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public KeyBundle(String username, String base32SyncKey) {
|
public KeyBundle(String username, String base32SyncKey) throws CryptoException {
|
||||||
if (base32SyncKey == null) {
|
if (base32SyncKey == null) {
|
||||||
throw new IllegalArgumentException("No sync key provided.");
|
throw new IllegalArgumentException("No sync key provided.");
|
||||||
}
|
}
|
||||||
@ -105,7 +107,15 @@ public class KeyBundle {
|
|||||||
byte[] syncKey = Utils.decodeFriendlyBase32(base32SyncKey);
|
byte[] syncKey = Utils.decodeFriendlyBase32(base32SyncKey);
|
||||||
byte[] user = username.getBytes();
|
byte[] user = username.getBytes();
|
||||||
|
|
||||||
Mac hmacHasher = HKDF.makeHMACHasher(syncKey);
|
Mac hmacHasher;
|
||||||
|
try {
|
||||||
|
hmacHasher = HKDF.makeHMACHasher(syncKey);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new CryptoException(e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new CryptoException(e);
|
||||||
|
}
|
||||||
|
assert(hmacHasher != null); // If makeHMACHasher doesn't throw, then hmacHasher is non-null.
|
||||||
|
|
||||||
byte[] encrBytes = Utils.concatAll(EMPTY_BYTES, HKDF.HMAC_INPUT, user, ENCR_INPUT_BYTES);
|
byte[] encrBytes = Utils.concatAll(EMPTY_BYTES, HKDF.HMAC_INPUT, user, ENCR_INPUT_BYTES);
|
||||||
byte[] encrKey = HKDF.digestBytes(encrBytes, hmacHasher);
|
byte[] encrKey = HKDF.digestBytes(encrBytes, hmacHasher);
|
||||||
|
@ -57,6 +57,7 @@ import org.mozilla.gecko.sync.crypto.CryptoInfo;
|
|||||||
import org.mozilla.gecko.sync.crypto.Cryptographer;
|
import org.mozilla.gecko.sync.crypto.Cryptographer;
|
||||||
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
||||||
import org.mozilla.gecko.sync.cryptographer.CryptoStatusBundle.CryptoStatus;
|
import org.mozilla.gecko.sync.cryptographer.CryptoStatusBundle.CryptoStatus;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This class acts as a wrapper for the Cryptographer class.
|
* This class acts as a wrapper for the Cryptographer class.
|
||||||
@ -192,6 +193,10 @@ public class SyncCryptographer {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json == null) {
|
||||||
|
throw new CryptoException(new GeneralSecurityException("Could not decrypt JSON payload"));
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that this is indeed the crypto/keys bundle and that
|
// Verify that this is indeed the crypto/keys bundle and that
|
||||||
// decryption worked.
|
// decryption worked.
|
||||||
String id = (String) json.get(KEY_ID);
|
String id = (String) json.get(KEY_ID);
|
||||||
@ -339,7 +344,7 @@ public class SyncCryptographer {
|
|||||||
/*
|
/*
|
||||||
* Get the keys needed to encrypt the crypto/keys bundle.
|
* Get the keys needed to encrypt the crypto/keys bundle.
|
||||||
*/
|
*/
|
||||||
public KeyBundle getCryptoKeysBundleKeys() {
|
public KeyBundle getCryptoKeysBundleKeys() throws CryptoException {
|
||||||
return new KeyBundle(username, syncKey);
|
return new KeyBundle(username, syncKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +74,8 @@ import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
|
|||||||
import ch.boye.httpclientandroidlib.entity.StringEntity;
|
import ch.boye.httpclientandroidlib.entity.StringEntity;
|
||||||
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
|
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
|
||||||
import ch.boye.httpclientandroidlib.message.BasicHeader;
|
import ch.boye.httpclientandroidlib.message.BasicHeader;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
|
||||||
public class JPakeClient implements JPakeRequestDelegate {
|
public class JPakeClient implements JPakeRequestDelegate {
|
||||||
private static String LOG_TAG = "JPakeClient";
|
private static String LOG_TAG = "JPakeClient";
|
||||||
@ -474,6 +476,14 @@ public class JPakeClient implements JPakeRequestDelegate {
|
|||||||
Log.e(LOG_TAG, "ZKP mismatch");
|
Log.e(LOG_TAG, "ZKP mismatch");
|
||||||
abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
abort(Constants.JPAKE_ERROR_WRONGMESSAGE);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
Log.e(LOG_TAG, "NoSuchAlgorithmException", e);
|
||||||
|
abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
Log.e(LOG_TAG, "InvalidKeyException", e);
|
||||||
|
abort(Constants.JPAKE_ERROR_INTERNAL);
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pairWithPin) { // Wait for other device to send verification of keys.
|
if (pairWithPin) { // Wait for other device to send verification of keys.
|
||||||
|
@ -51,6 +51,7 @@ import org.mozilla.gecko.sync.crypto.HKDF;
|
|||||||
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
import org.mozilla.gecko.sync.crypto.KeyBundle;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
|
||||||
public class JPakeCrypto {
|
public class JPakeCrypto {
|
||||||
private static final String LOG_TAG = "JPakeCrypto";
|
private static final String LOG_TAG = "JPakeCrypto";
|
||||||
@ -174,7 +175,7 @@ public class JPakeCrypto {
|
|||||||
* @throws IncorrectZkpException
|
* @throws IncorrectZkpException
|
||||||
*/
|
*/
|
||||||
public static KeyBundle finalRound(String secret, JPakeParty jp)
|
public static KeyBundle finalRound(String secret, JPakeParty jp)
|
||||||
throws IncorrectZkpException {
|
throws IncorrectZkpException, NoSuchAlgorithmException, InvalidKeyException {
|
||||||
Log.d(LOG_TAG, "Final round started.");
|
Log.d(LOG_TAG, "Final round started.");
|
||||||
BigInteger gb = jp.gx1.multiply(jp.gx2).mod(P).multiply(jp.gx3)
|
BigInteger gb = jp.gx1.multiply(jp.gx2).mod(P).multiply(jp.gx3)
|
||||||
.mod(P);
|
.mod(P);
|
||||||
@ -321,12 +322,12 @@ public class JPakeCrypto {
|
|||||||
/*
|
/*
|
||||||
* Helper function to generate encryption key and HMAC from a byte array.
|
* Helper function to generate encryption key and HMAC from a byte array.
|
||||||
*/
|
*/
|
||||||
public static void generateKeyAndHmac(BigInteger k, byte[] encOut, byte[] hmacOut) {
|
public static void generateKeyAndHmac(BigInteger k, byte[] encOut, byte[] hmacOut) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
// Generate HMAC and Encryption keys from synckey.
|
// Generate HMAC and Encryption keys from synckey.
|
||||||
byte[] zerokey = new byte[32];
|
byte[] zerokey = new byte[32];
|
||||||
byte[] prk = HMACSHA256(BigIntegerHelper.BigIntegerToByteArrayWithoutSign(k), zerokey);
|
byte[] prk = HMACSHA256(BigIntegerHelper.BigIntegerToByteArrayWithoutSign(k), zerokey);
|
||||||
|
|
||||||
byte[] okm = HKDF.hkdfExpand(prk, HKDF.HMAC_INPUT, 32 * 2);
|
byte[] okm = HKDF.hkdfExpand(prk, HKDF.HMAC_INPUT, 32 * 2);
|
||||||
System.arraycopy(okm, 0, encOut, 0, 32);
|
System.arraycopy(okm, 0, encOut, 0, 32);
|
||||||
System.arraycopy(okm, 32, hmacOut, 0, 32);
|
System.arraycopy(okm, 32, hmacOut, 0, 32);
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ public class BaseResource implements Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public BaseResource(String uri, boolean rewrite) throws URISyntaxException {
|
public BaseResource(String uri, boolean rewrite) throws URISyntaxException {
|
||||||
this(new URI(uri), rewrite);
|
this(new URI(uri), rewrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseResource(URI uri, boolean rewrite) {
|
public BaseResource(URI uri, boolean rewrite) {
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
*
|
*
|
||||||
* Contributor(s):
|
* Contributor(s):
|
||||||
* Richard Newman <rnewman@mozilla.com>
|
* Richard Newman <rnewman@mozilla.com>
|
||||||
|
* Nick Alexander <nalexander@mozilla.com>
|
||||||
*
|
*
|
||||||
* Alternatively, the contents of this file may be used under the terms of
|
* 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
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
@ -47,11 +48,16 @@ import org.mozilla.gecko.sync.ExtendedJSONObject;
|
|||||||
import org.mozilla.gecko.sync.NonObjectJSONException;
|
import org.mozilla.gecko.sync.NonObjectJSONException;
|
||||||
import org.mozilla.gecko.sync.Utils;
|
import org.mozilla.gecko.sync.Utils;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
import ch.boye.httpclientandroidlib.Header;
|
import ch.boye.httpclientandroidlib.Header;
|
||||||
import ch.boye.httpclientandroidlib.HttpEntity;
|
import ch.boye.httpclientandroidlib.HttpEntity;
|
||||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||||
|
import ch.boye.httpclientandroidlib.impl.cookie.DateParseException;
|
||||||
|
import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
|
||||||
|
|
||||||
public class SyncResponse {
|
public class SyncResponse {
|
||||||
|
private static final String HEADER_RETRY_AFTER = "retry-after";
|
||||||
|
private static final String LOG_TAG = "SyncResponse";
|
||||||
|
|
||||||
protected HttpResponse response;
|
protected HttpResponse response;
|
||||||
|
|
||||||
@ -123,10 +129,20 @@ public class SyncResponse {
|
|||||||
return this.response.containsHeader(h);
|
return this.response.containsHeader(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getIntegerHeader(String h) {
|
private static boolean missingHeader(String value) {
|
||||||
|
return value == null ||
|
||||||
|
value.trim().length() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getIntegerHeader(String h) throws NumberFormatException {
|
||||||
if (this.hasHeader(h)) {
|
if (this.hasHeader(h)) {
|
||||||
Header header = this.response.getFirstHeader(h);
|
Header header = this.response.getFirstHeader(h);
|
||||||
return Integer.parseInt(header.getValue(), 10);
|
String value = header.getValue();
|
||||||
|
if (missingHeader(value)) {
|
||||||
|
Log.w(LOG_TAG, h + " header present but empty.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return Integer.parseInt(value, 10);
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -135,7 +151,31 @@ public class SyncResponse {
|
|||||||
* @return A number of seconds, or -1 if the header was not present.
|
* @return A number of seconds, or -1 if the header was not present.
|
||||||
*/
|
*/
|
||||||
public int retryAfter() throws NumberFormatException {
|
public int retryAfter() throws NumberFormatException {
|
||||||
return this.getIntegerHeader("retry-after");
|
if (!this.hasHeader(HEADER_RETRY_AFTER)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Header header = this.response.getFirstHeader(HEADER_RETRY_AFTER);
|
||||||
|
String retryAfter = header.getValue();
|
||||||
|
if (missingHeader(retryAfter)) {
|
||||||
|
Log.w(LOG_TAG, "Retry-After header present but empty.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(retryAfter, 10);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Fall through to try date format.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final long then = DateUtils.parseDate(retryAfter).getTime();
|
||||||
|
final long now = System.currentTimeMillis();
|
||||||
|
return (int)((then - now) / 1000); // Convert milliseconds to seconds.
|
||||||
|
} catch (DateParseException e) {
|
||||||
|
Log.w(LOG_TAG, "Retry-After header neither integer nor date: " + retryAfter);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int weaveBackoff() throws NumberFormatException {
|
public int weaveBackoff() throws NumberFormatException {
|
||||||
@ -174,5 +214,4 @@ public class SyncResponse {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -98,6 +98,12 @@ public class SyncStorageCollectionRequest extends SyncStorageRequest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: at this point we can access X-Weave-Timestamp, compare
|
||||||
|
// that to our local timestamp, and compute an estimate of clock
|
||||||
|
// skew. We can provide this to the incremental delegate, which
|
||||||
|
// will allow it to seamlessly correct timestamps on the records
|
||||||
|
// it processes. Bug 721887.
|
||||||
|
|
||||||
// Line-by-line processing, then invoke success.
|
// Line-by-line processing, then invoke success.
|
||||||
SyncStorageCollectionRequestDelegate delegate = (SyncStorageCollectionRequestDelegate) this.request.delegate;
|
SyncStorageCollectionRequestDelegate delegate = (SyncStorageCollectionRequestDelegate) this.request.delegate;
|
||||||
InputStream content = null;
|
InputStream content = null;
|
||||||
|
@ -173,7 +173,7 @@ public class SyncStorageRequest implements Resource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String USER_AGENT = "Firefox AndroidSync 0.3";
|
public static String USER_AGENT = "Firefox AndroidSync 0.4";
|
||||||
protected SyncResourceDelegate resourceDelegate;
|
protected SyncResourceDelegate resourceDelegate;
|
||||||
public SyncStorageRequestDelegate delegate;
|
public SyncStorageRequestDelegate delegate;
|
||||||
protected BaseResource resource;
|
protected BaseResource resource;
|
||||||
|
@ -40,6 +40,10 @@ package org.mozilla.gecko.sync.net;
|
|||||||
public interface SyncStorageRequestDelegate {
|
public interface SyncStorageRequestDelegate {
|
||||||
String credentials();
|
String credentials();
|
||||||
String ifUnmodifiedSince();
|
String ifUnmodifiedSince();
|
||||||
|
|
||||||
|
// TODO: at this point we can access X-Weave-Timestamp, compare
|
||||||
|
// that to our local timestamp, and compute an estimate of clock
|
||||||
|
// skew. Bug 721887.
|
||||||
void handleRequestSuccess(SyncStorageResponse response);
|
void handleRequestSuccess(SyncStorageResponse response);
|
||||||
void handleRequestFailure(SyncStorageResponse response);
|
void handleRequestFailure(SyncStorageResponse response);
|
||||||
void handleRequestError(Exception ex);
|
void handleRequestError(Exception ex);
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
/* 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;
|
||||||
|
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.sync.CredentialsSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A kind of Server11Repository that supports explicit setting of limit and sort on operations.
|
||||||
|
*
|
||||||
|
* @author rnewman
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class ConstrainedServer11Repository extends Server11Repository {
|
||||||
|
|
||||||
|
private String sort = null;
|
||||||
|
private long limit = -1;
|
||||||
|
|
||||||
|
public ConstrainedServer11Repository(String serverURI, String username, String collection, CredentialsSource credentialsSource, long limit, String sort) throws URISyntaxException {
|
||||||
|
super(serverURI, username, collection, credentialsSource);
|
||||||
|
|
||||||
|
this.limit = limit;
|
||||||
|
this.sort = sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getDefaultSort() {
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long getDefaultFetchLimit() {
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/* 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;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.sync.repositories.domain.Record;
|
||||||
|
|
||||||
|
public class HashSetStoreTracker implements StoreTracker {
|
||||||
|
|
||||||
|
// Guarded by `this`.
|
||||||
|
// Used to store GUIDs that were not locally modified but
|
||||||
|
// have been modified by a call to `store`, and thus
|
||||||
|
// should not be returned by a subsequent fetch.
|
||||||
|
private HashSet<String> guids;
|
||||||
|
|
||||||
|
public HashSetStoreTracker() {
|
||||||
|
guids = new HashSet<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "#<Tracker: " + guids.size() + " guids tracked.>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean trackRecordForExclusion(String guid) {
|
||||||
|
return (guid != null) && guids.add(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean isTrackedForExclusion(String guid) {
|
||||||
|
return (guid != null) && guids.contains(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean untrackStoredForExclusion(String guid) {
|
||||||
|
return (guid != null) && guids.remove(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecordFilter getFilter() {
|
||||||
|
if (guids.size() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new RecordFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean excludeRecord(Record r) {
|
||||||
|
return isTrackedForExclusion(r.guid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
11
mobile/android/base/sync/repositories/RecordFilter.java
Normal file
11
mobile/android/base/sync/repositories/RecordFilter.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/* 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;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.sync.repositories.domain.Record;
|
||||||
|
|
||||||
|
public interface RecordFilter {
|
||||||
|
public boolean excludeRecord(Record r);
|
||||||
|
}
|
@ -41,6 +41,7 @@ package org.mozilla.gecko.sync.repositories;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.sync.Utils;
|
||||||
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
|
||||||
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
|
||||||
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
|
||||||
@ -74,6 +75,15 @@ public abstract class RepositorySession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final String LOG_TAG = "RepositorySession";
|
private static final String LOG_TAG = "RepositorySession";
|
||||||
|
|
||||||
|
private static void error(String message) {
|
||||||
|
Utils.error(LOG_TAG, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void trace(String message) {
|
||||||
|
Utils.trace(LOG_TAG, message);
|
||||||
|
}
|
||||||
|
|
||||||
protected SessionStatus status = SessionStatus.UNSTARTED;
|
protected SessionStatus status = SessionStatus.UNSTARTED;
|
||||||
protected Repository repository;
|
protected Repository repository;
|
||||||
protected RepositorySessionStoreDelegate delegate;
|
protected RepositorySessionStoreDelegate delegate;
|
||||||
@ -163,11 +173,6 @@ public abstract class RepositorySession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void error(String msg) {
|
|
||||||
System.err.println("ERROR: " + msg);
|
|
||||||
Log.e(LOG_TAG, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously perform the shared work of beginning. Throws on failure.
|
* Synchronously perform the shared work of beginning. Throws on failure.
|
||||||
* @throws InvalidSessionTransitionException
|
* @throws InvalidSessionTransitionException
|
||||||
@ -251,4 +256,91 @@ public abstract class RepositorySession {
|
|||||||
storeWorkQueue.shutdown();
|
storeWorkQueue.shutdown();
|
||||||
delegateQueue.shutdown();
|
delegateQueue.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce a record that is some combination of the remote and local records
|
||||||
|
* provided.
|
||||||
|
*
|
||||||
|
* The returned record must be produced without mutating either remoteRecord
|
||||||
|
* or localRecord. It is acceptable to return either remoteRecord or localRecord
|
||||||
|
* if no modifications are to be propagated.
|
||||||
|
*
|
||||||
|
* The returned record *should* have the local androidID and the remote GUID,
|
||||||
|
* and some optional merge of data from the two records.
|
||||||
|
*
|
||||||
|
* This method can be called with records that are identical, or differ in
|
||||||
|
* any regard.
|
||||||
|
*
|
||||||
|
* This method will not be called if:
|
||||||
|
*
|
||||||
|
* * either record is marked as deleted, or
|
||||||
|
* * there is no local mapping for a new remote record.
|
||||||
|
*
|
||||||
|
* Otherwise, it will be called precisely once.
|
||||||
|
*
|
||||||
|
* Side-effects (e.g., for transactional storage) can be hooked in here.
|
||||||
|
*
|
||||||
|
* @param remoteRecord
|
||||||
|
* The record retrieved from upstream, already adjusted for clock skew.
|
||||||
|
* @param localRecord
|
||||||
|
* The record retrieved from local storage.
|
||||||
|
* @param lastRemoteRetrieval
|
||||||
|
* The timestamp of the last retrieved set of remote records, adjusted for
|
||||||
|
* clock skew.
|
||||||
|
* @param lastLocalRetrieval
|
||||||
|
* The timestamp of the last retrieved set of local records.
|
||||||
|
* @return
|
||||||
|
* A Record instance to apply, or null to apply nothing.
|
||||||
|
*/
|
||||||
|
protected Record reconcileRecords(final Record remoteRecord,
|
||||||
|
final Record localRecord,
|
||||||
|
final long lastRemoteRetrieval,
|
||||||
|
final long lastLocalRetrieval) {
|
||||||
|
Log.d(LOG_TAG, "Reconciling remote " + remoteRecord.guid + " against local " + localRecord.guid);
|
||||||
|
|
||||||
|
if (localRecord.equalPayloads(remoteRecord)) {
|
||||||
|
if (remoteRecord.lastModified > localRecord.lastModified) {
|
||||||
|
Log.d(LOG_TAG, "Records are equal. No record application needed.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local wins.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Decide what to do based on:
|
||||||
|
// * Which of the two records is modified;
|
||||||
|
// * Whether they are equal or congruent;
|
||||||
|
// * The modified times of each record (interpreted through the lens of clock skew);
|
||||||
|
// * ...
|
||||||
|
boolean localIsMoreRecent = localRecord.lastModified > remoteRecord.lastModified;
|
||||||
|
Log.d(LOG_TAG, "Local record is more recent? " + localIsMoreRecent);
|
||||||
|
Record donor = localIsMoreRecent ? localRecord : remoteRecord;
|
||||||
|
|
||||||
|
// Modify the local record to match the remote record's GUID and values.
|
||||||
|
// Preserve the local Android ID, and merge data where possible.
|
||||||
|
// It sure would be nice if copyWithIDs didn't give a shit about androidID, mm?
|
||||||
|
Record out = donor.copyWithIDs(remoteRecord.guid, localRecord.androidID);
|
||||||
|
|
||||||
|
// We don't want to upload the record if the remote record was
|
||||||
|
// applied without changes.
|
||||||
|
// This logic will become more complicated as reconciling becomes smarter.
|
||||||
|
if (!localIsMoreRecent) {
|
||||||
|
trackRecord(out);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on the RepositorySession implementation, track
|
||||||
|
* that a record — most likely a brand-new record that has been
|
||||||
|
* applied unmodified — should be tracked so as to not be uploaded
|
||||||
|
* redundantly.
|
||||||
|
*
|
||||||
|
* The default implementation does nothing.
|
||||||
|
*
|
||||||
|
* @param record
|
||||||
|
*/
|
||||||
|
protected synchronized void trackRecord(Record record) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ package org.mozilla.gecko.sync.repositories;
|
|||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import org.mozilla.gecko.sync.CredentialsSource;
|
import org.mozilla.gecko.sync.CredentialsSource;
|
||||||
import org.mozilla.gecko.sync.Utils;
|
import org.mozilla.gecko.sync.Utils;
|
||||||
@ -92,32 +93,50 @@ public class Server11Repository extends Repository {
|
|||||||
return this.collectionPathURI;
|
return this.collectionPathURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
public URI collectionURI(boolean full, long newer, String ids) throws URISyntaxException {
|
public URI collectionURI(boolean full, long newer, long limit, String sort, String ids) throws URISyntaxException {
|
||||||
// Do it this way to make it easier to add more params later.
|
ArrayList<String> params = new ArrayList<String>();
|
||||||
// It's pretty ugly, I'll grant.
|
if (full) {
|
||||||
// I can't believe Java doesn't have a good way to do this.
|
params.add("full=1");
|
||||||
boolean anyParams = full;
|
|
||||||
String uriParams = "";
|
|
||||||
if (anyParams) {
|
|
||||||
StringBuilder params = new StringBuilder("?");
|
|
||||||
if (full) {
|
|
||||||
params.append("full=1");
|
|
||||||
}
|
|
||||||
if (newer >= 0) {
|
|
||||||
// Translate local millisecond timestamps into server decimal seconds.
|
|
||||||
String newerString = Utils.millisecondsToDecimalSecondsString(newer);
|
|
||||||
params.append((full ? "&newer=" : "newer=") + newerString);
|
|
||||||
}
|
|
||||||
if (ids != null) {
|
|
||||||
params.append(((full || newer >= 0) ? "&ids=" : "ids=") + ids);
|
|
||||||
}
|
|
||||||
uriParams = params.toString();
|
|
||||||
}
|
}
|
||||||
String uri = this.collectionPath + uriParams;
|
if (newer >= 0) {
|
||||||
|
// Translate local millisecond timestamps into server decimal seconds.
|
||||||
|
String newerString = Utils.millisecondsToDecimalSecondsString(newer);
|
||||||
|
params.add("newer=" + newerString);
|
||||||
|
}
|
||||||
|
if (limit > 0) {
|
||||||
|
params.add("limit=" + limit);
|
||||||
|
}
|
||||||
|
if (sort != null) {
|
||||||
|
params.add("sort=" + sort); // We trust these values.
|
||||||
|
}
|
||||||
|
if (ids != null) {
|
||||||
|
params.add("ids=" + ids); // We trust these values.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.size() == 0) {
|
||||||
|
return this.collectionPathURI;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
char indicator = '?';
|
||||||
|
for (String param : params) {
|
||||||
|
out.append(indicator);
|
||||||
|
indicator = '&';
|
||||||
|
out.append(param);
|
||||||
|
}
|
||||||
|
String uri = this.collectionPath + out.toString();
|
||||||
return new URI(uri);
|
return new URI(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public URI wboURI(String id) throws URISyntaxException {
|
public URI wboURI(String id) throws URISyntaxException {
|
||||||
return new URI(this.collectionPath + "/" + id);
|
return new URI(this.collectionPath + "/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override these.
|
||||||
|
protected long getDefaultFetchLimit() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
protected String getDefaultSort() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,21 +214,34 @@ public class Server11RepositorySession extends RepositorySession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void fetchWithParameters(long newer,
|
protected void fetchWithParameters(long newer,
|
||||||
|
long limit,
|
||||||
boolean full,
|
boolean full,
|
||||||
|
String sort,
|
||||||
String ids,
|
String ids,
|
||||||
SyncStorageRequestDelegate delegate) throws URISyntaxException {
|
SyncStorageRequestDelegate delegate)
|
||||||
|
throws URISyntaxException {
|
||||||
|
|
||||||
URI collectionURI = serverRepository.collectionURI(full, newer, ids);
|
URI collectionURI = serverRepository.collectionURI(full, newer, limit, sort, ids);
|
||||||
SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(collectionURI);
|
SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(collectionURI);
|
||||||
request.delegate = delegate;
|
request.delegate = delegate;
|
||||||
request.get();
|
request.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void fetchSince(long timestamp, long limit, String sort, RepositorySessionFetchRecordsDelegate delegate) {
|
||||||
|
try {
|
||||||
|
this.fetchWithParameters(timestamp, limit, true, sort, null, new RequestFetchDelegateAdapter(delegate));
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
delegate.onFetchFailed(e, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void fetchSince(long timestamp,
|
public void fetchSince(long timestamp,
|
||||||
RepositorySessionFetchRecordsDelegate delegate) {
|
RepositorySessionFetchRecordsDelegate delegate) {
|
||||||
try {
|
try {
|
||||||
this.fetchWithParameters(timestamp, true, null, new RequestFetchDelegateAdapter(delegate));
|
long limit = serverRepository.getDefaultFetchLimit();
|
||||||
|
String sort = serverRepository.getDefaultSort();
|
||||||
|
this.fetchWithParameters(timestamp, limit, true, sort, null, new RequestFetchDelegateAdapter(delegate));
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
delegate.onFetchFailed(e, null);
|
delegate.onFetchFailed(e, null);
|
||||||
}
|
}
|
||||||
@ -245,7 +258,7 @@ public class Server11RepositorySession extends RepositorySession {
|
|||||||
// TODO: watch out for URL length limits!
|
// TODO: watch out for URL length limits!
|
||||||
try {
|
try {
|
||||||
String ids = flattenIDs(guids);
|
String ids = flattenIDs(guids);
|
||||||
this.fetchWithParameters(-1, true, ids, new RequestFetchDelegateAdapter(delegate));
|
this.fetchWithParameters(-1, -1, true, "index", ids, new RequestFetchDelegateAdapter(delegate));
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
delegate.onFetchFailed(e, null);
|
delegate.onFetchFailed(e, null);
|
||||||
}
|
}
|
||||||
|
78
mobile/android/base/sync/repositories/StoreTracker.java
Normal file
78
mobile/android/base/sync/repositories/StoreTracker.java
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our hacky version of transactional semantics. The goal is to prevent
|
||||||
|
* the following situation:
|
||||||
|
*
|
||||||
|
* * AAA is not modified locally.
|
||||||
|
* * A modified AAA is downloaded during the storing phase. Its local
|
||||||
|
* timestamp is advanced.
|
||||||
|
* * The direction of syncing changes, and AAA is now uploaded to the server.
|
||||||
|
*
|
||||||
|
* The following situation should still be supported:
|
||||||
|
*
|
||||||
|
* * AAA is not modified locally.
|
||||||
|
* * A modified AAA is downloaded and merged with the local AAA.
|
||||||
|
* * The merged AAA is uploaded to the server.
|
||||||
|
*
|
||||||
|
* As should:
|
||||||
|
*
|
||||||
|
* * AAA is modified locally.
|
||||||
|
* * A modified AAA is downloaded, and discarded or merged.
|
||||||
|
* * The current version of AAA is uploaded to the server.
|
||||||
|
*
|
||||||
|
* We achieve this by tracking GUIDs during the storing phase. If we
|
||||||
|
* apply a record such that the local copy is substantially the same
|
||||||
|
* as the record we just downloaded, we add it to a list of records
|
||||||
|
* to avoid uploading. The definition of "substantially the same"
|
||||||
|
* depends on the particular repository. The only consideration is "do we
|
||||||
|
* want to upload this record in this sync?".
|
||||||
|
*
|
||||||
|
* Note that items are removed from this list when a fetch that
|
||||||
|
* considers them for upload completes successfully. The entire list
|
||||||
|
* is discarded when the session is completed.
|
||||||
|
*
|
||||||
|
* This interface exposes methods to:
|
||||||
|
*
|
||||||
|
* * During a store, recording that a record has been stored, and should
|
||||||
|
* thus not be returned in subsequent fetches;
|
||||||
|
* * During a fetch, checking whether a record should be returned.
|
||||||
|
*
|
||||||
|
* In the future this might also grow self-persistence.
|
||||||
|
*
|
||||||
|
* See also RepositorySession.trackRecord.
|
||||||
|
*
|
||||||
|
* @author rnewman
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface StoreTracker {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param guid
|
||||||
|
* The GUID of the item to track.
|
||||||
|
* @return
|
||||||
|
* Whether the GUID was a newly tracked value.
|
||||||
|
*/
|
||||||
|
public boolean trackRecordForExclusion(String guid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param guid
|
||||||
|
* The GUID of the item to check.
|
||||||
|
* @return
|
||||||
|
* true if the item is already tracked.
|
||||||
|
*/
|
||||||
|
public boolean isTrackedForExclusion(String guid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param guid
|
||||||
|
* @return true if the specified GUID was removed from the tracked set.
|
||||||
|
*/
|
||||||
|
public boolean untrackStoredForExclusion(String guid);
|
||||||
|
|
||||||
|
public RecordFilter getFilter();
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/* 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;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
|
||||||
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
|
||||||
|
import org.mozilla.gecko.sync.repositories.domain.Record;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public abstract class StoreTrackingRepositorySession extends RepositorySession {
|
||||||
|
private static final String LOG_TAG = "StoreTrackingRepositorySession";
|
||||||
|
protected StoreTracker storeTracker;
|
||||||
|
|
||||||
|
protected static StoreTracker createStoreTracker() {
|
||||||
|
return new HashSetStoreTracker();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoreTrackingRepositorySession(Repository repository) {
|
||||||
|
super(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void begin(RepositorySessionBeginDelegate delegate) {
|
||||||
|
RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue);
|
||||||
|
try {
|
||||||
|
super.sharedBegin();
|
||||||
|
} catch (InvalidSessionTransitionException e) {
|
||||||
|
deferredDelegate.onBeginFailed(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Or do this in your own subclass.
|
||||||
|
storeTracker = createStoreTracker();
|
||||||
|
deferredDelegate.onBeginSucceeded(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected synchronized void trackRecord(Record record) {
|
||||||
|
if (this.storeTracker == null) {
|
||||||
|
throw new IllegalStateException("Store tracker not yet initialized!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(LOG_TAG, "Tracking record " + record.guid +
|
||||||
|
" (" + record.lastModified + ") to avoid re-upload.");
|
||||||
|
// Future: we care about the timestamp…
|
||||||
|
this.storeTracker.trackRecordForExclusion(record.guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abort(RepositorySessionFinishDelegate delegate) {
|
||||||
|
this.storeTracker = null;
|
||||||
|
super.abort(delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish(RepositorySessionFinishDelegate delegate) {
|
||||||
|
this.storeTracker = null;
|
||||||
|
super.finish(delegate);
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,7 @@ import android.content.Context;
|
|||||||
|
|
||||||
public class AndroidBrowserBookmarksRepository extends AndroidBrowserRepository implements BookmarksRepository {
|
public class AndroidBrowserBookmarksRepository extends AndroidBrowserRepository implements BookmarksRepository {
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
|
protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
|
||||||
AndroidBrowserBookmarksRepositorySession session = new AndroidBrowserBookmarksRepositorySession(AndroidBrowserBookmarksRepository.this, context);
|
AndroidBrowserBookmarksRepositorySession session = new AndroidBrowserBookmarksRepositorySession(AndroidBrowserBookmarksRepository.this, context);
|
||||||
delegate.onSessionCreated(session);
|
delegate.onSessionCreated(session);
|
||||||
|
@ -56,7 +56,6 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
|
||||||
public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepositorySession {
|
public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepositorySession {
|
||||||
|
|
||||||
// TODO: synchronization for these.
|
// TODO: synchronization for these.
|
||||||
@ -68,12 +67,6 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
|
|||||||
private AndroidBrowserBookmarksDataAccessor dataAccessor;
|
private AndroidBrowserBookmarksDataAccessor dataAccessor;
|
||||||
private int needsReparenting = 0;
|
private int needsReparenting = 0;
|
||||||
|
|
||||||
private static void trace(String string) {
|
|
||||||
if (Utils.ENABLE_TRACE_LOGGING) {
|
|
||||||
Log.d(LOG_TAG, string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if the provided record GUID should be skipped
|
* Return true if the provided record GUID should be skipped
|
||||||
* in child lists or fetch results.
|
* in child lists or fetch results.
|
||||||
@ -314,10 +307,9 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
|
|||||||
super.finish(delegate);
|
super.finish(delegate);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO this code is yucky, cleanup or comment or something
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
@Override
|
||||||
protected long insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
@SuppressWarnings("unchecked")
|
||||||
|
protected Record prepareRecord(Record record) {
|
||||||
BookmarkRecord bmk = (BookmarkRecord) record;
|
BookmarkRecord bmk = (BookmarkRecord) record;
|
||||||
|
|
||||||
// Check if parent exists
|
// Check if parent exists
|
||||||
@ -356,16 +348,21 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
|
|||||||
" (" + bmk.parentID + ", " + bmk.parentName +
|
" (" + bmk.parentID + ", " + bmk.parentName +
|
||||||
", " + bmk.pos + ")");
|
", " + bmk.pos + ")");
|
||||||
}
|
}
|
||||||
long id = RepoUtils.getAndroidIdFromUri(dbHelper.insert(bmk));
|
return bmk;
|
||||||
Log.d(LOG_TAG, "Inserted as " + id);
|
}
|
||||||
|
|
||||||
putRecordToGuidMap(buildRecordString(bmk), bmk.guid);
|
@Override
|
||||||
bmk.androidID = id;
|
@SuppressWarnings("unchecked")
|
||||||
|
protected void updateBookkeeping(Record record) throws NoGuidForIdException,
|
||||||
|
NullCursorException,
|
||||||
|
ParentNotFoundException {
|
||||||
|
super.updateBookkeeping(record);
|
||||||
|
BookmarkRecord bmk = (BookmarkRecord) record;
|
||||||
|
|
||||||
// If record is folder, update maps and re-parent children if necessary
|
// If record is folder, update maps and re-parent children if necessary
|
||||||
if (bmk.type.equalsIgnoreCase(AndroidBrowserBookmarksDataAccessor.TYPE_FOLDER)) {
|
if (bmk.type.equalsIgnoreCase(AndroidBrowserBookmarksDataAccessor.TYPE_FOLDER)) {
|
||||||
guidToID.put(bmk.guid, id);
|
guidToID.put(bmk.guid, bmk.androidID);
|
||||||
idToGuid.put(id, bmk.guid);
|
idToGuid.put(bmk.androidID, bmk.guid);
|
||||||
|
|
||||||
JSONArray childArray = bmk.children;
|
JSONArray childArray = bmk.children;
|
||||||
|
|
||||||
@ -377,14 +374,13 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
|
|||||||
childArray.add(child);
|
childArray.add(child);
|
||||||
}
|
}
|
||||||
position = childArray.indexOf(child);
|
position = childArray.indexOf(child);
|
||||||
dataAccessor.updateParentAndPosition(child, id, position);
|
dataAccessor.updateParentAndPosition(child, bmk.androidID, position);
|
||||||
needsReparenting--;
|
needsReparenting--;
|
||||||
}
|
}
|
||||||
missingParentToChildren.remove(bmk.guid);
|
missingParentToChildren.remove(bmk.guid);
|
||||||
}
|
}
|
||||||
parentToChildArray.put(bmk.guid, childArray);
|
parentToChildArray.put(bmk.guid, childArray);
|
||||||
}
|
}
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -104,6 +104,16 @@ public class AndroidBrowserHistoryDataAccessor extends AndroidBrowserRepositoryD
|
|||||||
return super.insert(record);
|
return super.insert(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(String oldGUID, Record newRecord) {
|
||||||
|
HistoryRecord rec = (HistoryRecord) newRecord;
|
||||||
|
String newGUID = newRecord.guid;
|
||||||
|
Log.d(LOG_TAG, "Storing visits for " + newGUID + ", replacing " + oldGUID);
|
||||||
|
dataExtender.delete(oldGUID);
|
||||||
|
dataExtender.store(newGUID, rec.visits);
|
||||||
|
super.update(oldGUID, newRecord);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void delete(String guid) {
|
protected void delete(String guid) {
|
||||||
Log.d(LOG_TAG, "Deleting record " + guid);
|
Log.d(LOG_TAG, "Deleting record " + guid);
|
||||||
|
@ -135,4 +135,9 @@ public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserReposi
|
|||||||
hist.visits = visitsArray;
|
hist.visits = visitsArray;
|
||||||
return hist;
|
return hist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Record prepareRecord(Record record) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,4 +63,8 @@ public class AndroidBrowserPasswordsRepositorySession extends
|
|||||||
return rec.hostname + rec.formSubmitURL + rec.httpRealm + rec.username;
|
return rec.hostname + rec.formSubmitURL + rec.httpRealm + rec.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Record prepareRecord(Record record) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,8 +101,19 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
|
|||||||
Log.w(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
|
Log.w(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void update(String guid, Record newRecord) {
|
||||||
|
String where = BrowserContract.SyncColumns.GUID + " = ?";
|
||||||
|
String[] args = new String[] { guid };
|
||||||
|
ContentValues cv = getContentValues(newRecord);
|
||||||
|
int updated = context.getContentResolver().update(getUri(), cv, where, args);
|
||||||
|
if (updated != 1) {
|
||||||
|
Log.w(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Uri insert(Record record) {
|
public Uri insert(Record record) {
|
||||||
ContentValues cv = getContentValues(record);
|
ContentValues cv = getContentValues(record);
|
||||||
|
Log.d(LOG_TAG, "INSERTING: " + cv.getAsString("guid"));
|
||||||
return context.getContentResolver().insert(getUri(), cv);
|
return context.getContentResolver().insert(getUri(), cv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ package org.mozilla.gecko.sync.repositories.android;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.sync.Utils;
|
||||||
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
|
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
|
||||||
import org.mozilla.gecko.sync.repositories.InvalidRequestException;
|
import org.mozilla.gecko.sync.repositories.InvalidRequestException;
|
||||||
import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
|
import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
|
||||||
@ -50,8 +51,9 @@ import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
|
|||||||
import org.mozilla.gecko.sync.repositories.NullCursorException;
|
import org.mozilla.gecko.sync.repositories.NullCursorException;
|
||||||
import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
|
import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
|
||||||
import org.mozilla.gecko.sync.repositories.ProfileDatabaseException;
|
import org.mozilla.gecko.sync.repositories.ProfileDatabaseException;
|
||||||
|
import org.mozilla.gecko.sync.repositories.RecordFilter;
|
||||||
import org.mozilla.gecko.sync.repositories.Repository;
|
import org.mozilla.gecko.sync.repositories.Repository;
|
||||||
import org.mozilla.gecko.sync.repositories.RepositorySession;
|
import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
|
||||||
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
|
||||||
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
|
||||||
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
|
||||||
@ -59,6 +61,7 @@ import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelega
|
|||||||
import org.mozilla.gecko.sync.repositories.domain.Record;
|
import org.mozilla.gecko.sync.repositories.domain.Record;
|
||||||
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,10 +86,10 @@ import android.util.Log;
|
|||||||
* @author rnewman
|
* @author rnewman
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public abstract class AndroidBrowserRepositorySession extends RepositorySession {
|
public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepositorySession {
|
||||||
|
|
||||||
protected AndroidBrowserRepositoryDataAccessor dbHelper;
|
protected AndroidBrowserRepositoryDataAccessor dbHelper;
|
||||||
protected static final String LOG_TAG = "AndroidBrowserRepositorySession";
|
public static final String LOG_TAG = "AndroidBrowserRepositorySession";
|
||||||
private HashMap<String, String> recordToGuid;
|
private HashMap<String, String> recordToGuid;
|
||||||
|
|
||||||
public AndroidBrowserRepositorySession(Repository repository) {
|
public AndroidBrowserRepositorySession(Repository repository) {
|
||||||
@ -149,15 +152,17 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
deferredDelegate.onBeginFailed(e);
|
deferredDelegate.onBeginFailed(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
storeTracker = createStoreTracker();
|
||||||
deferredDelegate.onBeginSucceeded(this);
|
deferredDelegate.onBeginSucceeded(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract String buildRecordString(Record record);
|
protected abstract String buildRecordString(Record record);
|
||||||
|
|
||||||
protected void checkDatabase() throws ProfileDatabaseException, NullCursorException {
|
protected void checkDatabase() throws ProfileDatabaseException, NullCursorException {
|
||||||
Log.i(LOG_TAG, "Checking database.");
|
Utils.info(LOG_TAG, "BEGIN: checking database.");
|
||||||
try {
|
try {
|
||||||
dbHelper.fetch(new String[] { "none" }).close();
|
dbHelper.fetch(new String[] { "none" }).close();
|
||||||
|
Utils.info(LOG_TAG, "END: checking database.");
|
||||||
} catch (NullPointerException e) {
|
} catch (NullPointerException e) {
|
||||||
throw new ProfileDatabaseException(e);
|
throw new ProfileDatabaseException(e);
|
||||||
}
|
}
|
||||||
@ -223,7 +228,7 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
@Override
|
@Override
|
||||||
public void fetch(String[] guids,
|
public void fetch(String[] guids,
|
||||||
RepositorySessionFetchRecordsDelegate delegate) {
|
RepositorySessionFetchRecordsDelegate delegate) {
|
||||||
FetchRunnable command = new FetchRunnable(guids, now(), delegate);
|
FetchRunnable command = new FetchRunnable(guids, now(), null, delegate);
|
||||||
delegateQueue.execute(command);
|
delegateQueue.execute(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,7 +239,7 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void fetchFromCursor(Cursor cursor, long end) {
|
protected void fetchFromCursor(Cursor cursor, RecordFilter filter, long end) {
|
||||||
Log.d(LOG_TAG, "Fetch from cursor:");
|
Log.d(LOG_TAG, "Fetch from cursor:");
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
@ -244,9 +249,13 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
}
|
}
|
||||||
while (!cursor.isAfterLast()) {
|
while (!cursor.isAfterLast()) {
|
||||||
Log.d(LOG_TAG, "... one more record.");
|
Log.d(LOG_TAG, "... one more record.");
|
||||||
Record r = transformRecord(recordFromMirrorCursor(cursor));
|
Record r = recordFromMirrorCursor(cursor);
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
delegate.onFetchedRecord(r);
|
if (filter == null || !filter.excludeRecord(r)) {
|
||||||
|
delegate.onFetchedRecord(transformRecord(r));
|
||||||
|
} else {
|
||||||
|
Log.d(LOG_TAG, "Filter says to skip record.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cursor.moveToNext();
|
cursor.moveToNext();
|
||||||
}
|
}
|
||||||
@ -269,13 +278,16 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
class FetchRunnable extends FetchingRunnable {
|
class FetchRunnable extends FetchingRunnable {
|
||||||
private String[] guids;
|
private String[] guids;
|
||||||
private long end;
|
private long end;
|
||||||
|
private RecordFilter filter;
|
||||||
|
|
||||||
public FetchRunnable(String[] guids,
|
public FetchRunnable(String[] guids,
|
||||||
long end,
|
long end,
|
||||||
|
RecordFilter filter,
|
||||||
RepositorySessionFetchRecordsDelegate delegate) {
|
RepositorySessionFetchRecordsDelegate delegate) {
|
||||||
super(delegate);
|
super(delegate);
|
||||||
this.guids = guids;
|
this.guids = guids;
|
||||||
this.end = end;
|
this.end = end;
|
||||||
|
this.filter = filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -293,7 +305,7 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Cursor cursor = dbHelper.fetch(guids);
|
Cursor cursor = dbHelper.fetch(guids);
|
||||||
this.fetchFromCursor(cursor, end);
|
this.fetchFromCursor(cursor, filter, end);
|
||||||
} catch (NullCursorException e) {
|
} catch (NullCursorException e) {
|
||||||
delegate.onFetchFailed(e, null);
|
delegate.onFetchFailed(e, null);
|
||||||
}
|
}
|
||||||
@ -303,21 +315,28 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
@Override
|
@Override
|
||||||
public void fetchSince(long timestamp,
|
public void fetchSince(long timestamp,
|
||||||
RepositorySessionFetchRecordsDelegate delegate) {
|
RepositorySessionFetchRecordsDelegate delegate) {
|
||||||
|
if (this.storeTracker == null) {
|
||||||
|
throw new IllegalStateException("Store tracker not yet initialized!");
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(LOG_TAG, "Running fetchSince(" + timestamp + ").");
|
Log.i(LOG_TAG, "Running fetchSince(" + timestamp + ").");
|
||||||
FetchSinceRunnable command = new FetchSinceRunnable(timestamp, now(), delegate);
|
FetchSinceRunnable command = new FetchSinceRunnable(timestamp, now(), this.storeTracker.getFilter(), delegate);
|
||||||
delegateQueue.execute(command);
|
delegateQueue.execute(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FetchSinceRunnable extends FetchingRunnable {
|
class FetchSinceRunnable extends FetchingRunnable {
|
||||||
private long since;
|
private long since;
|
||||||
private long end;
|
private long end;
|
||||||
|
private RecordFilter filter;
|
||||||
|
|
||||||
public FetchSinceRunnable(long since,
|
public FetchSinceRunnable(long since,
|
||||||
long end,
|
long end,
|
||||||
|
RecordFilter filter,
|
||||||
RepositorySessionFetchRecordsDelegate delegate) {
|
RepositorySessionFetchRecordsDelegate delegate) {
|
||||||
super(delegate);
|
super(delegate);
|
||||||
this.since = since;
|
this.since = since;
|
||||||
this.end = end;
|
this.end = end;
|
||||||
|
this.filter = filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -329,7 +348,7 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Cursor cursor = dbHelper.fetchSince(since);
|
Cursor cursor = dbHelper.fetchSince(since);
|
||||||
this.fetchFromCursor(cursor, end);
|
this.fetchFromCursor(cursor, filter, end);
|
||||||
} catch (NullCursorException e) {
|
} catch (NullCursorException e) {
|
||||||
delegate.onFetchFailed(e, null);
|
delegate.onFetchFailed(e, null);
|
||||||
return;
|
return;
|
||||||
@ -363,9 +382,11 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the record is a valid type
|
// Check that the record is a valid type.
|
||||||
// TODO Currently for bookmarks we only take care of folders
|
// Fennec only supports bookmarks and folders. All other types of records,
|
||||||
// and bookmarks, all other types are ignored and thrown away
|
// including livemarks and queries, are simply ignored.
|
||||||
|
// See Bug 708149. This might be resolved by Fennec changing its database
|
||||||
|
// schema, or by Sync storing non-applied records in its own private database.
|
||||||
if (!checkRecordType(record)) {
|
if (!checkRecordType(record)) {
|
||||||
Log.d(LOG_TAG, "Ignoring record " + record.guid + " due to unknown record type.");
|
Log.d(LOG_TAG, "Ignoring record " + record.guid + " due to unknown record type.");
|
||||||
|
|
||||||
@ -374,27 +395,101 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// TODO: rnewman 2012-01-13: read and improve this code.
|
// TODO: lift these into the session.
|
||||||
// TODO:
|
// Temporary: this matches prior syncing semantics, in which only
|
||||||
|
// the relationship between the local and remote record is considered.
|
||||||
|
// In the future we'll track these two timestamps and use them to
|
||||||
|
// determine which records have changed, and thus process incoming
|
||||||
|
// records more efficiently.
|
||||||
|
long lastLocalRetrieval = 0; // lastSyncTimestamp?
|
||||||
|
long lastRemoteRetrieval = 0; // TODO: adjust for clock skew.
|
||||||
|
boolean remotelyModified = record.lastModified > lastRemoteRetrieval;
|
||||||
|
|
||||||
Record existingRecord;
|
Record existingRecord;
|
||||||
try {
|
try {
|
||||||
existingRecord = findExistingRecord(record);
|
// GUID matching only: deleted records don't have a payload with which to search.
|
||||||
|
existingRecord = recordForGUID(record.guid);
|
||||||
// If the record is new and not deleted, store it
|
if (record.deleted) {
|
||||||
if (existingRecord == null && !record.deleted) {
|
if (existingRecord == null) {
|
||||||
record.androidID = insert(record);
|
// We're done. Don't bother with a callback. That can change later
|
||||||
} else if (existingRecord != null) {
|
// if we want it to.
|
||||||
|
trace("Incoming record " + record.guid + " is deleted, and no local version. Bye!");
|
||||||
dbHelper.delete(existingRecord);
|
return;
|
||||||
// Or clause: We won't store a remotely deleted record ever, but if it is marked deleted
|
|
||||||
// and our existing record has a newer timestamp, we will restore the existing record
|
|
||||||
if (!record.deleted || (record.deleted && existingRecord.lastModified > record.lastModified)) {
|
|
||||||
// Record exists already, need to figure out what to store
|
|
||||||
Record store = reconcileRecords(existingRecord, record);
|
|
||||||
record.androidID = insert(store);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingRecord.deleted) {
|
||||||
|
trace("Local record already deleted. Bye!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which one wins?
|
||||||
|
if (!remotelyModified) {
|
||||||
|
trace("Ignoring deleted record from the past.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
|
||||||
|
if (!locallyModified) {
|
||||||
|
trace("Remote modified, local not. Deleting.");
|
||||||
|
storeRecordDeletion(record);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace("Both local and remote records have been modified.");
|
||||||
|
if (record.lastModified > existingRecord.lastModified) {
|
||||||
|
trace("Remote is newer, and deleted. Deleting local.");
|
||||||
|
storeRecordDeletion(record);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace("Remote is older, local is not deleted. Ignoring.");
|
||||||
|
if (!locallyModified) {
|
||||||
|
Log.w(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!");
|
||||||
|
// Ensure that this is tracked for upload.
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
// End deletion logic.
|
||||||
|
|
||||||
|
// Now we're processing a non-deleted incoming record.
|
||||||
|
if (existingRecord == null) {
|
||||||
|
trace("Looking up match for record " + record.guid);
|
||||||
|
existingRecord = findExistingRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingRecord == null) {
|
||||||
|
// The record is new.
|
||||||
|
trace("No match. Inserting.");
|
||||||
|
Record inserted = insert(record);
|
||||||
|
trackRecord(inserted);
|
||||||
|
delegate.onRecordStoreSucceeded(inserted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found a local dupe.
|
||||||
|
trace("Incoming record " + record.guid + " dupes to local record " + existingRecord.guid);
|
||||||
|
|
||||||
|
// Populate more expensive fields prior to reconciling.
|
||||||
|
existingRecord = transformRecord(existingRecord);
|
||||||
|
Record toStore = reconcileRecords(record, existingRecord, lastRemoteRetrieval, lastLocalRetrieval);
|
||||||
|
|
||||||
|
if (toStore == null) {
|
||||||
|
Log.d(LOG_TAG, "Reconciling returned null. Not inserting a record.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: pass in timestamps?
|
||||||
|
Log.d(LOG_TAG, "Replacing " + existingRecord.guid + " with record " + toStore.guid);
|
||||||
|
Record replaced = replace(toStore, existingRecord);
|
||||||
|
|
||||||
|
// Note that we don't track records here; deciding that is the job
|
||||||
|
// of reconcileRecords.
|
||||||
|
Log.d(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
|
||||||
|
"(" + replaced.androidID + ")");
|
||||||
|
delegate.onRecordStoreSucceeded(replaced);
|
||||||
|
return;
|
||||||
|
|
||||||
} catch (MultipleRecordsForGuidException e) {
|
} catch (MultipleRecordsForGuidException e) {
|
||||||
Log.e(LOG_TAG, "Multiple records returned for given guid: " + record.guid);
|
Log.e(LOG_TAG, "Multiple records returned for given guid: " + record.guid);
|
||||||
delegate.onRecordStoreFailed(e);
|
delegate.onRecordStoreFailed(e);
|
||||||
@ -412,17 +507,38 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
delegate.onRecordStoreFailed(e);
|
delegate.onRecordStoreFailed(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke callback with result.
|
|
||||||
delegate.onRecordStoreSucceeded(record);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
storeWorkQueue.execute(command);
|
storeWorkQueue.execute(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected long insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
protected void storeRecordDeletion(final Record record) {
|
||||||
putRecordToGuidMap(buildRecordString(record), record.guid);
|
// TODO: we ought to mark the record as deleted rather than deleting it,
|
||||||
return RepoUtils.getAndroidIdFromUri(dbHelper.insert(record));
|
// in order to support syncing to multiple destinations. Bug 722607.
|
||||||
|
dbHelper.delete(record); // TODO: mm?
|
||||||
|
delegate.onRecordStoreSucceeded(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Record insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
||||||
|
Record toStore = prepareRecord(record);
|
||||||
|
Uri recordURI = dbHelper.insert(toStore);
|
||||||
|
long id = RepoUtils.getAndroidIdFromUri(recordURI);
|
||||||
|
Log.d(LOG_TAG, "Inserted as " + id);
|
||||||
|
|
||||||
|
toStore.androidID = id;
|
||||||
|
updateBookkeeping(toStore);
|
||||||
|
Log.d(LOG_TAG, "insert() returning record " + toStore.guid);
|
||||||
|
return toStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Record replace(Record newRecord, Record existingRecord) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
||||||
|
Record toStore = prepareRecord(newRecord);
|
||||||
|
|
||||||
|
// newRecord should already have suitable androidID and guid.
|
||||||
|
dbHelper.update(existingRecord.guid, toStore);
|
||||||
|
updateBookkeeping(toStore);
|
||||||
|
Log.d(LOG_TAG, "replace() returning record " + toStore.guid);
|
||||||
|
return toStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Record recordForGUID(String guid) throws
|
protected Record recordForGUID(String guid) throws
|
||||||
@ -451,21 +567,23 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if record already exists locally.
|
/**
|
||||||
|
* Attempt to find an equivalent record through some means other than GUID.
|
||||||
|
*
|
||||||
|
* @param record
|
||||||
|
* The record for which to search.
|
||||||
|
* @return
|
||||||
|
* An equivalent Record object, or null if none is found.
|
||||||
|
*
|
||||||
|
* @throws MultipleRecordsForGuidException
|
||||||
|
* @throws NoGuidForIdException
|
||||||
|
* @throws NullCursorException
|
||||||
|
* @throws ParentNotFoundException
|
||||||
|
*/
|
||||||
protected Record findExistingRecord(Record record) throws MultipleRecordsForGuidException,
|
protected Record findExistingRecord(Record record) throws MultipleRecordsForGuidException,
|
||||||
NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
||||||
|
|
||||||
Log.d(LOG_TAG, "Finding existing record for GUID " + record.guid);
|
Log.d(LOG_TAG, "Finding existing record for incoming record with GUID " + record.guid);
|
||||||
Record r = recordForGUID(record.guid);
|
|
||||||
|
|
||||||
// One result. (Multiple throws an exception.)
|
|
||||||
if (r != null) {
|
|
||||||
Log.d(LOG_TAG, "Found one by GUID.");
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty result.
|
|
||||||
// Check to see if record exists but with a different guid.
|
|
||||||
String recordString = buildRecordString(record);
|
String recordString = buildRecordString(record);
|
||||||
Log.d(LOG_TAG, "Searching with record string " + recordString);
|
Log.d(LOG_TAG, "Searching with record string " + recordString);
|
||||||
String guid = getRecordToGuidMap().get(recordString);
|
String guid = getRecordToGuidMap().get(recordString);
|
||||||
@ -485,6 +603,7 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
private void createRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
||||||
|
Utils.info(LOG_TAG, "BEGIN: creating record -> GUID map.");
|
||||||
recordToGuid = new HashMap<String, String>();
|
recordToGuid = new HashMap<String, String>();
|
||||||
Cursor cur = dbHelper.fetchAll();
|
Cursor cur = dbHelper.fetchAll();
|
||||||
try {
|
try {
|
||||||
@ -501,33 +620,21 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
|
|||||||
} finally {
|
} finally {
|
||||||
cur.close();
|
cur.close();
|
||||||
}
|
}
|
||||||
|
Utils.info(LOG_TAG, "END: creating record -> GUID map.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putRecordToGuidMap(String guid, String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
public void putRecordToGuidMap(String recordString, String guid) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
|
||||||
if (recordToGuid == null) {
|
if (recordToGuid == null) {
|
||||||
createRecordToGuidMap();
|
createRecordToGuidMap();
|
||||||
}
|
}
|
||||||
recordToGuid.put(guid, recordString);
|
recordToGuid.put(recordString, guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Record reconcileRecords(Record local, Record remote) {
|
protected abstract Record prepareRecord(Record record);
|
||||||
Log.i(LOG_TAG, "Reconciling " + local.guid + " against " + remote.guid);
|
protected void updateBookkeeping(Record record) throws NoGuidForIdException,
|
||||||
|
NullCursorException,
|
||||||
// Determine which record is newer since this is the one we will take in case of conflict.
|
ParentNotFoundException {
|
||||||
// Yes, clock drift. *sigh*
|
putRecordToGuidMap(buildRecordString(record), record.guid);
|
||||||
Record newer;
|
|
||||||
if (local.lastModified > remote.lastModified) {
|
|
||||||
newer = local;
|
|
||||||
} else {
|
|
||||||
newer = remote;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newer.guid != remote.guid) {
|
|
||||||
newer.guid = remote.guid;
|
|
||||||
}
|
|
||||||
newer.androidID = local.androidID;
|
|
||||||
|
|
||||||
return newer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wipe method and thread.
|
// Wipe method and thread.
|
||||||
|
@ -95,6 +95,51 @@ public class BookmarkRecord extends Record {
|
|||||||
parentID + "/" + androidParentID + "/" + parentName + ">";
|
parentID + "/" + androidParentID + "/" + parentName + ">";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Oh God, this is terribly thread-unsafe. These record objects should be immutable.
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected JSONArray copyChildren() {
|
||||||
|
if (this.children == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONArray children = new JSONArray();
|
||||||
|
children.addAll(this.children);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected JSONArray copyTags() {
|
||||||
|
if (this.tags == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONArray tags = new JSONArray();
|
||||||
|
tags.addAll(this.tags);
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Record copyWithIDs(String guid, long androidID) {
|
||||||
|
BookmarkRecord out = new BookmarkRecord(guid, this.collection, this.lastModified, this.deleted);
|
||||||
|
out.androidID = androidID;
|
||||||
|
out.sortIndex = this.sortIndex;
|
||||||
|
|
||||||
|
// Copy BookmarkRecord fields.
|
||||||
|
out.title = this.title;
|
||||||
|
out.bookmarkURI = this.bookmarkURI;
|
||||||
|
out.description = this.description;
|
||||||
|
out.keyword = this.keyword;
|
||||||
|
out.parentID = this.parentID;
|
||||||
|
out.parentName = this.parentName;
|
||||||
|
out.androidParentID = this.androidParentID;
|
||||||
|
out.type = this.type;
|
||||||
|
out.pos = this.pos;
|
||||||
|
out.androidPosition = this.androidPosition;
|
||||||
|
|
||||||
|
out.children = this.copyChildren();
|
||||||
|
out.tags = this.copyTags();
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initFromPayload(CryptoRecord payload) {
|
public void initFromPayload(CryptoRecord payload) {
|
||||||
ExtendedJSONObject p = payload.payload;
|
ExtendedJSONObject p = payload.payload;
|
||||||
@ -179,21 +224,18 @@ public class BookmarkRecord extends Record {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void trace(String s) {
|
private void trace(String s) {
|
||||||
if (Utils.ENABLE_TRACE_LOGGING) {
|
Utils.trace(LOG_TAG, s);
|
||||||
Log.d(LOG_TAG, s);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equalPayloads(Object o) {
|
||||||
trace("Calling BookmarkRecord.equals.");
|
trace("Calling BookmarkRecord.equalPayloads.");
|
||||||
if (!(o instanceof BookmarkRecord)) {
|
if (o == null || !(o instanceof BookmarkRecord)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
BookmarkRecord other = (BookmarkRecord) o;
|
BookmarkRecord other = (BookmarkRecord) o;
|
||||||
|
if (!super.equalPayloads(other)) {
|
||||||
if (!super.equals(other)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +279,14 @@ public class BookmarkRecord extends Record {
|
|||||||
&& jsonArrayStringsEqual(this.tags, other.tags);
|
&& jsonArrayStringsEqual(this.tags, other.tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts to JSONArrays to strings and checks if they are the same.
|
// TODO: two records can be congruent if their child lists are different.
|
||||||
|
@Override
|
||||||
|
public boolean congruentWith(Object o) {
|
||||||
|
return this.equalPayloads(o) &&
|
||||||
|
super.congruentWith(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts two JSONArrays to strings and checks if they are the same.
|
||||||
// This is only useful for stuff like tags where we aren't actually
|
// This is only useful for stuff like tags where we aren't actually
|
||||||
// touching the data there (and therefore ordering won't change)
|
// touching the data there (and therefore ordering won't change)
|
||||||
private boolean jsonArrayStringsEqual(JSONArray a, JSONArray b) {
|
private boolean jsonArrayStringsEqual(JSONArray a, JSONArray b) {
|
||||||
@ -247,7 +296,6 @@ public class BookmarkRecord extends Record {
|
|||||||
if (a != null && b == null) return false;
|
if (a != null && b == null) return false;
|
||||||
return RepoUtils.stringsEqual(a.toJSONString(), b.toJSONString());
|
return RepoUtils.stringsEqual(a.toJSONString(), b.toJSONString());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,6 +83,32 @@ public class HistoryRecord extends Record {
|
|||||||
public long fennecDateVisited;
|
public long fennecDateVisited;
|
||||||
public long fennecVisitCount;
|
public long fennecVisitCount;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private JSONArray copyVisits() {
|
||||||
|
if (this.visits == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONArray out = new JSONArray();
|
||||||
|
out.addAll(this.visits);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Record copyWithIDs(String guid, long androidID) {
|
||||||
|
HistoryRecord out = new HistoryRecord(guid, this.collection, this.lastModified, this.deleted);
|
||||||
|
out.androidID = androidID;
|
||||||
|
out.sortIndex = this.sortIndex;
|
||||||
|
|
||||||
|
// Copy HistoryRecord fields.
|
||||||
|
out.title = this.title;
|
||||||
|
out.histURI = this.histURI;
|
||||||
|
out.fennecDateVisited = this.fennecDateVisited;
|
||||||
|
out.fennecVisitCount = this.fennecVisitCount;
|
||||||
|
out.visits = this.copyVisits();
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initFromPayload(CryptoRecord payload) {
|
public void initFromPayload(CryptoRecord payload) {
|
||||||
ExtendedJSONObject p = payload.payload;
|
ExtendedJSONObject p = payload.payload;
|
||||||
@ -115,31 +141,61 @@ public class HistoryRecord extends Record {
|
|||||||
return rec;
|
return rec;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean equalsExceptVisits(Object o) {
|
|
||||||
if (!(o instanceof HistoryRecord)) {
|
/**
|
||||||
|
* We consider two history records to be congruent if they represent the
|
||||||
|
* same history record regardless of visits.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean congruentWith(Object o) {
|
||||||
|
if (o == null || !(o instanceof HistoryRecord)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
HistoryRecord other = (HistoryRecord) o;
|
HistoryRecord other = (HistoryRecord) o;
|
||||||
return super.equals(other) &&
|
if (!super.congruentWith(other)) {
|
||||||
RepoUtils.stringsEqual(this.title, other.title) &&
|
return false;
|
||||||
|
}
|
||||||
|
return RepoUtils.stringsEqual(this.title, other.title) &&
|
||||||
RepoUtils.stringsEqual(this.histURI, other.histURI);
|
RepoUtils.stringsEqual(this.histURI, other.histURI);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean equalsIncludingVisits(Object o) {
|
@Override
|
||||||
|
public boolean equalPayloads(Object o) {
|
||||||
|
if (o == null || !(o instanceof HistoryRecord)) {
|
||||||
|
Log.d(LOG_TAG, "Not a HistoryRecord: " + o);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
HistoryRecord other = (HistoryRecord) o;
|
HistoryRecord other = (HistoryRecord) o;
|
||||||
return equalsExceptVisits(other) && this.checkVisitsEquals(other);
|
if (!super.equalPayloads(other)) {
|
||||||
|
Log.d(LOG_TAG, "super.equalPayloads returned false.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return RepoUtils.stringsEqual(this.title, other.title) &&
|
||||||
|
RepoUtils.stringsEqual(this.histURI, other.histURI) &&
|
||||||
|
checkVisitsEquals(other);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
/**
|
public boolean equalAndroidIDs(Record other) {
|
||||||
* We consider two history records to be equal if they represent the
|
return super.equalAndroidIDs(other) &&
|
||||||
* same history record regardless of visits.
|
this.equalFennecVisits(other);
|
||||||
*/
|
}
|
||||||
public boolean equals(Object o) {
|
|
||||||
return equalsExceptVisits(o);
|
private boolean equalFennecVisits(Record other) {
|
||||||
|
if (!(other instanceof HistoryRecord)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HistoryRecord h = (HistoryRecord) other;
|
||||||
|
return this.fennecDateVisited == h.fennecDateVisited &&
|
||||||
|
this.fennecVisitCount == h.fennecVisitCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkVisitsEquals(HistoryRecord other) {
|
private boolean checkVisitsEquals(HistoryRecord other) {
|
||||||
|
Log.d(LOG_TAG, "Checking visits.");
|
||||||
|
if (Utils.ENABLE_TRACE_LOGGING) {
|
||||||
|
Log.d(LOG_TAG, ">> Mine: " + ((this.visits == null) ? "null" : this.visits.toJSONString()));
|
||||||
|
Log.d(LOG_TAG, ">> Theirs: " + ((other.visits == null) ? "null" : other.visits.toJSONString()));
|
||||||
|
}
|
||||||
|
|
||||||
// Handle nulls.
|
// Handle nulls.
|
||||||
if (this.visits == other.visits) {
|
if (this.visits == other.visits) {
|
||||||
|
@ -75,6 +75,28 @@ public class PasswordRecord extends Record {
|
|||||||
public long timeLastUsed;
|
public long timeLastUsed;
|
||||||
public long timesUsed;
|
public long timesUsed;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Record copyWithIDs(String guid, long androidID) {
|
||||||
|
PasswordRecord out = new PasswordRecord(guid, this.collection, this.lastModified, this.deleted);
|
||||||
|
out.androidID = androidID;
|
||||||
|
out.sortIndex = this.sortIndex;
|
||||||
|
|
||||||
|
// Copy HistoryRecord fields.
|
||||||
|
out.hostname = this.hostname;
|
||||||
|
out.formSubmitURL = this.formSubmitURL;
|
||||||
|
out.httpRealm = this.httpRealm;
|
||||||
|
out.username = this.username;
|
||||||
|
out.password = this.password;
|
||||||
|
out.usernameField = this.usernameField;
|
||||||
|
out.passwordField = this.passwordField;
|
||||||
|
out.encType = this.encType;
|
||||||
|
out.timeLastUsed = this.timeLastUsed;
|
||||||
|
out.timesUsed = this.timesUsed;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initFromPayload(CryptoRecord payload) {
|
public void initFromPayload(CryptoRecord payload) {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
@ -88,10 +110,33 @@ public class PasswordRecord extends Record {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean congruentWith(Object o) {
|
||||||
if (!o.getClass().equals(PasswordRecord.class)) return false;
|
if (o == null || !(o instanceof PasswordRecord)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
PasswordRecord other = (PasswordRecord) o;
|
PasswordRecord other = (PasswordRecord) o;
|
||||||
if (!super.equals(other)) return false;
|
if (!super.congruentWith(other)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return RepoUtils.stringsEqual(this.hostname, other.hostname)
|
||||||
|
&& RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL)
|
||||||
|
&& RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
|
||||||
|
&& RepoUtils.stringsEqual(this.username, other.username)
|
||||||
|
&& RepoUtils.stringsEqual(this.password, other.password)
|
||||||
|
&& RepoUtils.stringsEqual(this.usernameField, other.usernameField)
|
||||||
|
&& RepoUtils.stringsEqual(this.passwordField, other.passwordField)
|
||||||
|
&& RepoUtils.stringsEqual(this.encType, other.encType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equalPayloads(Object o) {
|
||||||
|
if (o == null || !(o instanceof PasswordRecord)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PasswordRecord other = (PasswordRecord) o;
|
||||||
|
if (!super.equalPayloads(other)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return RepoUtils.stringsEqual(this.hostname, other.hostname)
|
return RepoUtils.stringsEqual(this.hostname, other.hostname)
|
||||||
&& RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL)
|
&& RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL)
|
||||||
&& RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
|
&& RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
|
||||||
|
@ -43,8 +43,62 @@ import java.io.UnsupportedEncodingException;
|
|||||||
import org.mozilla.gecko.sync.CryptoRecord;
|
import org.mozilla.gecko.sync.CryptoRecord;
|
||||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record is the abstract base class for all entries that Sync processes:
|
||||||
|
* bookmarks, passwords, history, and such.
|
||||||
|
*
|
||||||
|
* A Record can be initialized from or serialized to a CryptoRecord for
|
||||||
|
* submission to an encrypted store.
|
||||||
|
*
|
||||||
|
* Records should be considered to be conventionally immutable: modifications
|
||||||
|
* should be completed before the new record object escapes its constructing
|
||||||
|
* scope. Note that this is a critically important part of equality. As Rich
|
||||||
|
* Hickey notes:
|
||||||
|
*
|
||||||
|
* … the only things you can really compare for equality are immutable things,
|
||||||
|
* because if you compare two things for equality that are mutable, and ever
|
||||||
|
* say true, and they're ever not the same thing, you are wrong. Or you will
|
||||||
|
* become wrong at some point in the future.
|
||||||
|
*
|
||||||
|
* Records have a layered definition of equality. Two records can be said to be
|
||||||
|
* "equal" if:
|
||||||
|
*
|
||||||
|
* * They have the same GUID and collection. Two crypto/keys records are in some
|
||||||
|
* way "the same".
|
||||||
|
* This is `equalIdentifiers`.
|
||||||
|
*
|
||||||
|
* * Their most significant fields are the same. That is to say, they share a
|
||||||
|
* GUID, a collection, deletion, and domain-specific fields. Two copies of
|
||||||
|
* crypto/keys, neither deleted, with the same encrypted data but different
|
||||||
|
* modified times and sortIndex are in a stronger way "the same".
|
||||||
|
* This is `equalPayloads`.
|
||||||
|
*
|
||||||
|
* * Their most significant fields are the same, and their local fields (e.g.,
|
||||||
|
* the androidID to which we have decided that this record maps) are congruent.
|
||||||
|
* A record with the same androidID, or one whose androidID has not been set,
|
||||||
|
* can be considered "the same".
|
||||||
|
* This concept can be extended by Record subclasses. The key point is that
|
||||||
|
* reconciling should be applied to the contents of these records. For example,
|
||||||
|
* two history records with the same URI and GUID, but different visit arrays,
|
||||||
|
* can be said to be congruent.
|
||||||
|
* This is `congruentWith`.
|
||||||
|
*
|
||||||
|
* * They are strictly identical. Every field that is persisted, including
|
||||||
|
* lastModified and androidID, is equal.
|
||||||
|
* This is `equals`.
|
||||||
|
*
|
||||||
|
* Different parts of the codebase have use for different layers of this
|
||||||
|
* comparison hierarchy. For instance, lastModified times change every time a
|
||||||
|
* record is stored; a store followed by a retrieval will return a Record that
|
||||||
|
* shares its most significant fields with the input, but has a later
|
||||||
|
* lastModified time and might not yet have values set for others. Reconciling
|
||||||
|
* will thus ignore the modification time of a record.
|
||||||
|
*
|
||||||
|
* @author rnewman
|
||||||
|
*
|
||||||
|
*/
|
||||||
public abstract class Record {
|
public abstract class Record {
|
||||||
// TODO: consider immutability, effective immutability, and thread-safety.
|
|
||||||
public String guid;
|
public String guid;
|
||||||
public String collection;
|
public String collection;
|
||||||
public long lastModified;
|
public long lastModified;
|
||||||
@ -58,16 +112,22 @@ public abstract class Record {
|
|||||||
this.lastModified = lastModified;
|
this.lastModified = lastModified;
|
||||||
this.deleted = deleted;
|
this.deleted = deleted;
|
||||||
this.sortIndex = 0;
|
this.sortIndex = 0;
|
||||||
|
this.androidID = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public boolean equals(Object o) {
|
* Return true iff the input is a Record and has the same
|
||||||
if (o == null) {
|
* collection and guid as this object.
|
||||||
|
*
|
||||||
|
* @param o
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean equalIdentifiers(Object o) {
|
||||||
|
if (o == null || !(o instanceof Record)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Record other = (Record) o;
|
Record other = (Record) o;
|
||||||
|
|
||||||
if (this.guid == null) {
|
if (this.guid == null) {
|
||||||
if (other.guid != null) {
|
if (other.guid != null) {
|
||||||
return false;
|
return false;
|
||||||
@ -77,7 +137,6 @@ public abstract class Record {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.collection == null) {
|
if (this.collection == null) {
|
||||||
if (other.collection != null) {
|
if (other.collection != null) {
|
||||||
return false;
|
return false;
|
||||||
@ -87,13 +146,84 @@ public abstract class Record {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.deleted != other.deleted) {
|
/**
|
||||||
|
* Return true iff the input is a Record which is substantially the
|
||||||
|
* same as this object.
|
||||||
|
*
|
||||||
|
* @param o
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean equalPayloads(Object o) {
|
||||||
|
if (!this.equalIdentifiers(o)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Record other = (Record) o;
|
||||||
|
return this.deleted == other.deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true iff the input is a Record which is substantially the
|
||||||
|
* same as this object, considering the ability and desire two
|
||||||
|
* reconcile the two objects if possible.
|
||||||
|
*
|
||||||
|
* @param o
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean congruentWith(Object o) {
|
||||||
|
if (!this.equalIdentifiers(o)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Record other = (Record) o;
|
||||||
|
return congruentAndroidIDs(other) &&
|
||||||
|
(this.deleted == other.deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean congruentAndroidIDs(Record other) {
|
||||||
|
// We treat -1 as "unset", and treat this as
|
||||||
|
// congruent with any other value.
|
||||||
|
if (this.androidID != -1 &&
|
||||||
|
other.androidID != -1 &&
|
||||||
|
this.androidID != other.androidID) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true iff the input is both equal in terms of payload,
|
||||||
|
* and also shares transient values such as timestamps.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o == null || !(o instanceof Record)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Record other = (Record) o;
|
||||||
|
return equalTimestamps(other) &&
|
||||||
|
equalSortIndices(other) &&
|
||||||
|
equalAndroidIDs(other) &&
|
||||||
|
equalPayloads(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equalAndroidIDs(Record other) {
|
||||||
|
return this.androidID == other.androidID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equalSortIndices(Record other) {
|
||||||
|
return this.sortIndex == other.sortIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equalTimestamps(Object o) {
|
||||||
|
if (o == null || !(o instanceof Record)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ((Record) o).lastModified == this.lastModified;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract void initFromPayload(CryptoRecord payload);
|
public abstract void initFromPayload(CryptoRecord payload);
|
||||||
public abstract CryptoRecord getPayload();
|
public abstract CryptoRecord getPayload();
|
||||||
|
|
||||||
@ -122,4 +252,15 @@ public abstract class Record {
|
|||||||
throw new IllegalStateException(detailMessage);
|
throw new IllegalStateException(detailMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an identical copy of this record with the provided two values.
|
||||||
|
*
|
||||||
|
* Oh for persistent data structures.
|
||||||
|
*
|
||||||
|
* @param guid
|
||||||
|
* @param androidID
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public abstract Record copyWithIDs(String guid, long androidID);
|
||||||
}
|
}
|
||||||
|
@ -37,12 +37,21 @@
|
|||||||
|
|
||||||
package org.mozilla.gecko.sync.stage;
|
package org.mozilla.gecko.sync.stage;
|
||||||
|
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository;
|
||||||
import org.mozilla.gecko.sync.repositories.RecordFactory;
|
import org.mozilla.gecko.sync.repositories.RecordFactory;
|
||||||
import org.mozilla.gecko.sync.repositories.Repository;
|
import org.mozilla.gecko.sync.repositories.Repository;
|
||||||
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository;
|
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository;
|
||||||
import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory;
|
import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory;
|
||||||
|
|
||||||
public class AndroidBrowserBookmarksServerSyncStage extends ServerSyncStage {
|
public class AndroidBrowserBookmarksServerSyncStage extends ServerSyncStage {
|
||||||
|
|
||||||
|
// Eventually this kind of sync stage will be data-driven,
|
||||||
|
// and all this hard-coding can go away.
|
||||||
|
private static final String BOOKMARKS_SORT = "index";
|
||||||
|
private static final long BOOKMARKS_REQUEST_LIMIT = 5000; // Sanity limit.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(org.mozilla.gecko.sync.GlobalSession session) throws NoSuchStageException {
|
public void execute(org.mozilla.gecko.sync.GlobalSession session) throws NoSuchStageException {
|
||||||
super.execute(session);
|
super.execute(session);
|
||||||
@ -57,6 +66,16 @@ public class AndroidBrowserBookmarksServerSyncStage extends ServerSyncStage {
|
|||||||
return "bookmarks";
|
return "bookmarks";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Repository getRemoteRepository() throws URISyntaxException {
|
||||||
|
return new ConstrainedServer11Repository(session.config.getClusterURLString(),
|
||||||
|
session.config.username,
|
||||||
|
getCollection(),
|
||||||
|
session,
|
||||||
|
BOOKMARKS_REQUEST_LIMIT,
|
||||||
|
BOOKMARKS_SORT);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Repository getLocalRepository() {
|
protected Repository getLocalRepository() {
|
||||||
return new AndroidBrowserBookmarksRepository();
|
return new AndroidBrowserBookmarksRepository();
|
||||||
|
@ -37,12 +37,21 @@
|
|||||||
|
|
||||||
package org.mozilla.gecko.sync.stage;
|
package org.mozilla.gecko.sync.stage;
|
||||||
|
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.sync.repositories.ConstrainedServer11Repository;
|
||||||
import org.mozilla.gecko.sync.repositories.RecordFactory;
|
import org.mozilla.gecko.sync.repositories.RecordFactory;
|
||||||
import org.mozilla.gecko.sync.repositories.Repository;
|
import org.mozilla.gecko.sync.repositories.Repository;
|
||||||
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepository;
|
import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepository;
|
||||||
import org.mozilla.gecko.sync.repositories.domain.HistoryRecordFactory;
|
import org.mozilla.gecko.sync.repositories.domain.HistoryRecordFactory;
|
||||||
|
|
||||||
public class AndroidBrowserHistoryServerSyncStage extends ServerSyncStage {
|
public class AndroidBrowserHistoryServerSyncStage extends ServerSyncStage {
|
||||||
|
|
||||||
|
// Eventually this kind of sync stage will be data-driven,
|
||||||
|
// and all this hard-coding can go away.
|
||||||
|
private static final String HISTORY_SORT = "index";
|
||||||
|
private static final long HISTORY_REQUEST_LIMIT = 500;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(org.mozilla.gecko.sync.GlobalSession session) throws NoSuchStageException {
|
public void execute(org.mozilla.gecko.sync.GlobalSession session) throws NoSuchStageException {
|
||||||
super.execute(session);
|
super.execute(session);
|
||||||
@ -62,6 +71,16 @@ public class AndroidBrowserHistoryServerSyncStage extends ServerSyncStage {
|
|||||||
return new AndroidBrowserHistoryRepository();
|
return new AndroidBrowserHistoryRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Repository getRemoteRepository() throws URISyntaxException {
|
||||||
|
return new ConstrainedServer11Repository(session.config.getClusterURLString(),
|
||||||
|
session.config.username,
|
||||||
|
getCollection(),
|
||||||
|
session,
|
||||||
|
HISTORY_REQUEST_LIMIT,
|
||||||
|
HISTORY_SORT);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected RecordFactory getRecordFactory() {
|
protected RecordFactory getRecordFactory() {
|
||||||
return new HistoryRecordFactory();
|
return new HistoryRecordFactory();
|
||||||
|
@ -84,6 +84,14 @@ public abstract class ServerSyncStage implements
|
|||||||
protected abstract Repository getLocalRepository();
|
protected abstract Repository getLocalRepository();
|
||||||
protected abstract RecordFactory getRecordFactory();
|
protected abstract RecordFactory getRecordFactory();
|
||||||
|
|
||||||
|
// Override this in subclasses.
|
||||||
|
protected Repository getRemoteRepository() throws URISyntaxException {
|
||||||
|
return new Server11Repository(session.config.getClusterURLString(),
|
||||||
|
session.config.username,
|
||||||
|
getCollection(),
|
||||||
|
session);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a Crypto5Middleware-wrapped Server11Repository.
|
* Return a Crypto5Middleware-wrapped Server11Repository.
|
||||||
*
|
*
|
||||||
@ -97,11 +105,7 @@ public abstract class ServerSyncStage implements
|
|||||||
protected Repository wrappedServerRepo() throws NoCollectionKeysSetException, URISyntaxException {
|
protected Repository wrappedServerRepo() throws NoCollectionKeysSetException, URISyntaxException {
|
||||||
String collection = this.getCollection();
|
String collection = this.getCollection();
|
||||||
KeyBundle collectionKey = session.keyForCollection(collection);
|
KeyBundle collectionKey = session.keyForCollection(collection);
|
||||||
Server11Repository serverRepo = new Server11Repository(session.config.getClusterURLString(),
|
Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(getRemoteRepository(), collectionKey);
|
||||||
session.config.username,
|
|
||||||
collection,
|
|
||||||
session);
|
|
||||||
Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(serverRepo, collectionKey);
|
|
||||||
cryptoRepo.recordFactory = getRecordFactory();
|
cryptoRepo.recordFactory = getRecordFactory();
|
||||||
return cryptoRepo;
|
return cryptoRepo;
|
||||||
}
|
}
|
||||||
|
@ -65,21 +65,15 @@ class ConcurrentRecordConsumer extends RecordConsumer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void info(String message) {
|
private static void info(String message) {
|
||||||
Utils.logToStdout(LOG_TAG, "::INFO: ", message);
|
Utils.info(LOG_TAG, message);
|
||||||
Log.i(LOG_TAG, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void debug(String message) {
|
private static void debug(String message) {
|
||||||
Utils.logToStdout(LOG_TAG, ":: DEBUG: ", message);
|
Utils.debug(LOG_TAG, message);
|
||||||
Log.d(LOG_TAG, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void trace(String message) {
|
private static void trace(String message) {
|
||||||
if (!Utils.ENABLE_TRACE_LOGGING) {
|
Utils.trace(LOG_TAG, message);
|
||||||
return;
|
|
||||||
}
|
|
||||||
Utils.logToStdout(LOG_TAG, ":: TRACE: ", message);
|
|
||||||
Log.d(LOG_TAG, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object monitor = new Object();
|
private Object monitor = new Object();
|
||||||
@ -139,7 +133,13 @@ class ConcurrentRecordConsumer extends RecordConsumer {
|
|||||||
while (!delegate.getQueue().isEmpty()) {
|
while (!delegate.getQueue().isEmpty()) {
|
||||||
trace("Grabbing record...");
|
trace("Grabbing record...");
|
||||||
Record record = delegate.getQueue().remove();
|
Record record = delegate.getQueue().remove();
|
||||||
delegate.store(record);
|
trace("Storing record... " + delegate);
|
||||||
|
try {
|
||||||
|
delegate.store(record);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: Bug 709371: track records that failed to apply.
|
||||||
|
Log.e(LOG_TAG, "Caught error in store.", e);
|
||||||
|
}
|
||||||
trace("Done with record.");
|
trace("Done with record.");
|
||||||
}
|
}
|
||||||
synchronized (monitor) {
|
synchronized (monitor) {
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
mobile/android/base/resources/drawable/pin_background.xml
|
mobile/android/base/resources/drawable/pin_background.xml
|
||||||
mobile/android/base/resources/drawable/sync_icon.png
|
mobile/android/base/resources/drawable/sync_ic_launcher.png
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
mobile/android/base/resources/xml/sync_authenticator.xml
|
|
||||||
mobile/android/base/resources/xml/sync_options.xml
|
|
||||||
mobile/android/base/resources/xml/sync_syncadapter.xml
|
|
File diff suppressed because one or more lines are too long
@ -1,7 +1,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:icon="@drawable/sync_ic_launcher"
|
android:icon="@drawable/sync_ic_launcher"
|
||||||
android:label="@string/sync_app_name"
|
android:label="@string/sync_app_name"
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:configChanges="orientation"
|
android:configChanges="orientation"
|
||||||
android:windowSoftInputMode="adjustResize|stateHidden"
|
android:windowSoftInputMode="adjustResize|stateHidden"
|
||||||
@ -12,17 +11,14 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<data android:scheme="sync" android:host="org.mozilla.android" android:path="/setup"/>
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:clearTaskOnLaunch="true"
|
android:clearTaskOnLaunch="true"
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:name="org.mozilla.gecko.sync.setup.activities.AccountActivity"
|
android:name="org.mozilla.gecko.sync.setup.activities.AccountActivity"
|
||||||
android:windowSoftInputMode="adjustPan|stateHidden"/>
|
android:windowSoftInputMode="adjustPan|stateHidden"/>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.mozilla.gecko.sync.setup.activities.SetupFailureActivity" />
|
android:name="org.mozilla.gecko.sync.setup.activities.SetupFailureActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.mozilla.gecko.sync.setup.activities.SetupSuccessActivity"
|
android:name="org.mozilla.gecko.sync.setup.activities.SetupSuccessActivity" />
|
||||||
android:launchMode="singleTask" />
|
|
@ -1,10 +1,9 @@
|
|||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.MANAGE_CREDENTIALS" />
|
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<service
|
<service
|
||||||
android:exported="true"
|
android:exported="false"
|
||||||
android:name="org.mozilla.gecko.sync.setup.SyncAuthenticatorService" >
|
android:name="org.mozilla.gecko.sync.setup.SyncAuthenticatorService" >
|
||||||
<intent-filter >
|
<intent-filter >
|
||||||
<action android:name="android.accounts.AccountAuthenticator" />
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
@ -10,7 +10,7 @@
|
|||||||
android:resource="@xml/sync_authenticator" />
|
android:resource="@xml/sync_authenticator" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:exported="true"
|
android:exported="false"
|
||||||
android:name="org.mozilla.gecko.sync.syncadapter.SyncService" >
|
android:name="org.mozilla.gecko.sync.syncadapter.SyncService" >
|
||||||
<intent-filter >
|
<intent-filter >
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
|
@ -60,3 +60,4 @@
|
|||||||
|
|
||||||
<!-- Notification strings -->
|
<!-- Notification strings -->
|
||||||
<string name="sync_notification_oneaccount">&sync.notification.oneaccount.label;</string>
|
<string name="sync_notification_oneaccount">&sync.notification.oneaccount.label;</string>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user