Bug 709339 - First mostly functional drop of native Sync. a=mobile

This commit is contained in:
Richard Newman 2012-01-14 09:20:31 -08:00
parent 2b222659e0
commit 88ff884946
89 changed files with 5497 additions and 2295 deletions

View File

@ -42,6 +42,10 @@
<!-- Pair Device -->
<!ENTITY sync.pair.tryagain.label 'Please try again.'>
<!-- Firefox SyncAdapter Settings Screen -->
<!ENTITY sync.settings.options.label 'Options'>
<!ENTITY sync.summary.pair.label 'Link another device to your &syncBrand.shortName.label; account'>
<!-- Common text -->
<!ENTITY sync.button.cancel.label 'Cancel'>
<!ENTITY sync.button.connect.label 'Connect'>
@ -58,3 +62,6 @@
<!ENTITY bookmarks.folder.unfiled.label 'Unsorted Bookmarks'>
<!ENTITY bookmarks.folder.desktop.label 'Desktop Bookmarks'>
<!ENTITY bookmarks.folder.mobile.label 'Mobile Bookmarks'>
<!-- Notification strings -->
<!ENTITY sync.notification.oneaccount.label 'Only one &syncBrand.fullName.label; account is supported.'>

View File

@ -2,53 +2,67 @@
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/SyncTextFrame" >
<TextView
style="@style/SyncTextTitle"
android:text="@string/sync_title_connect" />
<View
android:layout_height="2dp"
android:background="#FFFFFF" />
<TextView
style="@style/SyncTextItem"
android:text="@string/sync_subtitle_account" />
<EditText android:id="@+id/username"
style="@style/SyncEditItem"
android:hint="@string/sync_input_username" />
<EditText android:id="@+id/password"
style="@style/SyncEditItem"
android:password="true"
android:hint="@string/sync_input_password" />
<EditText android:id="@+id/key"
style="@style/SyncEditItem"
android:hint="@string/sync_input_key" />
<CheckBox android:id="@+id/checkbox_server"
android:text="@string/sync_checkbox_server" />
<EditText android:id="@+id/server"
style="@style/SyncEditItem"
android:hint="@string/sync_input_server" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<Button
style="@style/SyncButtonCommon"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_cancel" />
<Button
style="@style/SyncButtonCommon"
android:onClick="connectClickHandler"
android:text="@string/sync_button_connect" />
</LinearLayout>
</TableLayout>
<ScrollView
style="@style/SyncLayout" >
<LinearLayout
style="@style/SyncLayout" >
<TextView
style="@style/SyncTextTitle"
android:text="@string/sync_title_connect" />
<View
style="@style/SyncViewLine" />
<TextView
style="@style/SyncTextItem"
android:text="@string/sync_subtitle_account" />
<EditText android:id="@+id/usernameInput"
style="@style/SyncEditItem"
android:hint="@string/sync_input_username" />
<EditText android:id="@+id/passwordInput"
style="@style/SyncEditItem"
android:password="true"
android:hint="@string/sync_input_password" />
<EditText android:id="@+id/keyInput"
style="@style/SyncEditItem"
android:hint="@string/sync_input_key" />
<CheckBox android:id="@+id/checkbox_server"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:imeOptions="actionDone"
android:text="@string/sync_checkbox_server" />
<EditText android:id="@+id/serverInput"
style="@style/SyncEditItem"
android:visibility="gone"
android:hint="@string/sync_input_server" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<Button
style="@style/SyncButtonCommon"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_cancel" />
<Button
android:id="@+id/accountConnectButton"
style="@style/SyncButtonCommon"
android:onClick="connectClickHandler"
android:clickable="false"
android:enabled="false"
android:text="@string/sync_button_connect" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</TableLayout>

View File

@ -2,57 +2,64 @@
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/SyncTextFrame" >
<TextView android:id="@+id/setup_title"
style="@style/SyncTextTitle"
android:text="@string/sync_title_connect" />
<ScrollView
style="@style/SyncLayout" >
<View
android:layout_width="wrap_content"
android:layout_height="2dp"
android:background="#FFFFFF" />
<LinearLayout
style="@style/SyncLayout" >
<TextView
android:id="@+id/setup_subtitle"
style="@style/SyncTextItem"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:text="@string/sync_subtitle_pair" />
<TextView
android:id="@+id/setup_title"
style="@style/SyncTextTitle"
android:text="@string/sync_title_connect" />
<TextView
style="@style/SyncTextItem"
android:autoLink="web"
android:clickable="true"
android:text="@string/sync_link_show" />
<View
android:layout_width="wrap_content"
android:layout_height="2dp"
android:background="#FFFFFF" />
<LinearLayout
style="@style/SyncTextItem"
android:orientation="vertical" >
<TextView android:id="@+id/text_pin"
style="@style/SyncTextItem"
android:text="@string/sync_pin_default"
android:textSize="40dp" />
</LinearLayout>
<TextView android:id="@+id/link_nodevice"
style="@style/SyncTextItem"
android:clickable="true"
android:onClick="manualClickHandler"
android:text="@string/sync_link_nodevice" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
style="@style/SyncButtonCommon"
<TextView
android:id="@+id/setup_subtitle"
style="@style/SyncTextItem"
android:layout_marginTop="10dp"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_cancel" />
android:layout_marginBottom="10dp"
android:text="@string/sync_subtitle_pair" />
</LinearLayout>
<TextView
style="@style/SyncLinkItem"
android:onClick="showClickHandler"
android:text="@string/sync_link_show" />
<LinearLayout
style="@style/SyncTextItem"
android:orientation="vertical" >
<TextView
android:id="@+id/text_pin"
style="@style/SyncTextItem"
android:text="@string/sync_pin_default"
android:textSize="40dp" />
</LinearLayout>
<TextView
android:id="@+id/link_nodevice"
style="@style/SyncLinkItem"
android:onClick="manualClickHandler"
android:text="@string/sync_link_nodevice" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
style="@style/SyncButtonCommon"
android:layout_marginTop="10dp"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_cancel" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</TableLayout>

View File

@ -5,9 +5,8 @@
style="@style/SyncTextTitle"
android:text="@string/sync_title_fail" />
<View
android:layout_width="fill_parent"
android:layout_height="2dp"
android:background="#FFFFFF"/>
style="@style/SyncViewLine" />
<TextView
style="@style/SyncTextItem"
android:text="@string/sync_subtitle_fail" />
@ -24,8 +23,8 @@
<Button
style="@style/SyncButtonCommon"
android:text="@string/sync_button_tryagain"
android:onClick="tryAgainClickHandler" />
android:onClick="tryAgainClickHandler"
android:text="@string/sync_button_tryagain" />
<Button
style="@style/SyncButtonCommon"

View File

@ -5,14 +5,14 @@
style="@style/SyncTextTitle"
android:text="@string/sync_title_connect" />
<View
android:layout_width="fill_parent"
android:layout_height="2dp"
android:background="#FFFFFF" />
style="@style/SyncViewLine" />
<ProgressBar
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.ProgressBar.Horizontal"
android:indeterminateOnly="true" />
android:indeterminateOnly="true"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp" />
<TextView
style="@style/SyncTextItem"
android:text="@string/sync_jpake_subtitle_waiting" />

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/SyncTextFrame" >
<TextView
style="@style/SyncTextTitle"
android:text="@string/sync_title_fail" />
<View
style="@style/SyncViewLine"/>
<TextView
style="@style/SyncTextItem"
android:text="@string/sync_subtitle_nointernet" />
<Button
style="@style/SyncButtonCommon"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_ok" />
</TableLayout>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/SyncTextFrame" >
<ScrollView
style="@style/SyncLayout" >
<LinearLayout
style="@style/SyncLayout" >
<TextView
android:id="@+id/setup_title"
style="@style/SyncTextTitle"
android:text="@string/sync_title_pair" />
<View
style="@style/SyncViewLine" />
<TextView
android:id="@+id/setup_subtitle"
style="@style/SyncTextItem"
android:layout_marginBottom="10dp"
android:text="@string/sync_subtitle_connect" />
<TextView
style="@style/SyncLinkItem"
android:onClick="showClickHandler"
android:text="@string/sync_link_show" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical" >
<EditText
android:id="@+id/pair_row1"
style="@style/SyncEditPin" />
<EditText
android:id="@+id/pair_row2"
style="@style/SyncEditPin" />
<EditText
android:id="@+id/pair_row3"
style="@style/SyncEditPin"
android:imeOptions="actionDone" />
</LinearLayout>
<LinearLayout
android:id="@+id/pair_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:visibility="invisible" >
<TextView
style="@style/SyncTextItem"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:text="@string/sync_pair_tryagain"
android:textSize="10dp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal" >
<Button
style="@style/SyncButtonCommon"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_cancel" />
<Button
android:id="@+id/pair_button_connect"
style="@style/SyncButtonCommon"
android:onClick="connectClickHandler"
android:clickable="false"
android:enabled="false"
android:text="@string/sync_button_connect" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</TableLayout>

View File

@ -1,13 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/SyncTextFrame" >
<TextView
style="@style/SyncTextTitle"
android:text="@string/sync_title_success" />
<View
android:layout_width="fill_parent"
android:layout_height="2dp"
android:background="#FFFFFF" />
<View style="@style/SyncViewLine" />
<TextView
android:id="@+id/setup_success_subtitle"
@ -16,10 +15,24 @@
android:gravity="center"
android:padding="20dp"
android:text="@string/sync_subtitle_success" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:onClick="settingsClickHandler"
android:text="@string/sync_settings" />
</TableLayout>
<View
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1" />
<TextView
android:id="@+id/link_pair"
style="@style/SyncLinkItem"
android:layout_gravity="center|bottom"
android:onClick="pairClickHandler"
android:text="@string/sync_title_pair" />
</TableLayout>

View File

@ -1,34 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="SyncLayout" parent="@android:style/Widget">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">fill_parent</item>
<item name="android:gravity">center</item>
<item name="android:orientation">vertical</item>
</style>
<!-- TextView Styles -->
<style name="SyncTextFrame" parent="@android:style/TextAppearance">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">fill_parent</item>
<item name="android:padding">20dp</item>
<item name="android:layout_gravity">center</item>
<item name="android:background">#82818A</item>
<item name="android:padding">20dp</item>
<item name="android:orientation">vertical</item>
<item name="android:background">#82818A</item>
</style>
<style name="SyncTextItem" parent="@android:style/TextAppearance.Medium">
<item name="android:gravity">center</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:textSize">15dp</item>
</style>
<style name="SyncLinkItem" parent="SyncTextItem">
<item name="android:clickable">true</item>
<item name="android:textColor">#99CCFF</item>
</style>
<style name="SyncTextTitle" parent="@style/SyncTextItem">
<item name="android:textSize">20dp</item>
<item name="android:gravity">center</item>
<item name="android:paddingBottom">10dp</item>
<item name="android:textSize">20dp</item>
</style>
<!-- EditView Styles -->
<style name="SyncEditItem" parent="@android:style/Widget.EditText">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:singleLine">true</item>
</style>
<style name="SyncButtonCommon">
<item name="android:layout_height">wrap_content</item>
<style name="SyncEditPin" parent="@style/SyncEditItem">
<item name="android:layout_width">wrap_content</item>
<item name="android:gravity">center_horizontal</item>
<item name="android:ems">4</item>
<item name="android:maxLength">4</item>
<item name="android:imeOptions">actionNext</item>
</style>
<!-- Misc Styles -->
<style name="SyncButtonCommon">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
</style>
<style name="SyncViewLine">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">2dp</item>
<item name="android:paddingTop">5dp</item>
<item name="android:paddingBottom">10dp</item>
<item name="android:background">#FFFFFF</item>
</style>
</resources>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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:smallIcon="@drawable/sync_icon"
android:label="@string/sync_account_label" />
android:label="@string/sync_account_label"
android:accountPreferences="@xml/sync_options" />

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/sync_settings_options" />
<PreferenceScreen
android:key="sync_options"
android:title="@string/sync_title_pair"
android:summary="@string/sync_settings_summary_pair">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="org.mozilla.gecko"
android:targetClass="org.mozilla.gecko.sync.setup.activities.SetupSyncActivity">
<extra
android:name="isSetup"
android:value="false" />
</intent>
</PreferenceScreen>
</PreferenceScreen>

View File

@ -2,7 +2,7 @@
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="@ANDROID_PACKAGE_NAME@.db.browser"
android:accountType="org.mozilla.firefox.sync"
android:accountType="org.mozilla.firefox_sync"
android:supportsUploading="true"
android:userVisible="true"
/>

View File

@ -40,17 +40,16 @@ package org.mozilla.gecko.sync;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.CryptoInfo;
import org.mozilla.gecko.sync.crypto.Cryptographer;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.crypto.MissingCryptoInputException;
import org.mozilla.gecko.sync.crypto.NoKeyBundleException;
import org.mozilla.gecko.sync.crypto.Utils;
import org.mozilla.gecko.sync.repositories.domain.Record;
/**
@ -75,6 +74,8 @@ public class CryptoRecord extends Record {
private static final String KEY_ID = "id";
private static final String KEY_COLLECTION = "collection";
private static final String KEY_PAYLOAD = "payload";
private static final String KEY_MODIFIED = "modified";
private static final String KEY_SORTINDEX = "sortindex";
private static final String KEY_CIPHERTEXT = "ciphertext";
private static final String KEY_HMAC = "hmac";
private static final String KEY_IV = "IV";
@ -142,20 +143,29 @@ public class CryptoRecord extends Record {
* @throws ParseException
* @throws IOException
*/
public static CryptoRecord fromJSONRecord(String jsonRecord) throws ParseException, NonObjectJSONException, IOException {
return CryptoRecord.fromJSONRecord(CryptoRecord.parseUTF8AsJSONObject(jsonRecord.getBytes("UTF-8")));
public static CryptoRecord fromJSONRecord(String jsonRecord)
throws ParseException, NonObjectJSONException, IOException {
byte[] bytes = jsonRecord.getBytes("UTF-8");
ExtendedJSONObject object = CryptoRecord.parseUTF8AsJSONObject(bytes);
return CryptoRecord.fromJSONRecord(object);
}
// TODO: defensive programming.
public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord) throws IOException, ParseException, NonObjectJSONException {
String id = (String) jsonRecord.get(KEY_ID);
String collection = (String) jsonRecord.get(KEY_COLLECTION);
public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord)
throws IOException, ParseException, NonObjectJSONException {
String id = (String) jsonRecord.get(KEY_ID);
String collection = (String) jsonRecord.get(KEY_COLLECTION);
ExtendedJSONObject payload = jsonRecord.getJSONObject(KEY_PAYLOAD);
CryptoRecord record = new CryptoRecord(payload);
record.guid = id;
record.collection = collection;
// TODO: lastModified?
record.guid = id;
record.collection = collection;
if (jsonRecord.containsKey(KEY_MODIFIED)) {
record.lastModified = jsonRecord.getTimestamp(KEY_MODIFIED);
}
if (jsonRecord.containsKey(KEY_SORTINDEX )) {
record.sortIndex = jsonRecord.getLong(KEY_SORTINDEX);
}
// TODO: deleted?
return record;
}
@ -230,4 +240,9 @@ public class CryptoRecord extends Record {
o.put(KEY_ID, this.guid);
return o.object;
}
@Override
public String toJSONString() {
return toJSONObject().toJSONString();
}
}

View File

@ -45,6 +45,7 @@ import java.io.StringReader;
import java.util.Map;
import java.util.Map.Entry;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
@ -59,8 +60,7 @@ public class ExtendedJSONObject {
public JSONObject object;
public static Object parse(InputStreamReader reader) throws IOException, ParseException {
Object parseOutput = new JSONParser().parse(reader);
private static Object processParseOutput(Object parseOutput) {
if (parseOutput instanceof JSONObject) {
return new ExtendedJSONObject((JSONObject) parseOutput);
} else {
@ -68,6 +68,15 @@ public class ExtendedJSONObject {
}
}
public static Object parse(String string) throws IOException, ParseException {
return processParseOutput(new JSONParser().parse(string));
}
public static Object parse(InputStreamReader reader) throws IOException, ParseException {
return processParseOutput(new JSONParser().parse(reader));
}
public static Object parse(InputStream stream) throws IOException, ParseException {
InputStreamReader reader = new InputStreamReader(stream, "UTF-8");
return ExtendedJSONObject.parse(reader);
@ -119,6 +128,31 @@ public class ExtendedJSONObject {
return (Long) this.get(key);
}
/**
* Return a server timestamp value as milliseconds since epoch.
* @param string
* @return A Long, or null if the value is non-numeric or doesn't exist.
*/
public Long getTimestamp(String key) {
Object val = this.object.get(key);
// This is absurd.
if (val instanceof Double) {
double millis = ((Double) val).doubleValue() * 1000;
return new Double(millis).longValue();
}
if (val instanceof Float) {
double millis = ((Float) val).doubleValue() * 1000;
return new Double(millis).longValue();
}
if (val instanceof Number) {
// Must be an integral number.
return ((Number) val).longValue() * 1000;
}
return null;
}
public boolean containsKey(String key) {
return this.object.containsKey(key);
}
@ -139,6 +173,9 @@ public class ExtendedJSONObject {
public ExtendedJSONObject getObject(String key) throws NonObjectJSONException {
Object o = this.object.get(key);
if (o == null) {
return null;
}
if (o instanceof ExtendedJSONObject) {
return (ExtendedJSONObject) o;
}
@ -174,4 +211,15 @@ public class ExtendedJSONObject {
public Iterable<Entry<String, Object>> entryIterable() {
return this.object.entrySet();
}
public org.json.simple.JSONArray getArray(String key) throws NonArrayJSONException {
Object o = this.object.get(key);
if (o == null) {
return null;
}
if (o instanceof JSONArray) {
return (JSONArray) o;
}
throw new NonArrayJSONException(o);
}
}

View File

@ -71,14 +71,21 @@ import org.mozilla.gecko.sync.stage.GlobalSyncStage;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
import org.mozilla.gecko.sync.stage.NoSuchStageException;
import ch.boye.httpclientandroidlib.HttpResponse;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
public class GlobalSession implements CredentialsSource {
public class GlobalSession implements CredentialsSource, PrefsSource {
private static final String LOG_TAG = "GlobalSession";
public static final String API_VERSION = "1.1";
public static final long STORAGE_VERSION = 5;
private static final String LOG_TAG = "GlobalSession";
private static final String HEADER_RETRY_AFTER = "retry-after";
private static final String HEADER_X_WEAVE_BACKOFF = "x-weave-backoff";
public SyncConfiguration config = null;
@ -139,6 +146,7 @@ public class GlobalSession implements CredentialsSource {
String serverURL,
String username,
String password,
String prefsPath,
KeyBundle syncKeyBundle,
GlobalSessionCallback callback,
Context context,
@ -166,16 +174,16 @@ public class GlobalSession implements CredentialsSource {
throw new SyncConfigurationException();
}
// TODO: use persisted.
config = new SyncConfiguration();
this.callback = callback;
this.context = context;
config = new SyncConfiguration(prefsPath, this);
config.userAPI = userAPI;
config.serverURL = serverURI;
config.username = username;
config.password = password;
config.syncKeyBundle = syncKeyBundle;
this.callback = callback;
this.context = context;
// clusterURL and syncID are set through `persisted`, or fetched from the server.
// TODO: populate saved configurations. We'll amend these after processing meta/global.
this.synchronizerConfigurations = new SynchronizerConfigurations(persisted);
@ -250,6 +258,15 @@ public class GlobalSession implements CredentialsSource {
return config.syncID;
}
/*
* PrefsSource methods.
*/
@Override
public SharedPreferences getPrefs(String name, int mode) {
return this.getContext().getSharedPreferences(name, mode);
}
@Override
public Context getContext() {
return this.context;
}
@ -279,7 +296,10 @@ public class GlobalSession implements CredentialsSource {
*/
protected void restart() throws AlreadySyncingException {
this.currentState = GlobalSyncStage.Stage.idle;
// TODO: respect backoff.
if (callback.shouldBackOff()) {
this.callback.handleAborted(this, "Told to back off.");
return;
}
this.start();
}
@ -297,9 +317,32 @@ public class GlobalSession implements CredentialsSource {
// TODO: handling of 50x (backoff), 401 (node reassignment or auth error).
// Fall back to aborting.
Log.w(LOG_TAG, "Aborting sync due to HTTP " + response.getStatusCode());
this.interpretHTTPFailure(response.httpResponse());
this.abort(new HTTPFailureException(response), reason);
}
/**
* Perform appropriate backoff etc. extraction.
*/
public void interpretHTTPFailure(HttpResponse response) {
// TODO: handle permanent rejection.
long retryAfter = 0;
long weaveBackoff = 0;
if (response.containsHeader(HEADER_RETRY_AFTER)) {
// Handles non-decimals just fine.
String headerValue = response.getFirstHeader(HEADER_RETRY_AFTER).getValue();
retryAfter = Utils.decimalSecondsToMilliseconds(headerValue);
}
if (response.containsHeader(HEADER_X_WEAVE_BACKOFF)) {
// Handles non-decimals just fine.
String headerValue = response.getFirstHeader(HEADER_X_WEAVE_BACKOFF).getValue();
weaveBackoff = Utils.decimalSecondsToMilliseconds(headerValue);
}
long backoff = Math.max(retryAfter, weaveBackoff);
if (backoff > 0) {
callback.requestBackoff(backoff);
}
}
public void fetchMetaGlobal(MetaGlobalDelegate callback) throws URISyntaxException {
@ -319,7 +362,7 @@ public class GlobalSession implements CredentialsSource {
public void uploadKeys(CryptoRecord keysRecord,
final KeyUploadDelegate keyUploadDelegate) {
SyncStorageRecordRequest request;
final GlobalSession globalSession = this;
final GlobalSession self = this;
try {
request = new SyncStorageRecordRequest(this.config.keysURI());
} catch (URISyntaxException e) {
@ -341,6 +384,7 @@ public class GlobalSession implements CredentialsSource {
@Override
public void handleRequestFailure(SyncStorageResponse response) {
self.interpretHTTPFailure(response.httpResponse());
keyUploadDelegate.onKeyUploadFailed(new HTTPFailureException(response));
}
@ -351,7 +395,7 @@ public class GlobalSession implements CredentialsSource {
@Override
public String credentials() {
return globalSession.credentials();
return self.credentials();
}
};
@ -400,6 +444,7 @@ public class GlobalSession implements CredentialsSource {
config.syncID = remoteSyncID;
// TODO TODO TODO
}
config.persistToPrefs();
advance();
}
@ -422,6 +467,7 @@ public class GlobalSession implements CredentialsSource {
@Override
public void onFreshStart() {
try {
globalSession.config.persistToPrefs();
globalSession.restart();
} catch (Exception e) {
Log.w(LOG_TAG, "Got exception when restarting sync after freshStart.", e);
@ -446,6 +492,7 @@ public class GlobalSession implements CredentialsSource {
public void onWiped(long timestamp) {
session.resetClient();
session.config.collectionKeys.clear(); // TODO: make sure we clear our keys timestamp.
session.config.persistToPrefs();
MetaGlobal mg = new MetaGlobal(metaURL, credentials);
mg.setSyncID(newSyncID);
@ -458,7 +505,7 @@ public class GlobalSession implements CredentialsSource {
mg.upload(new MetaGlobalDelegate() {
@Override
public void handleSuccess(MetaGlobal global) {
public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
session.config.metaGlobal = global;
Log.i(LOG_TAG, "New meta/global uploaded with sync ID " + newSyncID);
@ -487,7 +534,7 @@ public class GlobalSession implements CredentialsSource {
}
@Override
public void handleMissing(MetaGlobal global) {
public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
// Shouldn't happen.
Log.w(LOG_TAG, "Got 'missing' response uploading new meta/global.");
freshStartDelegate.onFreshStartFailed(new Exception("meta/global missing"));
@ -497,6 +544,7 @@ public class GlobalSession implements CredentialsSource {
public void handleFailure(SyncStorageResponse response) {
// TODO: respect backoffs etc.
Log.w(LOG_TAG, "Got failure " + response.getStatusCode() + " uploading new meta/global.");
session.interpretHTTPFailure(response.httpResponse());
freshStartDelegate.onFreshStartFailed(new HTTPFailureException(response));
}
@ -512,20 +560,20 @@ public class GlobalSession implements CredentialsSource {
return new MetaGlobalDelegate() {
@Override
public void handleSuccess(final MetaGlobal global) {
public void handleSuccess(final MetaGlobal global, final SyncStorageResponse response) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.handleSuccess(global);
self.handleSuccess(global, response);
}});
}
@Override
public void handleMissing(final MetaGlobal global) {
public void handleMissing(final MetaGlobal global, final SyncStorageResponse response) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.handleMissing(global);
self.handleMissing(global, response);
}});
}
@ -567,6 +615,8 @@ public class GlobalSession implements CredentialsSource {
private void wipeServer(final CredentialsSource credentials, final WipeServerDelegate wipeDelegate) {
SyncStorageRequest request;
final GlobalSession self = this;
try {
request = new SyncStorageRequest(config.storageURL(false));
} catch (URISyntaxException ex) {
@ -590,7 +640,8 @@ public class GlobalSession implements CredentialsSource {
@Override
public void handleRequestFailure(SyncStorageResponse response) {
Log.w(LOG_TAG, "Got request failure " + response.getStatusCode() + " in wipeServer.");
// TODO: process HTTP failures here to pick up backoffs etc.
// Process HTTP failures here to pick up backoffs, etc.
self.interpretHTTPFailure(response.httpResponse());
wipeDelegate.onWipeFailed(new HTTPFailureException(response));
}
@ -608,8 +659,13 @@ public class GlobalSession implements CredentialsSource {
request.delete();
}
/**
* Reset our state. Clear our sync ID, reset each engine, drop any
* cached records.
*/
private void resetClient() {
// TODO Auto-generated method stub
// TODO: futz with config?!
// TODO: engines?!
}

View File

@ -49,6 +49,18 @@ public class HTTPFailureException extends SyncException {
this.response = response;
}
@Override
public String toString() {
String errorMessage = "[unknown error message]";
try {
errorMessage = this.response.getErrorMessage();
} catch (Exception e) {
// Oh well.
}
return "<HTTPFailureException " + this.response.getStatusCode() +
" :: (" + errorMessage + ")>";
}
@Override
public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
switch (response.getStatusCode()) {

View File

@ -40,6 +40,8 @@ package org.mozilla.gecko.sync;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Set;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.sync.delegates.InfoCollectionsDelegate;
@ -59,20 +61,20 @@ public class InfoCollections implements SyncStorageRequestDelegate {
private ExtendedJSONObject record;
// Fields.
private HashMap<String, Double> timestamps;
// Rather than storing decimal/double timestamps, as provided by the
// server, we convert immediately to milliseconds since epoch.
private HashMap<String, Long> timestamps;
public HashMap<String, Double> getTimestamps() {
public HashMap<String, Long> getTimestamps() {
if (!this.wasSuccessful()) {
throw new IllegalStateException("No record fetched.");
}
return this.timestamps;
}
// TODO
// public Iterable<String> changedCollections(HashMap<String, Long> formerTimestamps) {
// for (Entry<String, Long> oldEntry : formerTimestamps.entrySet()) {
//}
// }
public Long getTimestamp(String collection) {
return this.getTimestamps().get(collection);
}
public boolean wasSuccessful() {
return this.response.wasSuccessful() &&
@ -98,9 +100,15 @@ public class InfoCollections implements SyncStorageRequestDelegate {
private void doFetch() {
try {
SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.infoURL);
final SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.infoURL);
r.delegate = this;
r.get();
// TODO: it might be nice to make Resource include its
// own thread pool, and automatically run asynchronously.
ThreadPool.run(new Runnable() {
@Override
public void run() {
r.get();
}});
} catch (URISyntaxException e) {
callback.handleError(e);
}
@ -126,8 +134,28 @@ public class InfoCollections implements SyncStorageRequestDelegate {
this.response = response;
this.setRecord(response.jsonObjectBody());
Log.i(LOG_TAG, "info/collections is " + this.record.toJSONString());
HashMap<String, Double> map = new HashMap<String, Double>();
map.putAll((HashMap<String, Double>) this.record.object);
HashMap<String, Long> map = new HashMap<String, Long>();
Set<Entry<String, Object>> entrySet = this.record.object.entrySet();
for (Entry<String, Object> entry : entrySet) {
// These objects are most likely going to be Doubles. Regardless, we
// want to get them in a more sane time format.
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Double) {
map.put(key, Utils.decimalSecondsToMilliseconds((Double) value));
continue;
}
if (value instanceof Long) {
map.put(key, Utils.decimalSecondsToMilliseconds((Long) value));
continue;
}
if (value instanceof Integer) {
map.put(key, Utils.decimalSecondsToMilliseconds((Integer) value));
continue;
}
Log.w(LOG_TAG, "Skipping info/collections entry for " + key);
}
this.timestamps = map;
}

View File

@ -56,8 +56,7 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
public boolean isModified;
protected boolean isNew;
// Fetched objects.
protected SyncStorageResponse response;
// Fetched object.
private CryptoRecord record;
// Fields.
@ -77,12 +76,8 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
}
public void fetch(MetaGlobalDelegate callback) {
if (this.response == null) {
this.callback = callback;
this.doFetch();
return;
}
callback.deferred().handleSuccess(this);
this.callback = callback;
this.doFetch();
}
private void doFetch() {
@ -96,10 +91,6 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
}
}
public SyncStorageResponse getResponse() {
return this.response;
}
public void upload(MetaGlobalDelegate callback) {
try {
this.isUploading = true;
@ -125,7 +116,6 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
}
private void unpack(SyncStorageResponse response) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
this.response = response;
this.setRecord(response.jsonObjectBody());
Log.i(LOG_TAG, "meta/global is " + record.payload.toJSONString());
this.isModified = false;
@ -180,7 +170,7 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
private void handleUploadSuccess(SyncStorageResponse response) {
this.isModified = false;
this.callback.handleSuccess(this);
this.callback.handleSuccess(this, response);
this.callback = null;
}
@ -188,7 +178,7 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
if (response.wasSuccessful()) {
try {
this.unpack(response);
this.callback.handleSuccess(this);
this.callback.handleSuccess(this, response);
this.callback = null;
} catch (Exception e) {
this.callback.handleError(e);
@ -202,8 +192,7 @@ public class MetaGlobal implements SyncStorageRequestDelegate {
public void handleRequestFailure(SyncStorageResponse response) {
if (response.getStatusCode() == 404) {
this.response = response;
this.callback.handleMissing(this);
this.callback.handleMissing(this, response);
this.callback = null;
return;
}

View File

@ -19,7 +19,7 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chenxia Liu <liuche@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -35,29 +35,12 @@
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.setup.activities;
package org.mozilla.gecko.sync;
import org.mozilla.gecko.R;
public class NonArrayJSONException extends UnexpectedJSONException {
private static final long serialVersionUID = 5582918057432365749L;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
public class SetupWaitingActivity extends Activity {
private Context mContext;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sync_setup_jpake_waiting);
mContext = this.getApplicationContext();
public NonArrayJSONException(Object object) {
super(object);
}
public void cancelClickHandler(View target) {
setResult(RESULT_CANCELED, null);
finish();
}
}

View File

@ -19,7 +19,7 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -37,10 +37,10 @@
package org.mozilla.gecko.sync;
public class NonObjectJSONException extends Exception {
private static final long serialVersionUID = 435366246452253073L;
Object obj;
public class NonObjectJSONException extends UnexpectedJSONException {
private static final long serialVersionUID = 2214238763035650087L;
public NonObjectJSONException(Object object) {
obj = object;
super(object);
}
}

View File

@ -0,0 +1,66 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync;
import android.content.Context;
import android.content.SharedPreferences;
/**
* Implement PrefsSource to allow other components to fetch a SharedPreferences
* instance via a Context that you provide.
*
* This allows components to use SharedPreferences without being tightly
* coupled to an Activity.
*
* @author rnewman
*
*/
public interface PrefsSource {
public Context getContext();
/**
* Return a SharedPreferences instance.
* @param name
* A String, used to identify a preferences 'branch'. Must not be null.
* @param mode
* A bitmask mode, as described in http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29.
* @return
* A new or existing SharedPreferences instance.
*/
public SharedPreferences getPrefs(String name, int mode);
}

View File

@ -39,12 +39,168 @@ package org.mozilla.gecko.sync;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Set;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;
public class SyncConfiguration implements CredentialsSource {
public class EditorBranch implements Editor {
private String prefix;
private Editor editor;
public EditorBranch(SyncConfiguration config, String prefix) {
if (!prefix.endsWith(".")) {
throw new IllegalArgumentException("No trailing period in prefix.");
}
this.prefix = prefix;
this.editor = config.getEditor();
}
@Override
public void apply() {
this.editor.apply();
}
@Override
public Editor clear() {
this.editor = this.editor.clear();
return this;
}
@Override
public boolean commit() {
return this.editor.commit();
}
@Override
public Editor putBoolean(String key, boolean value) {
this.editor = this.editor.putBoolean(prefix + key, value);
return this;
}
@Override
public Editor putFloat(String key, float value) {
this.editor = this.editor.putFloat(prefix + key, value);
return this;
}
@Override
public Editor putInt(String key, int value) {
this.editor = this.editor.putInt(prefix + key, value);
return this;
}
@Override
public Editor putLong(String key, long value) {
this.editor = this.editor.putLong(prefix + key, value);
return this;
}
@Override
public Editor putString(String key, String value) {
this.editor = this.editor.putString(prefix + key, value);
return this;
}
// Not marking as Override, because Android <= 10 doesn't have
// putStringSet. Neither can we implement it.
public Editor putStringSet(String key, Set<String> value) {
throw new RuntimeException("putStringSet not available.");
}
@Override
public Editor remove(String key) {
this.editor = this.editor.remove(prefix + key);
return this;
}
}
/**
* A wrapper around a portion of the SharedPreferences space.
*
* @author rnewman
*
*/
public class ConfigurationBranch implements SharedPreferences {
private SyncConfiguration config;
private String prefix; // Including trailing period.
public ConfigurationBranch(SyncConfiguration syncConfiguration,
String prefix) {
if (!prefix.endsWith(".")) {
throw new IllegalArgumentException("No trailing period in prefix.");
}
this.config = syncConfiguration;
this.prefix = prefix;
}
@Override
public boolean contains(String key) {
return config.getPrefs().contains(prefix + key);
}
@Override
public Editor edit() {
return new EditorBranch(config, prefix);
}
@Override
public Map<String, ?> getAll() {
// Not implemented. TODO
return null;
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return config.getPrefs().getBoolean(prefix + key, defValue);
}
@Override
public float getFloat(String key, float defValue) {
return config.getPrefs().getFloat(prefix + key, defValue);
}
@Override
public int getInt(String key, int defValue) {
return config.getPrefs().getInt(prefix + key, defValue);
}
@Override
public long getLong(String key, long defValue) {
return config.getPrefs().getLong(prefix + key, defValue);
}
@Override
public String getString(String key, String defValue) {
return config.getPrefs().getString(prefix + key, defValue);
}
// Not marking as Override, because Android <= 10 doesn't have
// getStringSet. Neither can we implement it.
public Set<String> getStringSet(String key, Set<String> defValue) {
throw new RuntimeException("getStringSet not available.");
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
config.getPrefs().registerOnSharedPreferenceChangeListener(listener);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
config.getPrefs().unregisterOnSharedPreferenceChangeListener(listener);
}
}
public static final String DEFAULT_USER_API = "https://auth.services.mozilla.com/user/1.0/";
private static final String LOG_TAG = "SyncConfiguration";
@ -52,7 +208,7 @@ public class SyncConfiguration implements CredentialsSource {
// These must be set in GlobalSession's constructor.
public String userAPI;
public URI serverURL;
public URI clusterURL;
protected URI clusterURL;
public String username;
public KeyBundle syncKeyBundle;
@ -62,8 +218,73 @@ public class SyncConfiguration implements CredentialsSource {
public String password;
public String syncID;
// Fields that maintain a reference to a SharedPreferences instance, used for
// persistence.
// Behavior is undefined if the PrefsSource is switched out in flight.
public String prefsPath;
public PrefsSource prefsSource;
public SyncConfiguration() {
/**
* Create a new SyncConfiguration instance. Pass in a PrefsSource to
* provide access to preferences.
*
* @param prefsPath
* @param context
*/
public SyncConfiguration(String prefsPath, PrefsSource prefsSource) {
this.prefsPath = prefsPath;
this.prefsSource = prefsSource;
this.loadFromPrefs(getPrefs());
}
public SharedPreferences getPrefs() {
Log.d(LOG_TAG, "Returning prefs for " + prefsPath);
return prefsSource.getPrefs(prefsPath, Utils.SHARED_PREFERENCES_MODE);
}
/**
* Return a convenient accessor for part of prefs.
* @param prefix
* @return
*/
public ConfigurationBranch getBranch(String prefix) {
return new ConfigurationBranch(this, prefix);
}
public void loadFromPrefs(SharedPreferences prefs) {
if (prefs.contains("clusterURL")) {
String u = prefs.getString("clusterURL", null);
try {
clusterURL = new URI(u);
Log.i(LOG_TAG, "Set clusterURL from bundle: " + u);
} catch (URISyntaxException e) {
Log.w(LOG_TAG, "Ignoring bundle clusterURL (" + u + "): invalid URI.", e);
}
}
if (prefs.contains("syncID")) {
syncID = prefs.getString("syncID", null);
Log.i(LOG_TAG, "Set syncID from bundle: " + syncID);
}
// TODO: MetaGlobal, password, infoCollections, collectionKeys.
}
public void persistToPrefs() {
this.persistToPrefs(this.getPrefs());
}
public void persistToPrefs(SharedPreferences prefs) {
Editor edit = prefs.edit();
if (clusterURL == null) {
edit.remove("clusterURL");
} else {
edit.putString("clusterURL", clusterURL.toASCIIString());
}
if (syncID != null) {
edit.putString("syncID", syncID);
}
edit.commit();
// TODO: keys.
}
@Override
@ -136,21 +357,57 @@ public class SyncConfiguration implements CredentialsSource {
return wboURI("crypto", "keys");
}
public URI getClusterURL() {
return clusterURL;
}
public String getClusterURLString() {
if (clusterURL == null) {
return null;
}
return clusterURL.toASCIIString();
}
public void setAndPersistClusterURL(URI u, SharedPreferences prefs) {
boolean shouldPersist = (prefs != null) && (clusterURL == null);
Log.d(LOG_TAG, "Setting cluster URL to " + u.toASCIIString() +
(shouldPersist ? ". Persisting." : ". Not persisting."));
clusterURL = u;
if (shouldPersist) {
Editor edit = prefs.edit();
edit.putString("clusterURL", clusterURL.toASCIIString());
edit.commit();
}
}
public void setClusterURL(URI u) {
setClusterURL(u, this.getPrefs());
}
public void setClusterURL(URI u, SharedPreferences prefs) {
if (u == null) {
Log.w(LOG_TAG, "Refusing to set cluster URL to null.");
return;
}
URI uri = u.normalize();
if (uri.toASCIIString().endsWith("/")) {
this.clusterURL = u;
setAndPersistClusterURL(u, prefs);
return;
}
this.clusterURL = uri.resolve("/");
Log.i(LOG_TAG, "Set cluster URL to " + this.clusterURL.toASCIIString() + ", given input " + u.toASCIIString());
setAndPersistClusterURL(uri.resolve("/"), prefs);
Log.i(LOG_TAG, "Set cluster URL to " + clusterURL.toASCIIString() + ", given input " + u.toASCIIString());
}
public void setClusterURL(String url) throws URISyntaxException {
this.setClusterURL(new URI(url));
}
/**
* Used for direct management of related prefs.
* @return
*/
public Editor getEditor() {
return this.getPrefs().edit();
}
}

View File

@ -37,14 +37,26 @@
package org.mozilla.gecko.sync;
import java.io.IOException;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.sync.SyncConfiguration.ConfigurationBranch;
import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
import android.content.SharedPreferences.Editor;
import android.util.Log;
public class SynchronizerConfiguration {
private static final String LOG_TAG = "SynchronizerConfiguration";
public String syncID;
public RepositorySessionBundle remoteBundle;
public RepositorySessionBundle localBundle;
public SynchronizerConfiguration(ConfigurationBranch config) throws NonObjectJSONException, IOException, ParseException {
this.load(config);
}
public SynchronizerConfiguration(String syncID, RepositorySessionBundle remoteBundle, RepositorySessionBundle localBundle) {
this.syncID = syncID;
this.remoteBundle = remoteBundle;
@ -58,4 +70,40 @@ public class SynchronizerConfiguration {
out[2] = localBundle.toJSONString();
return out;
}
// This should get partly shuffled back into SyncConfiguration, I think.
public void load(ConfigurationBranch config) throws NonObjectJSONException, IOException, ParseException {
if (config == null) {
throw new IllegalArgumentException("config cannot be null.");
}
String remoteJSON = config.getString("remote", null);
String localJSON = config.getString("local", null);
RepositorySessionBundle rB = new RepositorySessionBundle(remoteJSON);
RepositorySessionBundle lB = new RepositorySessionBundle(localJSON);
if (remoteJSON == null) {
rB.setTimestamp(0);
}
if (localJSON == null) {
lB.setTimestamp(0);
}
syncID = config.getString("syncID", null);
remoteBundle = rB;
localBundle = lB;
Log.i(LOG_TAG, "Initialized SynchronizerConfiguration. syncID: " + syncID + ", remoteBundle: " + remoteBundle + ", localBundle: " + localBundle);
}
public void persist(ConfigurationBranch config) {
if (config == null) {
throw new IllegalArgumentException("config cannot be null.");
}
String jsonRemote = remoteBundle.toJSONString();
String jsonLocal = localBundle.toJSONString();
Editor editor = config.edit();
editor.putString("remote", jsonRemote);
editor.putString("local", jsonLocal);
editor.putString("syncID", syncID);
// Synchronous.
editor.commit();
}
}

View File

@ -0,0 +1,47 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync;
public class UnexpectedJSONException extends Exception {
private static final long serialVersionUID = 4797570033096443169L;
public Object obj;
public UnexpectedJSONException(Object object) {
obj = object;
}
}

View File

@ -38,12 +38,45 @@
package org.mozilla.gecko.sync;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Random;
import org.mozilla.apache.commons.codec.binary.Base32;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.crypto.Cryptographer;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
public class Utils {
private static final String LOG_TAG = "Utils";
// See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29>
public static final int SHARED_PREFERENCES_MODE = 0;
// We don't really have a trace logger, so use this to toggle
// some debug logging.
// This is awful. I'm so sorry.
public static boolean ENABLE_TRACE_LOGGING = true;
// If true, log to System.out as well as using Android's Log.* calls.
public static boolean LOG_TO_STDOUT = false;
public static void logToStdout(String... s) {
if (LOG_TO_STDOUT) {
for (String string : s) {
System.out.print(string);
}
System.out.println("");
}
}
public static String generateGuid() {
byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false);
return new String(encodedBytes).replace("+", "-").replace("/", "_");
@ -55,4 +88,168 @@ public class Utils {
random.nextBytes(bytes);
return bytes;
}
/*
* Helper to convert Byte Array to a Hex String
* Input: byte[] array
* Output: Hex string
*/
public static String byte2hex(byte[] b) {
// String Buffer can be used instead
String hs = "";
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (java.lang.Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1) {
hs = hs + "0" + stmp;
} else {
hs = hs + stmp;
}
if (n < b.length - 1) {
hs = hs + "";
}
}
return hs;
}
/*
* Helper for array concatenation.
* Input: At least two byte[]
* Output: A concatenated version of them
*/
public static byte[] concatAll(byte[] first, byte[]... rest) {
int totalLength = first.length;
for (byte[] array : rest) {
totalLength += array.length;
}
byte[] result = Arrays.copyOf(first, totalLength);
int offset = first.length;
for (byte[] array : rest) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
return result;
}
/**
* Utility for Base64 decoding. Should ensure that the correct
* Apache Commons version is used.
*
* @param base64
* An input string. Will be decoded as UTF-8.
* @return
* A byte array of decoded values.
* @throws UnsupportedEncodingException
* Should not occur.
*/
public static byte[] decodeBase64(String base64) throws UnsupportedEncodingException {
return Base64.decodeBase64(base64.getBytes("UTF-8"));
}
/*
* Decode a friendly base32 string.
*/
public static byte[] decodeFriendlyBase32(String base32) {
Base32 converter = new Base32();
return converter.decode(base32.replace('8', 'l').replace('9', 'o')
.toUpperCase());
}
/*
* Helper to convert Hex String to Byte Array
* Input: Hex string
* Output: byte[] version of hex string
*/
public static byte[] hex2Byte(String str)
{
if (str.length() % 2 == 1) {
str = "0" + str;
}
byte[] bytes = new byte[str.length() / 2];
for (int i = 0; i < bytes.length; i++)
{
bytes[i] = (byte) Integer
.parseInt(str.substring(2 * i, 2 * i + 2), 16);
}
return bytes;
}
public static String millisecondsToDecimalSecondsString(long ms) {
return new BigDecimal(ms).movePointLeft(3).toString();
}
// This lives until Bug 708956 lands, and we don't have to do it any more.
public static long decimalSecondsToMilliseconds(String decimal) {
try {
return new BigDecimal(decimal).movePointRight(3).longValue();
} catch (Exception e) {
return -1;
}
}
// Oh, Java.
public static long decimalSecondsToMilliseconds(Double decimal) {
// Truncates towards 0.
return (long)(decimal * 1000);
}
public static long decimalSecondsToMilliseconds(Long decimal) {
return decimal * 1000;
}
public static long decimalSecondsToMilliseconds(Integer decimal) {
return (long)(decimal * 1000);
}
public static String getPrefsPath(String username, String serverURL)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
return "sync.prefs." + Cryptographer.sha1Base32(serverURL + ":" + username);
}
public static SharedPreferences getSharedPreferences(Context context, String username, String serverURL) throws NoSuchAlgorithmException, UnsupportedEncodingException {
String prefsPath = getPrefsPath(username, serverURL);
Log.d(LOG_TAG, "Shared preferences: " + prefsPath);
return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE);
}
/**
* Populate null slots in the provided array from keys in the provided Map.
* Set values in the map to be the new indices.
*
* @param dest
* @param source
* @throws Exception
*/
public static void fillArraySpaces(String[] dest, HashMap<String, Long> source) throws Exception {
int i = 0;
int c = dest.length;
int needed = source.size();
if (needed == 0) {
return;
}
if (needed > c) {
throw new Exception("Need " + needed + " array spaces, have no more than " + c);
}
for (String key : source.keySet()) {
while (i < c) {
if (dest[i] == null) {
// Great!
dest[i] = key;
source.put(key, (long) i);
break;
}
++i;
}
}
if (i >= c) {
throw new Exception("Could not fill array spaces.");
}
}
}

View File

@ -55,6 +55,7 @@ import javax.crypto.spec.SecretKeySpec;
import org.mozilla.apache.commons.codec.binary.Base32;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.Utils;
/*
* Implements the basic required cryptography options.

View File

@ -45,6 +45,8 @@ import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.mozilla.gecko.sync.Utils;
/*
* A standards-compliant implementation of RFC 5869

View File

@ -44,6 +44,7 @@ import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.Utils;
public class KeyBundle {

View File

@ -1,140 +0,0 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import org.mozilla.apache.commons.codec.binary.Base32;
import org.mozilla.apache.commons.codec.binary.Base64;
public class Utils {
/*
* Helper to convert Hex String to Byte Array
* Input: Hex string
* Output: byte[] version of hex string
*/
public static byte[] hex2Byte(String str)
{
if (str.length() % 2 == 1) {
str = "0" + str;
}
byte[] bytes = new byte[str.length() / 2];
for (int i = 0; i < bytes.length; i++)
{
bytes[i] = (byte) Integer
.parseInt(str.substring(2 * i, 2 * i + 2), 16);
}
return bytes;
}
/*
* Helper to convert Byte Array to a Hex String
* Input: byte[] array
* Output: Hex string
*/
public static String byte2hex(byte[] b) {
// String Buffer can be used instead
String hs = "";
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (java.lang.Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1) {
hs = hs + "0" + stmp;
} else {
hs = hs + stmp;
}
if (n < b.length - 1) {
hs = hs + "";
}
}
return hs;
}
/*
* Helper for array concatenation.
* Input: At least two byte[]
* Output: A concatenated version of them
*/
public static byte[] concatAll(byte[] first, byte[]... rest) {
int totalLength = first.length;
for (byte[] array : rest) {
totalLength += array.length;
}
byte[] result = Arrays.copyOf(first, totalLength);
int offset = first.length;
for (byte[] array : rest) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
return result;
}
/*
* Decode a friendly base32 string.
*/
public static byte[] decodeFriendlyBase32(String base32) {
Base32 converter = new Base32();
return converter.decode(base32.replace('8', 'l').replace('9', 'o')
.toUpperCase());
}
/**
* Utility for Base64 decoding. Should ensure that the correct
* Apache Commons version is used.
*
* @param base64
* An input string. Will be decoded as UTF-8.
* @return
* A byte array of decoded values.
* @throws UnsupportedEncodingException
* Should not occur.
*/
public static byte[] decodeBase64(String base64) throws UnsupportedEncodingException {
return Base64.decodeBase64(base64.getBytes("UTF-8"));
}
}

View File

@ -46,16 +46,16 @@ import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.CryptoInfo;
import org.mozilla.gecko.sync.crypto.Cryptographer;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.crypto.Utils;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.cryptographer.CryptoStatusBundle.CryptoStatus;
/*

View File

@ -40,9 +40,18 @@ package org.mozilla.gecko.sync.delegates;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
public interface GlobalSessionCallback {
/**
* Request that no further syncs occur within the next `backoff` milliseconds.
* @param backoff a duration in milliseconds.
*/
void requestBackoff(long backoff);
void handleAborted(GlobalSession globalSession, String reason);
void handleError(GlobalSession globalSession, Exception ex);
void handleSuccess(GlobalSession globalSession);
void handleStageCompleted(Stage currentState, GlobalSession globalSession);
boolean shouldBackOff();
}

View File

@ -41,8 +41,8 @@ import org.mozilla.gecko.sync.MetaGlobal;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
public interface MetaGlobalDelegate {
public void handleSuccess(MetaGlobal global);
public void handleMissing(MetaGlobal global);
public void handleSuccess(MetaGlobal global, SyncStorageResponse response);
public void handleMissing(MetaGlobal global, SyncStorageResponse response);
public void handleFailure(SyncStorageResponse response);
public void handleError(Exception e);
public MetaGlobalDelegate deferred();

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,7 @@ import android.util.Log;
import ch.boye.httpclientandroidlib.HttpEntity;
public class JPakeRequest implements Resource {
private static String LOG_TAG = "JPAKE_REQUEST";
private static String LOG_TAG = "JPakeRequest";
private BaseResource resource;
public JPakeRequestDelegate delegate;

View File

@ -38,10 +38,12 @@
package org.mozilla.gecko.sync.middleware;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.RecordFactory;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
@ -159,6 +161,13 @@ public class Crypto5MiddlewareRepositorySession extends RepositorySession {
public void onFetchCompleted(long end) {
next.onFetchCompleted(end);
}
@Override
public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
// Synchronously perform *our* work, passing through appropriately.
RepositorySessionFetchRecordsDelegate deferredNext = next.deferredFetchDelegate(executor);
return new DecryptingTransformingFetchDelegate(deferredNext, keyBundle, recordFactory);
}
}
private DecryptingTransformingFetchDelegate makeUnwrappingDelegate(RepositorySessionFetchRecordsDelegate inner) {
@ -193,24 +202,39 @@ public class Crypto5MiddlewareRepositorySession extends RepositorySession {
}
@Override
public void store(Record record, RepositorySessionStoreDelegate delegate) {
public void setStoreDelegate(RepositorySessionStoreDelegate delegate) {
// TODO: it remains to be seen how this will work.
inner.setStoreDelegate(delegate);
this.delegate = delegate; // So we can handle errors without involving inner.
}
@Override
public void store(Record record) throws NoStoreDelegateException {
if (delegate == null) {
throw new NoStoreDelegateException();
}
CryptoRecord rec = record.getPayload();
rec.keyBundle = this.keyBundle;
try {
rec.encrypt();
} catch (UnsupportedEncodingException e) {
delegate.onStoreFailed(e);
delegate.onRecordStoreFailed(e);
return;
} catch (CryptoException e) {
delegate.onStoreFailed(e);
delegate.onRecordStoreFailed(e);
return;
}
// TODO: it remains to be seen how this will work.
inner.store(rec, delegate);
// Allow the inner session to do delegate handling.
inner.store(rec);
}
@Override
public void wipe(RepositorySessionWipeDelegate delegate) {
inner.wipe(delegate);
}
@Override
public void storeDone() {
inner.storeDone();
}
}

View File

@ -19,7 +19,7 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Richard Newman <rnewman@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -70,6 +70,7 @@ import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
import ch.boye.httpclientandroidlib.impl.conn.tsccm.ThreadSafeClientConnManager;
import ch.boye.httpclientandroidlib.params.HttpConnectionParams;
import ch.boye.httpclientandroidlib.params.HttpParams;
import ch.boye.httpclientandroidlib.params.HttpProtocolParams;
import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
import ch.boye.httpclientandroidlib.protocol.HttpContext;
@ -80,18 +81,44 @@ import ch.boye.httpclientandroidlib.protocol.HttpContext;
* Exposes simple get/post/put/delete methods.
*/
public class BaseResource implements Resource {
public static boolean rewriteLocalhost = true;
private static final String LOG_TAG = "BaseResource";
protected URI uri;
protected BasicHttpContext context;
protected DefaultHttpClient client;
public ResourceDelegate delegate;
protected HttpRequestBase request;
public String charset = "utf-8";
public BaseResource(String uri) throws URISyntaxException {
this(new URI(uri));
this(uri, rewriteLocalhost);
}
public BaseResource(URI uri) {
this.uri = uri;
this(uri, rewriteLocalhost);
}
public BaseResource(String uri, boolean rewrite) throws URISyntaxException {
this(new URI(uri), rewrite);
}
public BaseResource(URI uri, boolean rewrite) {
if (rewrite && uri.getHost().equals("localhost")) {
// Rewrite localhost URIs to refer to the special Android emulator loopback passthrough interface.
Log.d(LOG_TAG, "Rewriting " + uri + " to point to 10.0.2.2.");
try {
this.uri = new URI(uri.getScheme(), uri.getUserInfo(), "10.0.2.2", uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
} catch (URISyntaxException e) {
Log.e(LOG_TAG, "Got error rewriting URI for Android emulator.", e);
}
} else {
this.uri = uri;
}
}
public URI getURI() {
return this.uri;
}
/**
@ -129,6 +156,7 @@ public class BaseResource implements Resource {
HttpParams params = client.getParams();
HttpConnectionParams.setConnectionTimeout(params, delegate.connectionTimeout());
HttpConnectionParams.setSoTimeout(params, delegate.socketTimeout());
HttpProtocolParams.setContentCharset(params, charset);
delegate.addHeaders(request, client);
}

View File

@ -40,12 +40,12 @@ package org.mozilla.gecko.sync.net;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.util.Scanner;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpEntity;
@ -75,10 +75,15 @@ public class SyncResponse {
return this.getStatusCode() == 200;
}
private String body = null;
public String body() throws IllegalStateException, IOException {
if (body != null) {
return body;
}
InputStreamReader is = new InputStreamReader(this.response.getEntity().getContent());
// Oh, Java, you are so evil.
return new Scanner(is).useDelimiter("\\A").next();
body = new Scanner(is).useDelimiter("\\A").next();
return body;
}
/**
@ -92,6 +97,10 @@ public class SyncResponse {
*/
public Object jsonBody() throws IllegalStateException, IOException,
ParseException {
if (body != null) {
// Do it from the cached String.
ExtendedJSONObject.parse(body);
}
HttpEntity entity = this.response.getEntity();
if (entity == null) {
return null;
@ -133,15 +142,6 @@ public class SyncResponse {
return this.getIntegerHeader("x-weave-backoff");
}
// This lives until Bug 708956 lands, and we don't have to do it any more.
public static long decimalSecondsToMilliseconds(String decimal) {
try {
return new BigDecimal(decimal).movePointRight(3).longValue();
} catch (Exception e) {
return -1;
}
}
/**
* The timestamp returned from a Sync server is a decimal number of seconds,
* e.g., 1323393518.04.
@ -157,7 +157,7 @@ public class SyncResponse {
return -1;
}
return decimalSecondsToMilliseconds(this.response.getFirstHeader(h).getValue());
return Utils.decimalSecondsToMilliseconds(this.response.getFirstHeader(h).getValue());
}
public int weaveRecords() throws NumberFormatException {

View File

@ -41,6 +41,9 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import android.util.Log;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
@ -51,6 +54,46 @@ import ch.boye.httpclientandroidlib.params.CoreProtocolPNames;
public class SyncStorageRequest implements Resource {
public static HashMap<String, String> SERVER_ERROR_MESSAGES;
static {
HashMap<String, String> errors = new HashMap<String, String>();
// Sync protocol errors.
errors.put("1", "Illegal method/protocol");
errors.put("2", "Incorrect/missing CAPTCHA");
errors.put("3", "Invalid/missing username");
errors.put("4", "Attempt to overwrite data that can't be overwritten (such as creating a user ID that already exists)");
errors.put("5", "User ID does not match account in path");
errors.put("6", "JSON parse failure");
errors.put("7", "Missing password field");
errors.put("8", "Invalid Weave Basic Object");
errors.put("9", "Requested password not strong enough");
errors.put("10", "Invalid/missing password reset code");
errors.put("11", "Unsupported function");
errors.put("12", "No email address on file");
errors.put("13", "Invalid collection");
errors.put("14", "User over quota");
errors.put("15", "The email does not match the username");
errors.put("16", "Client upgrade required");
errors.put("255", "An unexpected server error occurred: pool is empty.");
// Infrastructure-generated errors.
errors.put("\"server issue: getVS failed\"", "server issue: getVS failed");
errors.put("\"server issue: prefix not set\"", "server issue: prefix not set");
errors.put("\"server issue: host header not received from client\"", "server issue: host header not received from client");
errors.put("\"server issue: database lookup failed\"", "server issue: database lookup failed");
errors.put("\"server issue: database is not healthy\"", "server issue: database is not healthy");
errors.put("\"server issue: database not in pool\"", "server issue: database not in pool");
errors.put("\"server issue: database marked as down\"", "server issue: database marked as down");
SERVER_ERROR_MESSAGES = errors;
}
public static String getServerErrorMessage(String body) {
if (SERVER_ERROR_MESSAGES.containsKey(body)) {
return SERVER_ERROR_MESSAGES.get(body);
}
return body;
}
/**
* @param uri
* @throws URISyntaxException
@ -72,6 +115,7 @@ public class SyncStorageRequest implements Resource {
* A ResourceDelegate that mediates between Resource-level notifications and the SyncStorageRequest.
*/
public class SyncStorageResourceDelegate extends SyncResourceDelegate {
private static final String LOG_TAG = "SyncStorageResourceDelegate";
protected SyncStorageRequest request;
SyncStorageResourceDelegate(SyncStorageRequest request) {
@ -86,11 +130,18 @@ public class SyncStorageRequest implements Resource {
@Override
public void handleHttpResponse(HttpResponse response) {
Log.d(LOG_TAG, "SyncStorageResourceDelegate handling response: " + response.getStatusLine() + ".");
SyncStorageRequestDelegate d = this.request.delegate;
SyncStorageResponse res = new SyncStorageResponse(response);
if (res.wasSuccessful()) {
d.handleRequestSuccess(res);
} else {
Log.w(LOG_TAG, "HTTP request failed.");
try {
Log.w(LOG_TAG, "HTTP response body: " + res.getErrorMessage());
} catch (Exception e) {
Log.e(LOG_TAG, "Can't fetch HTTP response body.", e);
}
d.handleRequestFailure(res);
}
}
@ -122,7 +173,7 @@ public class SyncStorageRequest implements Resource {
}
}
public static String USER_AGENT = "Firefox AndroidSync 0.1";
public static String USER_AGENT = "Firefox AndroidSync 0.2";
protected SyncResourceDelegate resourceDelegate;
public SyncStorageRequestDelegate delegate;
protected BaseResource resource;

View File

@ -38,51 +38,63 @@
package org.mozilla.gecko.sync.net;
import java.io.IOException;
import java.util.HashMap;
import android.util.Log;
import ch.boye.httpclientandroidlib.HttpResponse;
public class SyncStorageResponse extends SyncResponse {
// Server responses on which we want to switch.
static final int SERVER_RESPONSE_OVER_QUOTA = 14;
// Higher-level interpretations of response contents.
public enum Reason {
SUCCESS,
OVER_QUOTA,
UNAUTHORIZED_OR_REASSIGNED,
SERVICE_UNAVAILABLE,
BAD_REQUEST,
UNKNOWN
private static final String LOG_TAG = "SyncStorageResponse";
public static HashMap<String, String> SERVER_ERROR_MESSAGES;
static {
HashMap<String, String> errors = new HashMap<String, String>();
// Sync protocol errors.
errors.put("1", "Illegal method/protocol");
errors.put("2", "Incorrect/missing CAPTCHA");
errors.put("3", "Invalid/missing username");
errors.put("4", "Attempt to overwrite data that can't be overwritten (such as creating a user ID that already exists)");
errors.put("5", "User ID does not match account in path");
errors.put("6", "JSON parse failure");
errors.put("7", "Missing password field");
errors.put("8", "Invalid Weave Basic Object");
errors.put("9", "Requested password not strong enough");
errors.put("10", "Invalid/missing password reset code");
errors.put("11", "Unsupported function");
errors.put("12", "No email address on file");
errors.put("13", "Invalid collection");
errors.put("14", "User over quota");
errors.put("15", "The email does not match the username");
errors.put("255", "An unexpected server error occurred: pool is empty.");
// Infrastructure-generated errors.
errors.put("\"server issue: getVS failed\"", "server issue: getVS failed");
errors.put("\"server issue: prefix not set\"", "server issue: prefix not set");
errors.put("\"server issue: host header not received from client\"", "server issue: host header not received from client");
errors.put("\"server issue: database lookup failed\"", "server issue: database lookup failed");
errors.put("\"server issue: database is not healthy\"", "server issue: database is not healthy");
errors.put("\"server issue: database not in pool\"", "server issue: database not in pool");
errors.put("\"server issue: database marked as down\"", "server issue: database marked as down");
SERVER_ERROR_MESSAGES = errors;
}
public static String getServerErrorMessage(String body) {
Log.d(LOG_TAG, "Looking up message for body \"" + body + "\"");
if (SERVER_ERROR_MESSAGES.containsKey(body)) {
return SERVER_ERROR_MESSAGES.get(body);
}
return body;
}
public SyncStorageResponse(HttpResponse res) {
this.response = res;
}
/**
* Return the high-level definition of the status of this request.
* @return
*/
public Reason reason() {
switch (this.response.getStatusLine().getStatusCode()) {
case 200:
return Reason.SUCCESS;
case 400:
try {
Object body = this.jsonBody();
if (body instanceof Number) {
if (((Number) body).intValue() == SERVER_RESPONSE_OVER_QUOTA) {
return Reason.OVER_QUOTA;
}
}
} catch (Exception e) {
}
return Reason.BAD_REQUEST;
case 401:
return Reason.UNAUTHORIZED_OR_REASSIGNED;
case 503:
return Reason.SERVICE_UNAVAILABLE;
}
return Reason.UNKNOWN;
public String getErrorMessage() throws IllegalStateException, IOException {
return SyncStorageResponse.getServerErrorMessage(this.body().trim());
}
// TODO: Content-Type and Content-Length validation.

View File

@ -0,0 +1,44 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.repositories;
import org.mozilla.gecko.sync.SyncException;
public class NoStoreDelegateException extends SyncException {
private static final long serialVersionUID = 6631689468978422074L;
}

View File

@ -38,6 +38,9 @@
package org.mozilla.gecko.sync.repositories;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
@ -73,6 +76,19 @@ public abstract class RepositorySession {
private static final String LOG_TAG = "RepositorySession";
protected SessionStatus status = SessionStatus.UNSTARTED;
protected Repository repository;
protected RepositorySessionStoreDelegate delegate;
/**
* A queue of Runnables which call out into delegates.
*/
protected ExecutorService delegateQueue = Executors.newSingleThreadExecutor();
/**
* A queue of Runnables which effect storing.
* This includes actual store work, and also the consequences of storeDone.
* This provides strict ordering.
*/
protected ExecutorService storeWorkQueue = Executors.newSingleThreadExecutor();
// The time that the last sync on this collection completed, in milliseconds since epoch.
public long lastSyncTimestamp;
@ -89,7 +105,48 @@ public abstract class RepositorySession {
public abstract void fetchSince(long timestamp, RepositorySessionFetchRecordsDelegate delegate);
public abstract void fetch(String[] guids, RepositorySessionFetchRecordsDelegate delegate);
public abstract void fetchAll(RepositorySessionFetchRecordsDelegate delegate);
public abstract void store(Record record, RepositorySessionStoreDelegate delegate);
/**
* Override this if you wish to short-circuit a sync when you know --
* e.g., by inspecting the database or info/collections -- that no new
* data are available.
*
* @return true if a sync should proceed.
*/
public boolean dataAvailable() {
return true;
}
/*
* Store operations proceed thusly:
*
* * Set a delegate
* * Store an arbitrary number of records. At any time the delegate can be
* notified of an error.
* * Call storeDone to notify the session that no more items are forthcoming.
* * The store delegate will be notified of error or completion.
*
* This arrangement of calls allows for batching at the session level.
*
* Store success calls are not guaranteed.
*/
public void setStoreDelegate(RepositorySessionStoreDelegate delegate) {
Log.d(LOG_TAG, "Setting store delegate to " + delegate);
this.delegate = delegate;
}
public abstract void store(Record record) throws NoStoreDelegateException;
public void storeDone() {
Log.d(LOG_TAG, "Scheduling onStoreCompleted for after storing is done.");
Runnable command = new Runnable() {
@Override
public void run() {
delegate.onStoreCompleted();
}
};
storeWorkQueue.execute(command);
}
public abstract void wipe(RepositorySessionWipeDelegate delegate);
public void unbundle(RepositorySessionBundle bundle) {
@ -128,10 +185,9 @@ public abstract class RepositorySession {
public void begin(RepositorySessionBeginDelegate delegate) {
try {
sharedBegin();
delegate.deferredBeginDelegate().onBeginSucceeded(this);
delegate.deferredBeginDelegate(delegateQueue).onBeginSucceeded(this);
} catch (Exception e) {
delegate.deferredBeginDelegate().onBeginFailed(e);
delegate.deferredBeginDelegate(delegateQueue).onBeginFailed(e);
}
}
@ -168,17 +224,21 @@ public abstract class RepositorySession {
*/
public void abort(RepositorySessionFinishDelegate delegate) {
this.status = SessionStatus.DONE; // TODO: ABORTED?
delegate.deferredFinishDelegate().onFinishSucceeded(this, this.getBundle(null));
delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle(null));
}
public void finish(RepositorySessionFinishDelegate delegate) {
public void finish(final RepositorySessionFinishDelegate delegate) {
if (this.status == SessionStatus.ACTIVE) {
this.status = SessionStatus.DONE;
delegate.deferredFinishDelegate().onFinishSucceeded(this, this.getBundle(null));
delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle(null));
} else {
Log.e(LOG_TAG, "Tried to finish() an unstarted or already finished session");
delegate.deferredFinishDelegate().onFinishFailed(new InvalidSessionTransitionException(null));
Exception e = new InvalidSessionTransitionException(null);
delegate.deferredFinishDelegate(delegateQueue).onFinishFailed(e);
}
Log.i(LOG_TAG, "Shutting down work queues.");
// storeWorkQueue.shutdown();
// delegateQueue.shutdown();
}
public boolean isActive() {
@ -188,5 +248,7 @@ public abstract class RepositorySession {
public void abort() {
// TODO: do something here.
status = SessionStatus.ABORTED;
storeWorkQueue.shutdown();
delegateQueue.shutdown();
}
}

View File

@ -41,6 +41,7 @@ import java.net.URI;
import java.net.URISyntaxException;
import org.mozilla.gecko.sync.CredentialsSource;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import android.content.Context;
@ -57,6 +58,7 @@ public class Server11Repository extends Repository {
private String username;
private String collection;
private String collectionPath;
private URI collectionPathURI;
public CredentialsSource credentialsSource;
public static final String VERSION_PATH_FRAGMENT = "1.1/";
@ -68,13 +70,15 @@ public class Server11Repository extends Repository {
* Username on the server (string)
* @param collection
* Name of the collection (string)
* @throws URISyntaxException
*/
public Server11Repository(String serverURI, String username, String collection, CredentialsSource credentialsSource) {
public Server11Repository(String serverURI, String username, String collection, CredentialsSource credentialsSource) throws URISyntaxException {
this.serverURI = serverURI;
this.username = username;
this.collection = collection;
this.collectionPath = this.serverURI + VERSION_PATH_FRAGMENT + this.username + "/storage/" + this.collection;
this.collectionPathURI = new URI(this.collectionPath);
this.credentialsSource = credentialsSource;
}
@ -84,6 +88,10 @@ public class Server11Repository extends Repository {
delegate.onSessionCreated(new Server11RepositorySession(this));
}
public URI collectionURI() {
return this.collectionPathURI;
}
public URI collectionURI(boolean full, long newer, String ids) throws URISyntaxException {
// Do it this way to make it easier to add more params later.
// It's pretty ugly, I'll grant.
@ -96,7 +104,9 @@ public class Server11Repository extends Repository {
params.append("full=1");
}
if (newer >= 0) {
params.append((full ? "&newer=" : "newer=") + newer);
// 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);

View File

@ -37,15 +37,23 @@
package org.mozilla.gecko.sync.repositories;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.json.simple.JSONArray;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.DelayedWorkTracker;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.UnexpectedJSONException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
import org.mozilla.gecko.sync.net.SyncStorageRequest;
import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.sync.net.WBOCollectionRequestDelegate;
@ -56,11 +64,32 @@ import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelega
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.util.Log;
import ch.boye.httpclientandroidlib.entity.ContentProducer;
import ch.boye.httpclientandroidlib.entity.EntityTemplate;
public class Server11RepositorySession extends RepositorySession {
private static byte[] recordsStart;
private static byte[] recordSeparator;
private static byte[] recordsEnd;
static {
try {
recordsStart = "[\n".getBytes("UTF-8");
recordSeparator = ",\n".getBytes("UTF-8");
recordsEnd = "\n]\n".getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// These won't fail.
}
}
public static final String LOG_TAG = "Server11RepositorySession";
private static final int UPLOAD_BYTE_THRESHOLD = 1024 * 1024; // 1MB.
private static final int UPLOAD_ITEM_THRESHOLD = 50;
private static final int PER_RECORD_OVERHEAD = 2; // Comma, newline.
// {}, newlines, but we get to skip one record overhead.
private static final int PER_BATCH_OVERHEAD = 5 - PER_RECORD_OVERHEAD;
/**
* Convert HTTP request delegate callbacks into fetch callbacks within the
* context of this RepositorySession.
@ -155,6 +184,7 @@ public class Server11RepositorySession extends RepositorySession {
}
}
Server11Repository serverRepository;
public Server11RepositorySession(Repository repository) {
super(repository);
@ -183,10 +213,10 @@ public class Server11RepositorySession extends RepositorySession {
}
private void fetchWithParameters(long newer,
boolean full,
String ids,
SyncStorageRequestDelegate delegate) throws URISyntaxException {
protected void fetchWithParameters(long newer,
boolean full,
String ids,
SyncStorageRequestDelegate delegate) throws URISyntaxException {
URI collectionURI = serverRepository.collectionURI(full, newer, ids);
SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(collectionURI);
@ -221,13 +251,210 @@ public class Server11RepositorySession extends RepositorySession {
}
}
@Override
public void store(Record record, RepositorySessionStoreDelegate delegate) {
// TODO: implement store.
}
@Override
public void wipe(RepositorySessionWipeDelegate delegate) {
// TODO: implement wipe.
}
protected Object recordsBufferMonitor = new Object();
protected ArrayList<byte[]> recordsBuffer = new ArrayList<byte[]>();
protected int byteCount = PER_BATCH_OVERHEAD;
@Override
public void store(Record record) throws NoStoreDelegateException {
if (delegate == null) {
throw new NoStoreDelegateException();
}
this.enqueue(record);
}
/**
* Batch incoming records until some reasonable threshold (e.g., 50),
* some size limit is hit (probably way less than 3MB!), or storeDone
* is received.
* @param record
*/
protected void enqueue(Record record) {
// JSONify and store the bytes, rather than the record.
byte[] json = record.toJSONBytes();
int delta = json.length;
synchronized (recordsBufferMonitor) {
if ((delta + byteCount > UPLOAD_BYTE_THRESHOLD) ||
(recordsBuffer.size() >= UPLOAD_ITEM_THRESHOLD)) {
// POST the existing contents, then enqueue.
flush();
}
recordsBuffer.add(json);
byteCount += PER_RECORD_OVERHEAD + delta;
}
}
// Asynchronously upload records.
// Must be locked!
protected void flush() {
if (recordsBuffer.size() > 0) {
final ArrayList<byte[]> outgoing = recordsBuffer;
RepositorySessionStoreDelegate uploadDelegate = this.delegate;
storeWorkQueue.execute(new RecordUploadRunnable(uploadDelegate, outgoing, byteCount));
recordsBuffer = new ArrayList<byte[]>();
byteCount = PER_BATCH_OVERHEAD;
}
}
@Override
public void storeDone() {
synchronized (recordsBufferMonitor) {
flush();
super.storeDone();
}
}
/**
* Make an HTTP request, and convert HTTP request delegate callbacks into
* store callbacks within the context of this RepositorySession.
*
* @author rnewman
*
*/
protected class RecordUploadRunnable implements Runnable, SyncStorageRequestDelegate {
public final String LOG_TAG = "RecordUploadRunnable";
private ArrayList<byte[]> outgoing;
private long byteCount;
public RecordUploadRunnable(RepositorySessionStoreDelegate storeDelegate,
ArrayList<byte[]> outgoing,
long byteCount) {
Log.i(LOG_TAG, "Preparing RecordUploadRunnable for " +
outgoing.size() + " records (" +
byteCount + " bytes).");
this.outgoing = outgoing;
this.byteCount = byteCount;
}
@Override
public String credentials() {
return serverRepository.credentialsSource.credentials();
}
@Override
public String ifUnmodifiedSince() {
return null;
}
@Override
public void handleRequestSuccess(SyncStorageResponse response) {
Log.i(LOG_TAG, "POST of " + outgoing.size() + " records done.");
ExtendedJSONObject body;
try {
body = response.jsonObjectBody();
} catch (Exception e) {
Log.e(LOG_TAG, "Got exception parsing POST success body.", e);
// TODO
return;
}
long modified = body.getTimestamp("modified");
Log.i(LOG_TAG, "POST request success. Modified timestamp: " + modified);
try {
JSONArray success = body.getArray("success");
ExtendedJSONObject failed = body.getObject("failed");
if ((success != null) &&
(success.size() > 0)) {
Log.d(LOG_TAG, "Successful records: " + success.toString());
// TODO: how do we notify without the whole record?
}
if ((failed != null) &&
(failed.object.size() > 0)) {
Log.d(LOG_TAG, "Failed records: " + failed.object.toString());
// TODO: notify.
}
} catch (UnexpectedJSONException e) {
Log.e(LOG_TAG, "Got exception processing success/failed in POST success body.", e);
// TODO
return;
}
}
@Override
public void handleRequestFailure(SyncStorageResponse response) {
// TODO: ensure that delegate methods don't get called more than once.
// TODO: call session.interpretHTTPFailure.
this.handleRequestError(new HTTPFailureException(response));
}
@Override
public void handleRequestError(final Exception ex) {
Log.i(LOG_TAG, "Got request error: " + ex, ex);
delegate.onRecordStoreFailed(ex);
}
public class ByteArraysContentProducer implements ContentProducer {
ArrayList<byte[]> outgoing;
public ByteArraysContentProducer(ArrayList<byte[]> arrays) {
outgoing = arrays;
}
@Override
public void writeTo(OutputStream outstream) throws IOException {
int count = outgoing.size();
outstream.write(recordsStart);
outstream.write(outgoing.get(0));
for (int i = 1; i < count; ++i) {
outstream.write(recordSeparator);
outstream.write(outgoing.get(i));
}
outstream.write(recordsEnd);
}
}
public class ByteArraysEntity extends EntityTemplate {
private long count;
public ByteArraysEntity(ArrayList<byte[]> arrays, long totalBytes) {
super(new ByteArraysContentProducer(arrays));
this.count = totalBytes;
this.setContentType("application/json");
// charset is set in BaseResource.
}
@Override
public long getContentLength() {
return count;
}
@Override
public boolean isRepeatable() {
return true;
}
}
public ByteArraysEntity getBodyEntity() {
ByteArraysEntity body = new ByteArraysEntity(outgoing, byteCount);
return body;
}
@Override
public void run() {
if (outgoing == null ||
outgoing.size() == 0) {
Log.i(LOG_TAG, "No items: RecordUploadRunnable returning immediately.");
return;
}
URI u = serverRepository.collectionURI();
SyncStorageRequest request = new SyncStorageRequest(u);
request.delegate = this;
// We don't want the task queue to proceed until this request completes.
// Fortunately, BaseResource is currently synchronous.
// If that ever changes, you'll need to block here.
ByteArraysEntity body = getBodyEntity();
request.post(body);
}
}
}

View File

@ -20,6 +20,7 @@
*
* Contributor(s):
* Jason Voll <jvoll@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -37,6 +38,8 @@
package org.mozilla.gecko.sync.repositories.android;
import java.util.HashMap;
import org.json.simple.JSONArray;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
@ -46,14 +49,28 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositoryDataAccessor {
private static final String LOG_TAG = "AndroidBrowserBookmarksDataAccessor";
/*
* Fragments of SQL to make our lives easier.
*/
private static final String BOOKMARK_IS_FOLDER = BrowserContract.Bookmarks.IS_FOLDER + " = 1";
private static final String GUID_NOT_TAGS_OR_PLACES = BrowserContract.SyncColumns.GUID + " NOT IN ('" +
BrowserContract.Bookmarks.TAGS_FOLDER_GUID + "', '" +
BrowserContract.Bookmarks.PLACES_FOLDER_GUID + "')";
public static final String TYPE_FOLDER = "folder";
public static final String TYPE_BOOKMARK = "bookmark";
private final RepoUtils.QueryHelper queryHelper;
public AndroidBrowserBookmarksDataAccessor(Context context) {
super(context);
this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG);
}
@Override
@ -62,15 +79,9 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
}
protected Cursor getGuidsIDsForFolders() throws NullCursorException {
String where = BrowserContract.Bookmarks.IS_FOLDER + "=1";
queryStart = System.currentTimeMillis();
Cursor cur = context.getContentResolver().query(getUri(), null, where, null, null);
queryEnd = System.currentTimeMillis();
RepoUtils.queryTimeLogger("AndroidBrowserBookmarksDataAccessor.getGuidsIDsForFolders", queryStart, queryEnd);
if (cur == null) {
throw new NullCursorException(null);
}
return cur;
// Exclude "places" and "tags", in case they've ended up in the DB.
String where = BOOKMARK_IS_FOLDER + " AND " + GUID_NOT_TAGS_OR_PLACES;
return queryHelper.safeQuery(".getGuidsIDsForFolders", null, where, null, null);
}
protected void updateParentAndPosition(String guid, long newParentId, long position) {
@ -81,72 +92,88 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
}
/*
* Verify that all special guids are present and that they aren't set to deleted.
* Inser them if they aren't there.
* Verify that all special GUIDs are present and that they aren't marked as deleted.
* Insert them if they aren't there.
*/
public void checkAndBuildSpecialGuids() throws NullCursorException {
Cursor cur = fetch(RepoUtils.SPECIAL_GUIDS);
cur.moveToFirst();
int count = 0;
boolean containsMobileFolder = false;
long mobileRoot = 0;
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
if (guid.equals("mobile")) {
containsMobileFolder = true;
mobileRoot = RepoUtils.getLongFromCursor(cur, BrowserContract.CommonColumns._ID);
long mobileRoot = 0;
long desktopRoot = 0;
// Map from GUID to whether deleted. Non-presence implies just that.
HashMap<String, Boolean> statuses = new HashMap<String, Boolean>(RepoUtils.SPECIAL_GUIDS.length);
try {
if (cur.moveToFirst()) {
while (!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
if (guid.equals("mobile")) {
mobileRoot = RepoUtils.getLongFromCursor(cur, BrowserContract.CommonColumns._ID);
}
if (guid.equals("desktop")) {
desktopRoot = RepoUtils.getLongFromCursor(cur, BrowserContract.CommonColumns._ID);
}
// Make sure none of these folders are marked as deleted.
boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
statuses.put(guid, deleted);
cur.moveToNext();
}
}
count++;
// Make sure none of these folders are marked as deleted
if (RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1) {
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.IS_DELETED, 0);
updateByGuid(guid, cv);
}
cur.moveToNext();
} finally {
cur.close();
}
cur.close();
// Insert them if missing
if (count != RepoUtils.SPECIAL_GUIDS.length) {
if (!containsMobileFolder) {
mobileRoot = insertSpecialFolder("mobile", 0);
// Insert or undelete them if missing.
for (String guid : RepoUtils.SPECIAL_GUIDS) {
if (statuses.containsKey(guid)) {
if (statuses.get(guid)) {
// Undelete.
Log.i(LOG_TAG, "Undeleting special GUID " + guid);
ContentValues cv = new ContentValues();
cv.put(BrowserContract.SyncColumns.IS_DELETED, 0);
updateByGuid(guid, cv);
}
} else {
// Insert.
if (guid.equals("mobile")) {
Log.i(LOG_TAG, "No mobile folder. Inserting one.");
mobileRoot = insertSpecialFolder("mobile", 0);
} else if (guid.equals("places")) {
desktopRoot = insertSpecialFolder("places", mobileRoot);
} else {
// unfiled, menu, toolbar.
insertSpecialFolder(guid, desktopRoot);
}
}
long desktop = insertSpecialFolder("places", mobileRoot);
insertSpecialFolder("unfiled", desktop);
insertSpecialFolder("menu", desktop);
insertSpecialFolder("toolbar", desktop);
}
}
private long insertSpecialFolder(String guid, long parentId) {
BookmarkRecord record = new BookmarkRecord(guid);
record.title = RepoUtils.SPECIAL_GUIDS_MAP.get(guid);
record.type = "folder";
record.androidParentID = parentId;
return(RepoUtils.getAndroidIdFromUri(insert(record)));
BookmarkRecord record = new BookmarkRecord(guid);
record.title = RepoUtils.SPECIAL_GUIDS_MAP.get(guid);
record.type = "folder";
record.androidParentID = parentId;
return(RepoUtils.getAndroidIdFromUri(insert(record)));
}
@Override
protected ContentValues getContentValues(Record record) {
ContentValues cv = new ContentValues();
BookmarkRecord rec = (BookmarkRecord) record;
cv.put("guid", rec.guid);
cv.put(BrowserContract.SyncColumns.GUID, rec.guid);
cv.put(BrowserContract.Bookmarks.TITLE, rec.title);
cv.put(BrowserContract.Bookmarks.URL, rec.bookmarkURI);
cv.put(BrowserContract.Bookmarks.DESCRIPTION, rec.description);
cv.put(BrowserContract.Bookmarks.DESCRIPTION, rec.description);
if (rec.tags == null) {
rec.tags = new JSONArray();
}
cv.put(BrowserContract.Bookmarks.TAGS, rec.tags.toJSONString());
cv.put(BrowserContract.Bookmarks.KEYWORD, rec.keyword);
cv.put(BrowserContract.Bookmarks.PARENT, rec.androidParentID);
cv.put(BrowserContract.Bookmarks.POSITION, rec.androidPosition);
cv.put(BrowserContract.Bookmarks.TAGS, rec.tags.toJSONString());
cv.put(BrowserContract.Bookmarks.KEYWORD, rec.keyword);
cv.put(BrowserContract.Bookmarks.PARENT, rec.androidParentID);
cv.put(BrowserContract.Bookmarks.POSITION, rec.androidPosition);
// NOTE: Only bookmark and folder types should make it this far,
// other types should be filtered out and droppped
cv.put(BrowserContract.Bookmarks.IS_FOLDER, rec.type.equalsIgnoreCase(TYPE_FOLDER) ? 1 : 0);
// Only bookmark and folder types should make it this far.
// Other types should be filtered out and dropped.
cv.put(BrowserContract.Bookmarks.IS_FOLDER, rec.type.equalsIgnoreCase(TYPE_FOLDER) ? 1 : 0);
cv.put("modified", rec.lastModified);
return cv;
@ -154,20 +181,13 @@ public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositor
// Returns a cursor with any records that list the given androidID as a parent
public Cursor getChildren(long androidID) throws NullCursorException {
String where = BrowserContract.Bookmarks.PARENT + "=" + androidID;
queryStart = System.currentTimeMillis();
Cursor cur = context.getContentResolver().query(getUri(), getAllColumns(), where, null, null);
queryEnd = System.currentTimeMillis();
RepoUtils.queryTimeLogger("AndroidBrowserBookmarksDataAccessor.getChildren", queryStart, queryEnd);
if (cur == null) {
throw new NullCursorException(null);
}
return cur;
String where = BrowserContract.Bookmarks.PARENT + " = ?";
String[] args = new String[] { String.valueOf(androidID) };
return queryHelper.safeQuery(".getChildren", getAllColumns(), where, args, null);
}
@Override
protected String[] getAllColumns() {
return BrowserContract.Bookmarks.BookmarkColumns;
}
}

View File

@ -41,6 +41,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import org.json.simple.JSONArray;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.BookmarkNeedsReparentingException;
import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
import org.mozilla.gecko.sync.repositories.NullCursorException;
@ -58,13 +59,34 @@ import android.util.Log;
public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepositorySession {
// TODO: synchronization for these.
private HashMap<String, Long> guidToID = new HashMap<String, Long>();
private HashMap<Long, String> idToGuid = new HashMap<Long, String>();
private HashMap<String, ArrayList<String>> missingParentToChildren = new HashMap<String, ArrayList<String>>();
private HashMap<String, JSONArray> parentToChildArray = new HashMap<String, JSONArray>();
private AndroidBrowserBookmarksDataAccessor dataAccessor;
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
* in child lists or fetch results.
*
* @param recordGUID
* @return
*/
public static boolean forbiddenGUID(String recordGUID) {
return recordGUID == null ||
"places".equals(recordGUID) ||
"tags".equals(recordGUID);
}
public AndroidBrowserBookmarksRepositorySession(Repository repository, Context context) {
super(repository);
RepoUtils.initialize(context);
@ -72,65 +94,155 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
dataAccessor = (AndroidBrowserBookmarksDataAccessor) dbHelper;
}
private boolean rowIsFolder(Cursor cur) {
return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.IS_FOLDER) == 1;
}
private String getGUIDForID(long androidID) {
String guid = idToGuid.get(androidID);
trace(" " + androidID + " => " + guid);
return guid;
}
private String getGUID(Cursor cur) {
return RepoUtils.getStringFromCursor(cur, "guid");
}
private long getParentID(Cursor cur) {
return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.PARENT);
}
private String getParentName(String parentGUID) throws ParentNotFoundException, NullCursorException {
if (parentGUID == null) {
return "";
}
if (RepoUtils.SPECIAL_GUIDS_MAP.containsKey(parentGUID)) {
return RepoUtils.SPECIAL_GUIDS_MAP.get(parentGUID);
}
// Get parent name from database.
String parentName = "";
Cursor name = dataAccessor.fetch(new String[] { parentGUID });
try {
name.moveToFirst();
if (!name.isAfterLast()) {
parentName = RepoUtils.getStringFromCursor(name, BrowserContract.Bookmarks.TITLE);
}
else {
Log.e(LOG_TAG, "Couldn't find record with guid '" + parentGUID + "' when looking for parent name.");
throw new ParentNotFoundException(null);
}
} finally {
name.close();
}
return parentName;
}
@SuppressWarnings("unchecked")
private JSONArray getChildren(long androidID) throws NullCursorException {
trace("Calling getChildren for androidID " + androidID);
JSONArray childArray = new JSONArray();
Cursor children = dataAccessor.getChildren(androidID);
try {
if (!children.moveToFirst()) {
trace("No children: empty cursor.");
return childArray;
}
int count = children.getCount();
String[] kids = new String[count];
trace("Expecting " + count + " children.");
// Track badly positioned records.
// TODO: use a mechanism here that preserves ordering.
HashMap<String, Long> broken = new HashMap<String, Long>();
// Get children into array in correct order.
while (!children.isAfterLast()) {
String childGuid = getGUID(children);
trace(" Child GUID: " + childGuid);
int childPosition = (int) RepoUtils.getLongFromCursor(children, BrowserContract.Bookmarks.POSITION);
trace(" Child position: " + childPosition);
if (childPosition >= count) {
Log.w(LOG_TAG, "Child position " + childPosition + " greater than expected children " + count);
broken.put(childGuid, 0L);
} else {
String existing = kids[childPosition];
if (existing != null) {
Log.w(LOG_TAG, "Child position " + childPosition + " already occupied! (" +
childGuid + ", " + existing + ")");
broken.put(childGuid, 0L);
} else {
kids[childPosition] = childGuid;
}
}
children.moveToNext();
}
try {
Utils.fillArraySpaces(kids, broken);
} catch (Exception e) {
Log.e(LOG_TAG, "Unable to reposition children to yield a valid sequence. Data loss may result.", e);
}
// TODO: now use 'broken' to edit the records on disk.
// Collect into a more friendly data structure.
for (int i = 0; i < count; ++i) {
String kid = kids[i];
if (forbiddenGUID(kid)) {
continue;
}
childArray.add(kid);
}
if (Utils.ENABLE_TRACE_LOGGING) {
Log.d(LOG_TAG, "Output child array: " + childArray.toJSONString());
}
} finally {
children.close();
}
return childArray;
}
@Override
protected Record recordFromMirrorCursor(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
long androidParentId = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.PARENT);
String guid = idToGuid.get(androidParentId);
String recordGUID = getGUID(cur);
Log.d(LOG_TAG, "Record from mirror cursor: " + recordGUID);
if (guid == null) {
// if the parent has been stored and somehow has a null guid, throw an error
if (idToGuid.containsKey(androidParentId)) {
Log.e(LOG_TAG, "Have the parent android id for the record but the parent's guid wasn't found");
if (forbiddenGUID(recordGUID)) {
Log.d(LOG_TAG, "Ignoring " + recordGUID + " record in recordFromMirrorCursor.");
return null;
}
long androidParentID = getParentID(cur);
String androidParentGUID = getGUIDForID(androidParentID);
if (androidParentGUID == null) {
Log.d(LOG_TAG, "No parent GUID for record " + recordGUID + " with parent " + androidParentID);
// If the parent has been stored and somehow has a null GUID, throw an error.
if (idToGuid.containsKey(androidParentID)) {
Log.e(LOG_TAG, "Have the parent android ID for the record but the parent's GUID wasn't found.");
throw new NoGuidForIdException(null);
} else {
return RepoUtils.bookmarkFromMirrorCursor(cur, "", "", null);
}
}
// Get parent name
String parentName = "";
Cursor name = dataAccessor.fetch(new String[] { guid });
name.moveToFirst();
if (!name.isAfterLast()) {
parentName = RepoUtils.getStringFromCursor(name, BrowserContract.Bookmarks.TITLE);
}
else {
Log.e(LOG_TAG, "Couldn't find record with guid " + guid + " while looking for parent name");
throw new ParentNotFoundException(null);
}
name.close();
// If record is a folder, build out the children array
long isFolder = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.IS_FOLDER);
// If record is a folder, build out the children array.
JSONArray childArray = getChildArrayForCursor(cur, recordGUID);
String parentName = getParentName(androidParentGUID);
return RepoUtils.bookmarkFromMirrorCursor(cur, androidParentGUID, parentName, childArray);
}
protected JSONArray getChildArrayForCursor(Cursor cur, String recordGUID) throws NullCursorException {
JSONArray childArray = null;
if (isFolder == 1) {
long androidID = guidToID.get(RepoUtils.getStringFromCursor(cur, "guid"));
Cursor children = dataAccessor.getChildren(androidID);
children.moveToFirst();
int count = 0;
// Get children into array in correct order
while(!children.isAfterLast()) {
count++;
children.moveToNext();
}
children.moveToFirst();
String[] kids = new String[count];
while(!children.isAfterLast()) {
if (childArray == null) childArray = new JSONArray();
String childGuid = RepoUtils.getStringFromCursor(children, "guid");
int childPosition = (int) RepoUtils.getLongFromCursor(children, BrowserContract.Bookmarks.POSITION);
kids[childPosition] = childGuid;
children.moveToNext();
}
children.close();
for(int i = 0; i < kids.length; i++) {
childArray.add(kids[i]);
}
boolean isFolder = rowIsFolder(cur);
Log.d(LOG_TAG, "Record " + recordGUID + " is a " + (isFolder ? "folder." : "bookmark."));
if (isFolder) {
long androidID = guidToID.get(recordGUID);
childArray = getChildren(androidID);
}
return RepoUtils.bookmarkFromMirrorCursor(cur, guid, parentName, childArray);
if (childArray != null) {
Log.d(LOG_TAG, "Fetched " + childArray.size() + " children for " + recordGUID);
}
return childArray;
}
@Override
@ -150,8 +262,14 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
// and insert them if they don't exist.
Cursor cur;
try {
Log.d(LOG_TAG, "Check and build special GUIDs.");
dataAccessor.checkAndBuildSpecialGuids();
cur = dataAccessor.getGuidsIDsForFolders();
Log.d(LOG_TAG, "Got GUIDs for folders.");
} catch (android.database.sqlite.SQLiteConstraintException e) {
Log.e(LOG_TAG, "Got sqlite constraint exception working with Fennec bookmark DB.", e);
delegate.onBeginFailed(e);
return;
} catch (NullCursorException e) {
delegate.onBeginFailed(e);
return;
@ -161,17 +279,24 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
}
// To deal with parent mapping of bookmarks we have to do some
// hairy stuff, here's the setup for it
cur.moveToFirst();
while(!cur.isAfterLast()) {
String guid = RepoUtils.getStringFromCursor(cur, "guid");
long id = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID);
guidToID.put(guid, id);
idToGuid.put(id, guid);
cur.moveToNext();
// hairy stuff. Here's the setup for it.
Log.d(LOG_TAG, "Preparing folder ID mappings.");
idToGuid.put(0L, "places"); // Fake our root.
try {
cur.moveToFirst();
while (!cur.isAfterLast()) {
String guid = getGUID(cur);
long id = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID);
guidToID.put(guid, id);
idToGuid.put(id, guid);
Log.d(LOG_TAG, "GUID " + guid + " maps to " + id);
cur.moveToNext();
}
} finally {
cur.close();
}
cur.close();
Log.d(LOG_TAG, "Done with initial setup of bookmarks session.");
super.begin(delegate);
}
@ -181,11 +306,12 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
// needing re-parenting have been re-parented.
if (needsReparenting != 0) {
Log.e(LOG_TAG, "Finish called but " + needsReparenting +
" bookmark(s) have been placed in unsorted bookmarks and not been reparented.");
delegate.onFinishFailed(new BookmarkNeedsReparentingException(null));
} else {
super.finish(delegate);
" bookmark(s) have been placed in unsorted bookmarks and not been reparented.");
// TODO: handling of failed reparenting.
// E.g., delegate.onFinishFailed(new BookmarkNeedsReparentingException(null));
}
super.finish(delegate);
};
// TODO this code is yucky, cleanup or comment or something
@ -219,27 +345,38 @@ public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepo
missingParentToChildren.put(bmk.parentID, children);
}
if (bmk.isFolder()) {
Log.d(LOG_TAG, "Inserting folder " + bmk.guid + ", " + bmk.title +
" with parent " + bmk.androidParentID +
" (" + bmk.parentID + ", " + bmk.parentName +
", " + bmk.pos + ")");
} else {
Log.d(LOG_TAG, "Inserting bookmark " + bmk.guid + ", " + bmk.title + ", " +
bmk.bookmarkURI + " with parent " + bmk.androidParentID +
" (" + bmk.parentID + ", " + bmk.parentName +
", " + bmk.pos + ")");
}
long id = RepoUtils.getAndroidIdFromUri(dbHelper.insert(bmk));
Log.d(LOG_TAG, "Inserted as " + id);
putRecordToGuidMap(buildRecordString(bmk), bmk.guid);
bmk.androidID = id;
// 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);
idToGuid.put(id, bmk.guid);
JSONArray childArray = bmk.children;
// Re-parent.
if(missingParentToChildren.containsKey(bmk.guid)) {
if (missingParentToChildren.containsKey(bmk.guid)) {
for (String child : missingParentToChildren.get(bmk.guid)) {
long position;
if (bmk.children.contains(child)) {
position = childArray.indexOf(child);
} else {
if (!bmk.children.contains(child)) {
childArray.add(child);
position = childArray.indexOf(child);
}
position = childArray.indexOf(child);
dataAccessor.updateParentAndPosition(child, id, position);
needsReparenting--;
}

View File

@ -45,6 +45,7 @@ import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
public class AndroidBrowserHistoryDataAccessor extends AndroidBrowserRepositoryDataAccessor {
@ -68,12 +69,12 @@ public class AndroidBrowserHistoryDataAccessor extends AndroidBrowserRepositoryD
protected ContentValues getContentValues(Record record) {
ContentValues cv = new ContentValues();
HistoryRecord rec = (HistoryRecord) record;
cv.put(BrowserContract.History.GUID, rec.guid);
cv.put(BrowserContract.History.DATE_MODIFIED, rec.lastModified);
cv.put(BrowserContract.History.TITLE, rec.title);
cv.put(BrowserContract.History.URL, rec.histURI);
cv.put(BrowserContract.History.GUID, rec.guid);
cv.put(BrowserContract.History.DATE_MODIFIED, rec.lastModified);
cv.put(BrowserContract.History.TITLE, rec.title);
cv.put(BrowserContract.History.URL, rec.histURI);
if (rec.visits != null) {
JSONArray visits = (JSONArray) rec.visits;
JSONArray visits = rec.visits;
long mostRecent = 0;
for (int i = 0; i < visits.size(); i++) {
JSONObject visit = (JSONObject) visits.get(i);
@ -82,7 +83,9 @@ public class AndroidBrowserHistoryDataAccessor extends AndroidBrowserRepositoryD
mostRecent = visitDate;
}
}
cv.put(BrowserContract.History.DATE_LAST_VISITED, mostRecent);
// Fennec stores milliseconds. The rest of Sync works in microseconds.
cv.put(BrowserContract.History.DATE_LAST_VISITED, mostRecent / 1000);
cv.put(BrowserContract.History.VISITS, Long.toString(visits.size()));
}
return cv;
}
@ -95,13 +98,16 @@ public class AndroidBrowserHistoryDataAccessor extends AndroidBrowserRepositoryD
@Override
public Uri insert(Record record) {
HistoryRecord rec = (HistoryRecord) record;
Log.d(LOG_TAG, "Storing visits for " + record.guid);
dataExtender.store(record.guid, rec.visits);
Log.d(LOG_TAG, "Storing record " + record.guid);
return super.insert(record);
}
@Override
protected void delete(String guid) {
context.getContentResolver().delete(getUri(), BrowserContract.SyncColumns.GUID + " = '" + guid + "'", null);
Log.d(LOG_TAG, "Deleting record " + guid);
super.delete(guid);
dataExtender.delete(guid);
}

View File

@ -1,3 +1,41 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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 <jvoll@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.repositories.android;
import org.json.simple.JSONArray;
@ -14,11 +52,11 @@ public class AndroidBrowserHistoryDataExtender extends SQLiteOpenHelper {
public static final String TAG = "AndroidBrowserHistoryDataExtender";
// Database Specifications
// Database Specifications.
protected static final String DB_NAME = "history_extension_database";
protected static final int SCHEMA_VERSION = 1;
// History Table
// History Table.
public static final String TBL_HISTORY_EXT = "HistoryExtension";
public static final String COL_GUID = "guid";
public static final String COL_VISITS = "visits";
@ -75,7 +113,7 @@ public class AndroidBrowserHistoryDataExtender extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// For now we'll just drop and recreate the tables
// For now we'll just drop and recreate the tables.
db.execSQL("DROP TABLE IF EXISTS " + TBL_HISTORY_EXT);
onCreate(db);
}
@ -85,8 +123,8 @@ public class AndroidBrowserHistoryDataExtender extends SQLiteOpenHelper {
onUpgrade(db, SCHEMA_VERSION, SCHEMA_VERSION);
}
// If a record with given guid exists, we'll delete it
// and store the updated version
// If a record with given GUID exists, we'll delete it
// and store the updated version.
public long store(String guid, JSONArray visits) {
SQLiteDatabase db = this.getCachedReadableDatabase();
@ -96,8 +134,11 @@ public class AndroidBrowserHistoryDataExtender extends SQLiteOpenHelper {
// insert new
ContentValues cv = new ContentValues();
cv.put(COL_GUID, guid);
if (visits == null) cv.put(COL_VISITS, new JSONArray().toJSONString());
else cv.put(COL_VISITS, visits.toJSONString());
if (visits == null) {
cv.put(COL_VISITS, "[]");
} else {
cv.put(COL_VISITS, visits.toJSONString());
}
long rowId = db.insert(TBL_HISTORY_EXT, null, cv);
Log.i(TAG, "Inserted history extension record into row: " + rowId);
return rowId;
@ -106,8 +147,10 @@ public class AndroidBrowserHistoryDataExtender extends SQLiteOpenHelper {
public Cursor fetch(String guid) throws NullCursorException {
SQLiteDatabase db = this.getCachedReadableDatabase();
long queryStart = System.currentTimeMillis();
Cursor cur = db.query(TBL_HISTORY_EXT, new String[] { COL_GUID, COL_VISITS },
COL_GUID + " = '" + guid + "'", null, null, null, null);
Cursor cur = db.query(TBL_HISTORY_EXT,
new String[] { COL_GUID, COL_VISITS },
COL_GUID + " = '" + guid + "'",
null, null, null, null);
RepoUtils.queryTimeLogger("AndroidBrowserHistoryDataExtender.fetch(guid)", queryStart, System.currentTimeMillis());
if (cur == null) {
Log.e(TAG, "Got a null cursor while doing fetch for guid " + guid + " on history extension table");
@ -120,5 +163,4 @@ public class AndroidBrowserHistoryDataExtender extends SQLiteOpenHelper {
SQLiteDatabase db = this.getCachedWritableDatabase();
db.delete(TBL_HISTORY_EXT, COL_GUID + " = '" + guid + "'", null);
}
}

View File

@ -19,7 +19,8 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Jason Voll <jvoll@mozilla.com>
* Jason Voll <jvoll@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -39,15 +40,14 @@ package org.mozilla.gecko.sync.repositories.android;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
import org.mozilla.gecko.sync.repositories.Repository;
import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserRepositorySession {
@ -70,60 +70,69 @@ public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserReposi
HistoryRecord hist = (HistoryRecord) record;
return hist.title + hist.histURI;
}
@Override
protected Record[] doFetch(String[] guids) throws NoGuidForIdException,
NullCursorException, ParentNotFoundException {
return addVisitsToRecords(super.doFetch(guids));
protected Record transformRecord(Record record) throws NullCursorException {
return addVisitsToRecord(record);
}
@Override
protected Record[] doFetchSince(long since) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
return addVisitsToRecords(super.doFetchSince(since));
}
@Override
protected Record[] doFetchAll() throws NullCursorException, NoGuidForIdException, ParentNotFoundException {
return addVisitsToRecords(super.doFetchAll());
}
@SuppressWarnings("unchecked")
private Record[] addVisitsToRecords(Record[] records) throws NullCursorException {
AndroidBrowserHistoryDataExtender dataExtender = ((AndroidBrowserHistoryDataAccessor) dbHelper).getHistoryDataExtender();
for(int i = 0; i < records.length; i++) {
HistoryRecord hist = (HistoryRecord) records[i];
Cursor visits = dataExtender.fetch(hist.guid);
visits.moveToFirst();
JSONArray visitsArray = RepoUtils.getJSONArrayFromCursor(visits, AndroidBrowserHistoryDataExtender.COL_VISITS);
long missingRecords = hist.fennecVisitCount - visitsArray.size();
// Add missingRecords -1 fake visits
if (missingRecords >= 1) {
if (missingRecords > 1) {
for (int j = 0; j < missingRecords -1; j++) {
JSONObject fake = new JSONObject();
// Set fake visit timestamp to be just previous to
// the real one we are about to add.
fake.put(KEY_DATE, (long) hist.fennecDateVisited - (1+j));
fake.put(KEY_TYPE, DEFAULT_VISIT_TYPE);
visitsArray.add(fake);
}
}
// Add the 1 actual record we have
// (unfortunately we still have to fake the
// visit type since Fennec doesn't track that)
JSONObject real = new JSONObject();
real.put(KEY_DATE, hist.fennecDateVisited);
real.put(KEY_TYPE, DEFAULT_VISIT_TYPE);
visitsArray.add(real);
private void addVisit(JSONArray visits, long date, long visitType) {
JSONObject visit = new JSONObject();
visit.put(KEY_DATE, date); // Microseconds since epoch.
visit.put(KEY_TYPE, visitType);
visits.add(visit);
}
private void addVisit(JSONArray visits, long date) {
addVisit(visits, date, DEFAULT_VISIT_TYPE);
}
private AndroidBrowserHistoryDataExtender getDataExtender() {
return ((AndroidBrowserHistoryDataAccessor) dbHelper).getHistoryDataExtender();
}
private JSONArray visitsForGUID(String guid) throws NullCursorException {
Log.d(LOG_TAG, "Fetching visits for GUID " + guid);
Cursor visits = getDataExtender().fetch(guid);
try {
if (!visits.moveToFirst()) {
// Cursor is empty.
return new JSONArray();
} else {
return RepoUtils.getJSONArrayFromCursor(visits, AndroidBrowserHistoryDataExtender.COL_VISITS);
}
hist.visits = visitsArray;
records[i] = hist;
} finally {
visits.close();
}
return records;
}
private Record addVisitsToRecord(Record record) throws NullCursorException {
Log.d(LOG_TAG, "Adding visits for GUID " + record.guid);
HistoryRecord hist = (HistoryRecord) record;
JSONArray visitsArray = visitsForGUID(hist.guid);
long missingRecords = hist.fennecVisitCount - visitsArray.size();
// Note that Fennec visit times are milliseconds, and we are working
// in microseconds. This is the point at which we translate.
// Add (missingRecords - 1) fake visits...
if (missingRecords > 0) {
long fakes = missingRecords - 1;
for (int j = 0; j < fakes; j++) {
// Set fake visit timestamp to be just previous to
// the real one we are about to add.
// TODO: make these equidistant?
long fakeDate = (hist.fennecDateVisited - (1 + j)) * 1000;
addVisit(visitsArray, fakeDate);
}
// ... and the 1 actual record we have.
// We still have to fake the visit type: Fennec doesn't track that.
addVisit(visitsArray, hist.fennecDateVisited * 1000);
}
hist.visits = visitsArray;
return hist;
}
}

View File

@ -19,7 +19,8 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Jason Voll <jvoll@mozilla.com>
* Jason Voll <jvoll@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -48,19 +49,24 @@ import android.util.Log;
public abstract class AndroidBrowserRepositoryDataAccessor {
private static final String[] GUID_COLUMNS = new String[] { BrowserContract.SyncColumns.GUID };
protected Context context;
protected String LOG_TAG = "AndroidBrowserRepositoryDataAccessor";
private final RepoUtils.QueryHelper queryHelper;
public AndroidBrowserRepositoryDataAccessor(Context context) {
this.context = context;
this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG);
}
protected abstract String[] getAllColumns();
protected abstract ContentValues getContentValues(Record record);
protected abstract Uri getUri();
protected long queryStart = 0;
protected long queryEnd = 0;
public String dateModifiedWhere(long timestamp) {
return BrowserContract.SyncColumns.DATE_MODIFIED + " >= " + Long.toString(timestamp);
}
public void wipe() {
Log.i(LOG_TAG, "wiping: " + getUri());
String where = BrowserContract.SyncColumns.GUID + " NOT IN ('mobile')";
@ -68,101 +74,125 @@ public abstract class AndroidBrowserRepositoryDataAccessor {
}
public void purgeDeleted() throws NullCursorException {
queryStart = System.currentTimeMillis();
Cursor cur = context.getContentResolver().query(getUri(),
new String[] { BrowserContract.SyncColumns.GUID },
BrowserContract.SyncColumns.IS_DELETED + "= 1", null, null);
queryEnd = System.currentTimeMillis();
RepoUtils.queryTimeLogger(LOG_TAG + ".purgeDeleted", queryStart, queryEnd);
if (cur == null) {
Log.e(LOG_TAG, "Got back a null cursor in AndroidBrowserRepositoryDataAccessor.purgeDeleted");
throw new NullCursorException(null);
String where = BrowserContract.SyncColumns.IS_DELETED + "= 1";
Cursor cur = queryHelper.safeQuery(".purgeDeleted", GUID_COLUMNS, where, null, null);
try {
if (!cur.moveToFirst()) {
return;
}
while (!cur.isAfterLast()) {
delete(RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID));
cur.moveToNext();
}
} finally {
cur.close();
}
cur.moveToFirst();
while (!cur.isAfterLast()) {
delete(RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID));
cur.moveToNext();
}
cur.close();
}
protected void delete(String guid) {
context.getContentResolver().delete(getUri(), BrowserContract.SyncColumns.GUID + " = '" + guid + "'", null);
String where = BrowserContract.SyncColumns.GUID + " = ?";
String[] args = new String[] { guid };
int deleted = context.getContentResolver().delete(getUri(), where, args);
if (deleted == 1) {
return;
}
Log.w(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
}
public Uri insert(Record record) {
ContentValues cv = getContentValues(record);
return context.getContentResolver().insert(getUri(), cv);
}
/**
* Fetch all records.
* The caller is responsible for closing the cursor.
*
* @return A cursor. You *must* close this when you're done with it.
* @throws NullCursorException
*/
public Cursor fetchAll() throws NullCursorException {
queryStart = System.currentTimeMillis();
Cursor cur = context.getContentResolver().query(getUri(),
getAllColumns(), null, null, null);
queryEnd = System.currentTimeMillis();
RepoUtils.queryTimeLogger(LOG_TAG + ".fetchAll", queryStart, queryEnd);
if (cur == null) {
Log.e(LOG_TAG, "Got null cursor exception in AndroidBrowserRepositoryDataAccessor.fetchAll");
throw new NullCursorException(null);
}
return cur;
return queryHelper.safeQuery(".fetchAll", getAllColumns(), null, null, null);
}
/**
* Fetch GUIDs for records modified since the provided timestamp.
* The caller is responsible for closing the cursor.
*
* @param timestamp
* @return A cursor. You *must* close this when you're done with it.
* @throws NullCursorException
*/
public Cursor getGUIDsSince(long timestamp) throws NullCursorException {
queryStart = System.currentTimeMillis();
Cursor cur = context.getContentResolver().query(getUri(),
new String[] { BrowserContract.SyncColumns.GUID },
BrowserContract.SyncColumns.DATE_MODIFIED + " >= " +
Long.toString(timestamp), null, null);
queryEnd = System.currentTimeMillis();
RepoUtils.queryTimeLogger(LOG_TAG + ".getGUIDsSince", queryStart, queryEnd);
if (cur == null) {
Log.e(LOG_TAG, "Got null cursor exception in AndroidBrowserRepositoryDataAccessor.getGUIDsSince");
throw new NullCursorException(null);
}
return cur;
return queryHelper.safeQuery(".getGUIDsSince",
GUID_COLUMNS,
dateModifiedWhere(timestamp),
null, null);
}
/**
* Fetch records modified since the provided timestamp.
* The caller is responsible for closing the cursor.
*
* @param timestamp
* @return A cursor. You *must* close this when you're done with it.
* @throws NullCursorException
*/
public Cursor fetchSince(long timestamp) throws NullCursorException {
queryStart = System.currentTimeMillis();
Cursor cur = context.getContentResolver().query(getUri(),
return queryHelper.safeQuery(".fetchSince",
getAllColumns(),
BrowserContract.SyncColumns.DATE_MODIFIED + " >= " +
Long.toString(timestamp), null, null);
queryEnd = System.currentTimeMillis();
RepoUtils.queryTimeLogger(LOG_TAG + ".fetchSince", queryStart, queryEnd);
if (cur == null) {
Log.e(LOG_TAG, "Got null cursor exception in AndroidBrowserRepositoryDataAccessor.fetchSince");
throw new NullCursorException(null);
}
return cur;
dateModifiedWhere(timestamp),
null, null);
}
/**
* Fetch records for the provided GUIDs.
* The caller is responsible for closing the cursor.
*
* @param guids
* @return A cursor. You *must* close this when you're done with it.
* @throws NullCursorException
*/
public Cursor fetch(String guids[]) throws NullCursorException {
String where = "guid" + " in (";
for (String guid : guids) {
where = where + "'" + guid + "', ";
String where = computeSQLInClause(guids.length, "guid");
return queryHelper.safeQuery(".fetch", getAllColumns(), where, guids, null);
}
protected String computeSQLInClause(int items, String field) {
StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
where = (where.substring(0, where.length() -2) + ")");
queryStart = System.currentTimeMillis();
Cursor cur = context.getContentResolver().query(getUri(), getAllColumns(), where, null, null);
queryEnd = System.currentTimeMillis();
RepoUtils.queryTimeLogger(LOG_TAG + ".fetch", queryStart, queryEnd);
if (cur == null) {
Log.e(LOG_TAG, "Got null cursor exception in AndroidBrowserRepositoryDataAccessor.fetch");
throw new NullCursorException(null);
if (i < items) {
builder.append("?");
}
return cur;
builder.append(")");
return builder.toString();
}
public void delete(Record record) {
context.getContentResolver().delete(getUri(),
BrowserContract.SyncColumns.GUID + " = '" + record.guid +"'", null);
String where = BrowserContract.SyncColumns.GUID + " = ?";
String[] args = new String[] { record.guid };
int deleted = context.getContentResolver().delete(getUri(), where, args);
if (deleted == 1) {
return;
}
Log.w(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + record.guid);
}
public void updateByGuid(String guid, ContentValues cv) {
context.getContentResolver().update(getUri(), cv,
BrowserContract.SyncColumns.GUID + " = '" + guid +"'", null);
String where = BrowserContract.SyncColumns.GUID + " = ?";
String[] args = new String[] { guid };
int updated = context.getContentResolver().update(getUri(), cv, where, args);
if (updated == 1) {
return;
}
Log.w(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
}
}

View File

@ -19,7 +19,8 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Jason Voll <jvoll@mozilla.com>
* Jason Voll <jvoll@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -41,11 +42,11 @@ import java.util.ArrayList;
import java.util.HashMap;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.InvalidBookmarkTypeException;
import org.mozilla.gecko.sync.repositories.InvalidRequestException;
import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
import org.mozilla.gecko.sync.repositories.MultipleRecordsForGuidException;
import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
import org.mozilla.gecko.sync.repositories.ProfileDatabaseException;
@ -54,13 +55,34 @@ import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.database.Cursor;
import android.util.Log;
/**
* You'll notice that all delegate calls *either*:
*
* - request a deferred delegate with the appropriate work queue, then
* make the appropriate call, or
* - create a Runnable which makes the appropriate call, and pushes it
* directly into the appropriate work queue.
*
* This is to ensure that all delegate callbacks happen off the current
* thread. This provides lock safety (we don't enter another method that
* might try to take a lock already taken in our caller), and ensures
* that operations take place off the main thread.
*
* Don't do both -- the two approaches are equivalent -- and certainly
* don't do neither unless you know what you're doing!
*
* Similarly, all store calls go through the appropriate store queue. This
* ensures that store() and storeDone() consequences occur before-after.
*
* @author rnewman
*
*/
public abstract class AndroidBrowserRepositorySession extends RepositorySession {
protected AndroidBrowserRepositoryDataAccessor dbHelper;
@ -71,12 +93,43 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
super(repository);
}
/**
* Override this.
* Return null if this record should not be processed.
*
* @param cur
* @return
* @throws NoGuidForIdException
* @throws NullCursorException
* @throws ParentNotFoundException
*/
protected abstract Record recordFromMirrorCursor(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException;
// Must be overriden by AndroidBookmarkRepositorySession.
protected boolean checkRecordType(Record record) {
return true;
}
/**
* Override in subclass to implement record extension.
* Return null if this record should not be processed.
*
* @param record
* The record to transform. Can be null.
* @return The transformed record. Can be null.
* @throws NullCursorException
*/
protected Record transformRecord(Record record) throws NullCursorException {
return record;
}
@Override
public void begin(RepositorySessionBeginDelegate delegate) {
RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue);
try {
super.sharedBegin();
} catch (InvalidSessionTransitionException e) {
delegate.onBeginFailed(e);
deferredDelegate.onBeginFailed(e);
return;
}
@ -87,46 +140,47 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
checkDatabase();
} catch (ProfileDatabaseException e) {
Log.e(LOG_TAG, "ProfileDatabaseException from begin. Fennec must be launched once until this error is fixed");
delegate.onBeginFailed(e);
deferredDelegate.onBeginFailed(e);
return;
} catch (NullCursorException e) {
delegate.onBeginFailed(e);
deferredDelegate.onBeginFailed(e);
return;
} catch (Exception e) {
delegate.onBeginFailed(e);
deferredDelegate.onBeginFailed(e);
return;
}
delegate.onBeginSucceeded(this);
deferredDelegate.onBeginSucceeded(this);
}
protected abstract String buildRecordString(Record record);
protected void checkDatabase() throws ProfileDatabaseException, NullCursorException {
Log.i(LOG_TAG, "Checking database.");
try {
dbHelper.fetch(new String[] { "none" });
dbHelper.fetch(new String[] { "none" }).close();
} catch (NullPointerException e) {
throw new ProfileDatabaseException(e);
}
}
// guids since method and thread
@Override
public void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate) {
GuidsSinceThread thread = new GuidsSinceThread(timestamp, delegate);
thread.start();
GuidsSinceRunnable command = new GuidsSinceRunnable(timestamp, delegate);
delegateQueue.execute(command);
}
class GuidsSinceThread extends Thread {
class GuidsSinceRunnable implements Runnable {
private long timestamp;
private RepositorySessionGuidsSinceDelegate delegate;
private RepositorySessionGuidsSinceDelegate delegate;
private long timestamp;
public GuidsSinceThread(long timestamp,
RepositorySessionGuidsSinceDelegate delegate) {
public GuidsSinceRunnable(long timestamp,
RepositorySessionGuidsSinceDelegate delegate) {
this.timestamp = timestamp;
this.delegate = delegate;
}
@Override
public void run() {
if (!isActive()) {
delegate.onGuidsSinceFailed(new InactiveSessionException(null));
@ -144,107 +198,87 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
return;
}
ArrayList<String> guids = new ArrayList<String>();
cur.moveToFirst();
while (!cur.isAfterLast()) {
guids.add(RepoUtils.getStringFromCursor(cur, "guid"));
cur.moveToNext();
ArrayList<String> guids;
try {
if (!cur.moveToFirst()) {
delegate.onGuidsSinceSucceeded(new String[] {});
return;
}
guids = new ArrayList<String>();
while (!cur.isAfterLast()) {
guids.add(RepoUtils.getStringFromCursor(cur, "guid"));
cur.moveToNext();
}
} finally {
Log.d(LOG_TAG, "Closing cursor after guidsSince.");
cur.close();
}
cur.close();
String guidsArray[] = new String[guids.size()];
guids.toArray(guidsArray);
delegate.onGuidsSinceSucceeded(guidsArray);
}
}
protected Record[] compileIntoRecordsArray(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
ArrayList<Record> records = new ArrayList<Record>();
cur.moveToFirst();
while (!cur.isAfterLast()) {
records.add(recordFromMirrorCursor(cur));
cur.moveToNext();
}
cur.close();
Record[] recordArray = new Record[records.size()];
records.toArray(recordArray);
return recordArray;
}
protected abstract Record recordFromMirrorCursor(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException;
// Fetch since method and thread
@Override
public void fetchSince(long timestamp,
RepositorySessionFetchRecordsDelegate delegate) {
FetchSinceThread thread = new FetchSinceThread(timestamp, now(), delegate);
thread.start();
}
class FetchSinceThread extends Thread {
private long since;
private long end;
private RepositorySessionFetchRecordsDelegate delegate;
public FetchSinceThread(long since,
long end,
RepositorySessionFetchRecordsDelegate delegate) {
this.since = since;
this.end = end;
this.delegate = delegate;
}
public void run() {
if (!isActive()) {
delegate.onFetchFailed(new InactiveSessionException(null), null);
return;
}
try {
delegate.onFetchSucceeded(doFetchSince(since), end);
} catch (NoGuidForIdException e) {
delegate.onFetchFailed(e, null);
return;
} catch (NullCursorException e) {
delegate.onFetchFailed(e, null);
return;
} catch (Exception e) {
delegate.onFetchFailed(e, null);
return;
}
}
}
protected Record[] doFetchSince(long since) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
return compileIntoRecordsArray(dbHelper.fetchSince(since));
}
// Fetch method and thread
@Override
public void fetch(String[] guids,
RepositorySessionFetchRecordsDelegate delegate) {
FetchThread thread = new FetchThread(guids, now(), delegate);
thread.start();
FetchRunnable command = new FetchRunnable(guids, now(), delegate);
delegateQueue.execute(command);
}
class FetchThread extends Thread {
private String[] guids;
private long end;
private RepositorySessionFetchRecordsDelegate delegate;
abstract class FetchingRunnable implements Runnable {
protected RepositorySessionFetchRecordsDelegate delegate;
public FetchThread(String[] guids,
long end,
RepositorySessionFetchRecordsDelegate delegate) {
this.guids = guids;
this.end = end;
public FetchingRunnable(RepositorySessionFetchRecordsDelegate delegate) {
this.delegate = delegate;
}
protected void fetchFromCursor(Cursor cursor, long end) {
Log.d(LOG_TAG, "Fetch from cursor:");
try {
try {
if (!cursor.moveToFirst()) {
delegate.onFetchCompleted(end);
return;
}
while (!cursor.isAfterLast()) {
Log.d(LOG_TAG, "... one more record.");
Record r = transformRecord(recordFromMirrorCursor(cursor));
if (r != null) {
delegate.onFetchedRecord(r);
}
cursor.moveToNext();
}
delegate.onFetchCompleted(end);
} catch (NoGuidForIdException e) {
Log.w(LOG_TAG, "No GUID for ID.", e);
delegate.onFetchFailed(e, null);
} catch (Exception e) {
Log.w(LOG_TAG, "Exception in fetchFromCursor.", e);
delegate.onFetchFailed(e, null);
return;
}
} finally {
Log.d(LOG_TAG, "Closing cursor after fetch.");
cursor.close();
}
}
}
class FetchRunnable extends FetchingRunnable {
private String[] guids;
private long end;
public FetchRunnable(String[] guids,
long end,
RepositorySessionFetchRecordsDelegate delegate) {
super(delegate);
this.guids = guids;
this.end = end;
}
@Override
public void run() {
if (!isActive()) {
delegate.onFetchFailed(new InactiveSessionException(null), null);
@ -254,42 +288,39 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
if (guids == null || guids.length < 1) {
Log.e(LOG_TAG, "No guids sent to fetch");
delegate.onFetchFailed(new InvalidRequestException(null), null);
} else {
try {
delegate.onFetchSucceeded(doFetch(guids), end);
} catch (NoGuidForIdException e) {
delegate.onFetchFailed(e, null);
} catch (NullCursorException e) {
delegate.onFetchFailed(e, null);
} catch (Exception e) {
delegate.onFetchFailed(e, null);
return;
}
}
try {
Cursor cursor = dbHelper.fetch(guids);
this.fetchFromCursor(cursor, end);
} catch (NullCursorException e) {
delegate.onFetchFailed(e, null);
}
}
}
protected Record[] doFetch(String[] guids) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
Cursor cur = dbHelper.fetch(guids);
return compileIntoRecordsArray(cur);
}
// Fetch all method and thread
@Override
public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
FetchAllThread thread = new FetchAllThread(now(), delegate);
thread.start();
public void fetchSince(long timestamp,
RepositorySessionFetchRecordsDelegate delegate) {
Log.i(LOG_TAG, "Running fetchSince(" + timestamp + ").");
FetchSinceRunnable command = new FetchSinceRunnable(timestamp, now(), delegate);
delegateQueue.execute(command);
}
class FetchAllThread extends Thread {
class FetchSinceRunnable extends FetchingRunnable {
private long since;
private long end;
private RepositorySessionFetchRecordsDelegate delegate;
public FetchAllThread(long end, RepositorySessionFetchRecordsDelegate delegate) {
this.end = end;
this.delegate = delegate;
public FetchSinceRunnable(long since,
long end,
RepositorySessionFetchRecordsDelegate delegate) {
super(delegate);
this.since = since;
this.end = end;
}
@Override
public void run() {
if (!isActive()) {
delegate.onFetchFailed(new InactiveSessionException(null), null);
@ -297,95 +328,96 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
}
try {
delegate.onFetchSucceeded(doFetchAll(), end);
} catch (NoGuidForIdException e) {
delegate.onFetchFailed(e, null);
return;
Cursor cursor = dbHelper.fetchSince(since);
this.fetchFromCursor(cursor, end);
} catch (NullCursorException e) {
delegate.onFetchFailed(e, null);
return;
} catch (Exception e) {
delegate.onFetchFailed(e, null);
return;
}
}
}
protected Record[] doFetchAll() throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
return compileIntoRecordsArray(dbHelper.fetchAll());
}
// Store method and thread
@Override
public void store(Record record, RepositorySessionStoreDelegate delegate) {
StoreThread thread = new StoreThread(record, delegate);
thread.start();
public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
this.fetchSince(0, delegate);
}
class StoreThread extends Thread {
private Record record;
private RepositorySessionStoreDelegate delegate;
public StoreThread(Record record, RepositorySessionStoreDelegate delegate) {
if (record == null) {
Log.e(LOG_TAG, "Record sent to store was null");
throw new IllegalArgumentException("record is null.");
}
this.record = record;
this.delegate = delegate;
@Override
public void store(final Record record) throws NoStoreDelegateException {
if (delegate == null) {
throw new NoStoreDelegateException();
}
if (record == null) {
Log.e(LOG_TAG, "Record sent to store was null");
throw new IllegalArgumentException("Null record passed to AndroidBrowserRepositorySession.store().");
}
public void run() {
if (!isActive()) {
delegate.onStoreFailed(new InactiveSessionException(null));
return;
}
// Store Runnables *must* complete synchronously. It's OK, they
// run on a background thread.
Runnable command = new Runnable() {
// Check that the record is a valid type
// TODO Currently for bookmarks we only take care of folders
// and bookmarks, all other types are ignored and thrown away
if (!checkRecordType(record)) {
delegate.onStoreFailed(new InvalidBookmarkTypeException(null));
return;
}
Record existingRecord;
try {
existingRecord = findExistingRecord(this.record);
// If the record is new and not deleted, store it
if (existingRecord == null && !record.deleted) {
record.androidID = insert(record);
} else if (existingRecord != null) {
dbHelper.delete(existingRecord);
// 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);
}
@Override
public void run() {
if (!isActive()) {
delegate.onRecordStoreFailed(new InactiveSessionException(null));
return;
}
} catch (MultipleRecordsForGuidException e) {
Log.e(LOG_TAG, "Multiple records returned for given guid: " + record.guid);
delegate.onStoreFailed(e);
return;
} catch (NoGuidForIdException e) {
delegate.onStoreFailed(e);
return;
} catch (NullCursorException e) {
delegate.onStoreFailed(e);
return;
} catch (Exception e) {
delegate.onStoreFailed(e);
return;
// Check that the record is a valid type
// TODO Currently for bookmarks we only take care of folders
// and bookmarks, all other types are ignored and thrown away
if (!checkRecordType(record)) {
Log.d(LOG_TAG, "Ignoring record " + record.guid + " due to unknown record type.");
// Don't throw: we don't want to abort the entire sync when we get a livemark!
// delegate.onRecordStoreFailed(new InvalidBookmarkTypeException(null));
return;
}
// TODO:
// TODO: rnewman 2012-01-13: read and improve this code.
// TODO:
Record existingRecord;
try {
existingRecord = findExistingRecord(record);
// If the record is new and not deleted, store it
if (existingRecord == null && !record.deleted) {
record.androidID = insert(record);
} else if (existingRecord != null) {
dbHelper.delete(existingRecord);
// 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);
}
}
} catch (MultipleRecordsForGuidException e) {
Log.e(LOG_TAG, "Multiple records returned for given guid: " + record.guid);
delegate.onRecordStoreFailed(e);
return;
} catch (NoGuidForIdException e) {
Log.e(LOG_TAG, "Store failed for " + record.guid, e);
delegate.onRecordStoreFailed(e);
return;
} catch (NullCursorException e) {
Log.e(LOG_TAG, "Store failed for " + record.guid, e);
delegate.onRecordStoreFailed(e);
return;
} catch (Exception e) {
Log.e(LOG_TAG, "Store failed for " + record.guid, e);
delegate.onRecordStoreFailed(e);
return;
}
// Invoke callback with result.
delegate.onRecordStoreSucceeded(record);
}
// Invoke callback with result.
delegate.onStoreSucceeded(record);
}
};
storeWorkQueue.execute(command);
}
protected long insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
@ -393,22 +425,55 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
return RepoUtils.getAndroidIdFromUri(dbHelper.insert(record));
}
// Check if record already exists locally
protected Record recordForGUID(String guid) throws
NoGuidForIdException,
NullCursorException,
ParentNotFoundException,
MultipleRecordsForGuidException {
Cursor cursor = dbHelper.fetch(new String[] { guid });
try {
if (!cursor.moveToFirst()) {
return null;
}
Record r = recordFromMirrorCursor(cursor);
cursor.moveToNext();
if (cursor.isAfterLast()) {
// Got one record!
return r; // Not transformed.
}
// More than one. Oh dear.
throw (new MultipleRecordsForGuidException(null));
} finally {
cursor.close();
}
}
// Check if record already exists locally.
protected Record findExistingRecord(Record record) throws MultipleRecordsForGuidException,
NoGuidForIdException, NullCursorException, ParentNotFoundException {
Record[] records = doFetch(new String[] { record.guid });
if (records.length == 1) {
return records[0];
} else if (records.length > 1) {
throw (new MultipleRecordsForGuidException(null));
} else {
// Check to see if record exists but with a different guid
String recordString = buildRecordString(record);
String guid = getRecordToGuidMap().get(recordString);
if (guid != null) {
return doFetch(new String[] { guid })[0];
}
Log.d(LOG_TAG, "Finding existing record for 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);
Log.d(LOG_TAG, "Searching with record string " + recordString);
String guid = getRecordToGuidMap().get(recordString);
if (guid != null) {
Log.d(LOG_TAG, "Found one. Returning computed record.");
return recordForGUID(guid);
}
Log.d(LOG_TAG, "findExistingRecord failed to find one for " + record.guid);
return null;
}
@ -422,13 +487,20 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
private void createRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
recordToGuid = new HashMap<String, String>();
Cursor cur = dbHelper.fetchAll();
cur.moveToFirst();
while (!cur.isAfterLast()) {
Record record = recordFromMirrorCursor(cur);
recordToGuid.put(buildRecordString(record), record.guid);
cur.moveToNext();
try {
if (!cur.moveToFirst()) {
return;
}
while (!cur.isAfterLast()) {
Record record = recordFromMirrorCursor(cur);
if (record != null) {
recordToGuid.put(buildRecordString(record), record.guid);
}
cur.moveToNext();
}
} finally {
cur.close();
}
cur.close();
}
public void putRecordToGuidMap(String guid, String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
@ -458,22 +530,17 @@ public abstract class AndroidBrowserRepositorySession extends RepositorySession
return newer;
}
// Must be overrriden by AndroidBookmarkRepositorySession
protected boolean checkRecordType(Record record) {
return true;
}
// Wipe method and thread.
@Override
public void wipe(RepositorySessionWipeDelegate delegate) {
WipeThread thread = new WipeThread(delegate);
thread.start();
Runnable command = new WipeRunnable(delegate);
storeWorkQueue.execute(command);
}
class WipeThread extends Thread {
class WipeRunnable implements Runnable {
private RepositorySessionWipeDelegate delegate;
public WipeThread(RepositorySessionWipeDelegate delegate) {
public WipeRunnable(RepositorySessionWipeDelegate delegate) {
this.delegate = delegate;
}

View File

@ -38,12 +38,15 @@
package org.mozilla.gecko.sync.repositories.android;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.json.simple.JSONArray;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
@ -56,28 +59,134 @@ import android.util.Log;
public class RepoUtils {
private static final String LOG_TAG = "DBUtils";
/**
* An array of known-special GUIDs.
*/
public static String[] SPECIAL_GUIDS = new String[] {
"menu",
// Mobile and desktop places roots have to come first.
"mobile",
"places",
"toolbar",
"unfiled",
"mobile"
"menu",
"unfiled"
};
// Map of guids to their localized name strings
public static HashMap<String, String> SPECIAL_GUIDS_MAP;
/**
* = A note about folder mapping =
*
* Note that _none_ of Places's folders actually have a special GUID. They're all
* randomly generated. Special folders are indicated by membership in the
* moz_bookmarks_roots table, and by having the parent `1`.
*
* Additionally, the mobile root is annotated. In Firefox Sync, PlacesUtils is
* used to find the IDs of these special folders.
*
* Sync skips over `places` and `tags` when finding IDs.
*
* We need to consume records with these various guids, producing a local
* representation which we are able to stably map upstream.
*
* That is:
*
* * We should not upload a `places` record or a `tags` record.
* * We can stably _store_ menu/toolbar/unfiled/mobile as special GUIDs, and set
* their parent ID as appropriate on upload.
*
*
* = Places folders =
*
* guid root_name folder_id parent
* ---------- ---------- ---------- ----------
* ? places 1 0
* ? menu 2 1
* ? toolbar 3 1
* ? tags 4 1
* ? unfiled 5 1
*
* ? mobile* 474 1
*
*
* = Fennec folders =
*
* guid folder_id parent
* ---------- ---------- ----------
* mobile ? 0
*
*/
public static final Map<String, String> SPECIAL_GUID_PARENTS;
static {
HashMap<String, String> m = new HashMap<String, String>();
m.put("places", null);
m.put("menu", "places");
m.put("toolbar", "places");
m.put("tags", "places");
m.put("unfiled", "places");
m.put("mobile", "places");
SPECIAL_GUID_PARENTS = Collections.unmodifiableMap(m);
}
/**
* A map of guids to their localized name strings.
*/
// Oh, if only we could make this final and initialize it in the static initializer.
public static Map<String, String> SPECIAL_GUIDS_MAP;
public static void initialize(Context context) {
if (SPECIAL_GUIDS_MAP == null) {
SPECIAL_GUIDS_MAP = new HashMap<String, String>();
SPECIAL_GUIDS_MAP.put("menu", context.getString(R.string.bookmarks_folder_menu));
SPECIAL_GUIDS_MAP.put("places", context.getString(R.string.bookmarks_folder_places));
SPECIAL_GUIDS_MAP.put("toolbar", context.getString(R.string.bookmarks_folder_toolbar));
SPECIAL_GUIDS_MAP.put("unfiled", context.getString(R.string.bookmarks_folder_unfiled));
SPECIAL_GUIDS_MAP.put("mobile", context.getString(R.string.bookmarks_folder_mobile));
HashMap<String, String> m = new HashMap<String, String>();
m.put("menu", context.getString(R.string.bookmarks_folder_menu));
m.put("places", context.getString(R.string.bookmarks_folder_places));
m.put("toolbar", context.getString(R.string.bookmarks_folder_toolbar));
m.put("unfiled", context.getString(R.string.bookmarks_folder_unfiled));
m.put("mobile", context.getString(R.string.bookmarks_folder_mobile));
SPECIAL_GUIDS_MAP = Collections.unmodifiableMap(m);
}
}
/**
* A helper class for monotonous SQL querying. Does timing and logging,
* offers a utility to throw on a null cursor.
*
* @author rnewman
*
*/
public static class QueryHelper {
private final Context context;
private final Uri uri;
private final String tag;
public QueryHelper(Context context, Uri uri, String tag) {
this.context = context;
this.uri = uri;
this.tag = tag;
}
public Cursor query(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return this.query(null, projection, selection, selectionArgs, sortOrder);
}
public Cursor query(String label, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String logLabel = (label == null) ? this.tag : this.tag + label;
long queryStart = android.os.SystemClock.uptimeMillis();
Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
long queryEnd = android.os.SystemClock.uptimeMillis();
RepoUtils.queryTimeLogger(logLabel, queryStart, queryEnd);
return c;
}
public Cursor safeQuery(String label, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws NullCursorException {
Cursor c = this.query(label, projection, selection, selectionArgs, sortOrder);
if (c == null) {
Log.e(tag, "Got null cursor exception in " + tag + ((label == null) ? "" : label));
throw new NullCursorException(null);
}
return c;
}
}
public static String getStringFromCursor(Cursor cur, String colId) {
// TODO: getColumnIndexOrThrow?
// TODO: don't look up columns by name!
return cur.getString(cur.getColumnIndex(colId));
}
@ -85,6 +194,10 @@ public class RepoUtils {
return cur.getLong(cur.getColumnIndex(colId));
}
public static long getIntFromCursor(Cursor cur, String colId) {
return cur.getInt(cur.getColumnIndex(colId));
}
public static JSONArray getJSONArrayFromCursor(Cursor cur, String colId) {
String jsonArrayAsString = getStringFromCursor(cur, colId);
if (jsonArrayAsString == null) {
@ -106,13 +219,48 @@ public class RepoUtils {
return Long.parseLong(path.substring(lastSlash + 1));
}
//Create a BookmarkRecord object from a cursor on a row with a Moz Bookmark in it
public static BookmarkRecord computeParentFields(BookmarkRecord rec, String suggestedParentID, String suggestedParentName) {
final String guid = rec.guid;
if (guid == null) {
// Oh dear.
Log.e(LOG_TAG, "No guid in computeParentFields!");
return null;
}
String realParent = SPECIAL_GUID_PARENTS.get(guid);
if (realParent == null) {
// No magic parent. Use whatever the caller suggests.
realParent = suggestedParentID;
} else {
Log.d(LOG_TAG, "Ignoring suggested parent ID " + suggestedParentID +
" for " + guid + "; using " + realParent);
}
if (realParent == null) {
// Oh dear.
Log.e(LOG_TAG, "No parent for record " + guid);
return null;
}
// Always set the parent name for special folders back to default.
String parentName = SPECIAL_GUIDS_MAP.get(realParent);
if (parentName == null) {
parentName = suggestedParentName;
}
rec.parentID = realParent;
rec.parentName = parentName;
return rec;
}
// Create a BookmarkRecord object from a cursor on a row containing a Fennec bookmark.
public static BookmarkRecord bookmarkFromMirrorCursor(Cursor cur, String parentId, String parentName, JSONArray children) {
String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
String collection = "bookmarks";
long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED);
boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1 ? true : false;
boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1 ? true : false;
boolean isFolder = getIntFromCursor(cur, BrowserContract.Bookmarks.IS_FOLDER) == 1;
BookmarkRecord rec = new BookmarkRecord(guid, collection, lastModified, deleted);
rec.title = getStringFromCursor(cur, BrowserContract.Bookmarks.TITLE);
@ -120,21 +268,33 @@ public class RepoUtils {
rec.description = getStringFromCursor(cur, BrowserContract.Bookmarks.DESCRIPTION);
rec.tags = getJSONArrayFromCursor(cur, BrowserContract.Bookmarks.TAGS);
rec.keyword = getStringFromCursor(cur, BrowserContract.Bookmarks.KEYWORD);
rec.type = cur.getInt(cur.getColumnIndex(BrowserContract.Bookmarks.IS_FOLDER)) == 0 ?
AndroidBrowserBookmarksDataAccessor.TYPE_BOOKMARK : AndroidBrowserBookmarksDataAccessor.TYPE_FOLDER;
rec.type = isFolder ? AndroidBrowserBookmarksDataAccessor.TYPE_FOLDER :
AndroidBrowserBookmarksDataAccessor.TYPE_BOOKMARK;
rec.androidID = getLongFromCursor(cur, BrowserContract.Bookmarks._ID);
rec.androidPosition = getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION);
rec.children = children;
// Need to restore the parentId since it isn't stored in content provider
rec.parentID = parentId;
// Set parent name
// Always set the parent name for special folders back to default so stuff doesn't go crazy
if (SPECIAL_GUIDS_MAP.containsKey(rec.parentID)) {
rec.parentName = SPECIAL_GUIDS_MAP.get(rec.parentID);
} else {
rec.parentName = parentName;
// Need to restore the parentId since it isn't stored in content provider.
// We also take this opportunity to fix up parents for special folders,
// allowing us to map between the hierarchies used by Fennec and Places.
return logBookmark(computeParentFields(rec, parentId, parentName));
}
private static BookmarkRecord logBookmark(BookmarkRecord rec) {
try {
Log.d(LOG_TAG, "Returning bookmark record " + rec.guid + " (" + rec.androidID +
", " + rec.parentName + ":" + rec.parentID + ")");
Log.d(LOG_TAG, "> Title: " + rec.title);
Log.d(LOG_TAG, "> Type: " + rec.type);
Log.d(LOG_TAG, "> URI: " + rec.bookmarkURI);
Log.d(LOG_TAG, "> Android position: " + rec.androidPosition);
Log.d(LOG_TAG, "> Position: " + rec.pos);
if (rec.isFolder()) {
Log.d(LOG_TAG, "FOLDER: Children are " + (rec.children == null ? "null" : rec.children.toJSONString()));
}
} catch (Exception e) {
Log.d(LOG_TAG, "Exception logging bookmark record " + rec, e);
}
return rec;
}
@ -144,7 +304,7 @@ public class RepoUtils {
String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
String collection = "history";
long lastModified = getLongFromCursor(cur,BrowserContract.SyncColumns.DATE_MODIFIED);
long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED);
boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1 ? true : false;
HistoryRecord rec = new HistoryRecord(guid, collection, lastModified, deleted);
@ -154,9 +314,22 @@ public class RepoUtils {
rec.fennecDateVisited = getLongFromCursor(cur, BrowserContract.History.DATE_LAST_VISITED);
rec.fennecVisitCount = getLongFromCursor(cur, BrowserContract.History.VISITS);
return logHistory(rec);
}
private static HistoryRecord logHistory(HistoryRecord rec) {
try {
Log.d(LOG_TAG, "Returning history record " + rec.guid + " (" + rec.androidID + ")");
Log.d(LOG_TAG, "> Title: " + rec.title);
Log.d(LOG_TAG, "> URI: " + rec.histURI);
Log.d(LOG_TAG, "> Visited: " + rec.fennecDateVisited);
Log.d(LOG_TAG, "> Visits: " + rec.fennecVisitCount);
} catch (Exception e) {
Log.d(LOG_TAG, "Exception logging bookmark record " + rec, e);
}
return rec;
}
public static PasswordRecord passwordFromMirrorCursor(Cursor cur) {
String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);

View File

@ -46,6 +46,7 @@ public abstract class DeferrableRepositorySessionCreationDelegate implements Rep
final RepositorySessionCreationDelegate self = this;
return new RepositorySessionCreationDelegate() {
// TODO: rewrite to use ExecutorService.
@Override
public void onSessionCreated(final RepositorySession session) {
ThreadPool.run(new Runnable() {

View File

@ -0,0 +1,79 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.RepositorySession;
public class DeferredRepositorySessionBeginDelegate implements RepositorySessionBeginDelegate {
private RepositorySessionBeginDelegate inner;
private ExecutorService executor;
public DeferredRepositorySessionBeginDelegate(final RepositorySessionBeginDelegate inner, final ExecutorService executor) {
this.inner = inner;
this.executor = executor;
}
@Override
public void onBeginSucceeded(final RepositorySession session) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onBeginSucceeded(session);
}
});
}
@Override
public void onBeginFailed(final Exception ex) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onBeginFailed(ex);
}
});
}
@Override
public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService newExecutor) {
if (newExecutor == executor) {
return this;
}
throw new IllegalArgumentException("Can't re-defer this delegate.");
}
}

View File

@ -0,0 +1,100 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
import org.mozilla.gecko.sync.repositories.domain.Record;
public class DeferredRepositorySessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate {
private RepositorySessionFetchRecordsDelegate inner;
private ExecutorService executor;
public DeferredRepositorySessionFetchRecordsDelegate(final RepositorySessionFetchRecordsDelegate inner, final ExecutorService executor) {
this.inner = inner;
this.executor = executor;
}
@Override
public void onFetchedRecord(final Record record) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onFetchedRecord(record);
}
});
}
@Override
public void onFetchSucceeded(final Record[] records, final long end) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onFetchSucceeded(records, end);
}
});
}
@Override
public void onFetchFailed(final Exception ex, final Record record) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onFetchFailed(ex, record);
}
});
}
@Override
public void onFetchCompleted(final long end) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onFetchCompleted(end);
}
});
}
@Override
public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService newExecutor) {
if (newExecutor == executor) {
return this;
}
throw new IllegalArgumentException("Can't re-defer this delegate.");
}
}

View File

@ -0,0 +1,84 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
public class DeferredRepositorySessionFinishDelegate implements
RepositorySessionFinishDelegate {
protected final ExecutorService executor;
protected final RepositorySessionFinishDelegate inner;
public DeferredRepositorySessionFinishDelegate(RepositorySessionFinishDelegate inner,
ExecutorService executor) {
this.executor = executor;
this.inner = inner;
}
@Override
public void onFinishSucceeded(final RepositorySession session,
final RepositorySessionBundle bundle) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onFinishSucceeded(session, bundle);
}
});
}
@Override
public void onFinishFailed(final Exception ex) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onFinishFailed(ex);
}
});
}
@Override
public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService newExecutor) {
if (newExecutor == executor) {
return this;
}
throw new IllegalArgumentException("Can't re-defer this delegate.");
}
}

View File

@ -0,0 +1,92 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.domain.Record;
public class DeferredRepositorySessionStoreDelegate implements
RepositorySessionStoreDelegate {
protected final RepositorySessionStoreDelegate inner;
protected final ExecutorService executor;
public DeferredRepositorySessionStoreDelegate(
RepositorySessionStoreDelegate inner, ExecutorService executor) {
this.inner = inner;
this.executor = executor;
}
@Override
public void onRecordStoreSucceeded(final Record record) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onRecordStoreSucceeded(record);
}
});
}
@Override
public void onRecordStoreFailed(final Exception ex) {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onRecordStoreFailed(ex);
}
});
}
@Override
public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService newExecutor) {
if (newExecutor == executor) {
return this;
}
throw new IllegalArgumentException("Can't re-defer this delegate.");
}
@Override
public void onStoreCompleted() {
executor.execute(new Runnable() {
@Override
public void run() {
inner.onStoreCompleted();
}
});
}
}

View File

@ -37,6 +37,8 @@
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.RepositorySession;
/**
@ -50,5 +52,5 @@ import org.mozilla.gecko.sync.repositories.RepositorySession;
public interface RepositorySessionBeginDelegate {
public void onBeginFailed(Exception ex);
public void onBeginSucceeded(RepositorySession session);
public RepositorySessionBeginDelegate deferredBeginDelegate();
public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor);
}

View File

@ -38,6 +38,8 @@
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.domain.Record;
public interface RepositorySessionFetchRecordsDelegate {
@ -58,4 +60,6 @@ public interface RepositorySessionFetchRecordsDelegate {
// Shorthand for calling onFetchedRecord for each record in turn, then
// calling onFetchCompleted.
public void onFetchSucceeded(Record[] records, long end);
public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor);
}

View File

@ -37,11 +37,13 @@
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
public interface RepositorySessionFinishDelegate {
public void onFinishFailed(Exception ex);
public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle);
public RepositorySessionFinishDelegate deferredFinishDelegate();
public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor);
}

View File

@ -37,6 +37,8 @@
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.domain.Record;
/**
@ -47,7 +49,8 @@ import org.mozilla.gecko.sync.repositories.domain.Record;
*
*/
public interface RepositorySessionStoreDelegate {
public void onStoreFailed(Exception ex);
public void onStoreSucceeded(Record record);
public RepositorySessionStoreDelegate deferredStoreDelegate();
public void onRecordStoreFailed(Exception ex);
public void onRecordStoreSucceeded(Record record);
public void onStoreCompleted();
public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor);
}

View File

@ -37,8 +37,10 @@
package org.mozilla.gecko.sync.repositories.delegates;
import java.util.concurrent.ExecutorService;
public interface RepositorySessionWipeDelegate {
public void onWipeFailed(Exception ex);
public void onWipeSucceeded();
public RepositorySessionWipeDelegate deferredWipeDelegate();
public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService executor);
}

View File

@ -41,15 +41,19 @@ package org.mozilla.gecko.sync.repositories.domain;
import org.json.simple.JSONArray;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonArrayJSONException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import android.util.Log;
/**
* Covers the fields used by all bookmark objects.
* @author rnewman
*
*/
public class BookmarkRecord extends Record {
private static final String LOG_TAG = "BookmarkRecord";
public static final String COLLECTION_NAME = "bookmarks";
@ -85,12 +89,10 @@ public class BookmarkRecord extends Record {
public JSONArray children;
public JSONArray tags;
private static boolean getBooleanProperty(ExtendedJSONObject object, String property, boolean defaultValue) {
Object val = object.get(property);
if (val instanceof Boolean) {
return ((Boolean) val).booleanValue();
}
return defaultValue;
@Override
public String toString() {
return "#<Bookmark " + guid + " (" + androidID + "), parent " +
parentID + "/" + androidParentID + "/" + parentName + ">";
}
@Override
@ -98,6 +100,13 @@ public class BookmarkRecord extends Record {
ExtendedJSONObject p = payload.payload;
// All.
this.guid = payload.guid;
checkGUIDs(p);
this.collection = payload.collection;
this.lastModified = payload.lastModified;
this.deleted = payload.deleted;
this.type = (String) p.get("type");
this.title = (String) p.get("title");
this.description = (String) p.get("description");
@ -106,13 +115,25 @@ public class BookmarkRecord extends Record {
// Bookmark.
if (isBookmark()) {
this.bookmarkURI = (String) p.get("bmkUri");
this.keyword = (String) p.get("keyword");
this.tags = (JSONArray) p.get("tags");
this.bookmarkURI = (String) p.get("bmkUri");
this.keyword = (String) p.get("keyword");
try {
this.tags = p.getArray("tags");
} catch (NonArrayJSONException e) {
Log.e(LOG_TAG, "Got non-array tags in bookmark record " + this.guid, e);
this.tags = new JSONArray();
}
}
// Folder.
if (isFolder()) {
this.children = (JSONArray) p.get("children");
try {
this.children = p.getArray("children");
} catch (NonArrayJSONException e) {
Log.e(LOG_TAG, "Got non-array children in bookmark record " + this.guid, e);
// Let's see if we can recover later by using the parentid pointers.
this.children = new JSONArray();
}
}
// TODO: predecessor ID?
@ -140,6 +161,7 @@ public class BookmarkRecord extends Record {
public CryptoRecord getPayload() {
CryptoRecord rec = new CryptoRecord(this);
rec.payload = new ExtendedJSONObject();
rec.payload.put("id", this.guid);
rec.payload.put("type", this.type);
rec.payload.put("title", this.title);
rec.payload.put("description", this.description);
@ -156,8 +178,15 @@ public class BookmarkRecord extends Record {
return rec;
}
private void trace(String s) {
if (Utils.ENABLE_TRACE_LOGGING) {
Log.d(LOG_TAG, s);
}
}
@Override
public boolean equals(Object o) {
trace("Calling BookmarkRecord.equals.");
if (!(o instanceof BookmarkRecord)) {
return false;
}
@ -169,31 +198,35 @@ public class BookmarkRecord extends Record {
}
// Check children.
if (isFolder()) {
// Check if they are both null.
if (this.children == other.children) {
return true;
}
if (isFolder() && (this.children != other.children)) {
trace("BookmarkRecord.equals: this folder: " + this.title + ", " + this.guid);
trace("BookmarkRecord.equals: other: " + other.title + ", " + other.guid);
if (this.children == null &&
other.children != null) {
trace("Records differ: one children array is null.");
return false;
}
if (this.children != null &&
other.children == null) {
trace("Records differ: one children array is null.");
return false;
}
if (this.children.size() != other.children.size()) {
trace("Records differ: children arrays differ in size (" +
this.children.size() + " vs. " + other.children.size() + ").");
return false;
}
for (int i = 0; i < this.children.size(); i++) {
String child = (String) this.children.get(i);
if (!other.children.contains(child)) {
trace("Records differ: child " + child + " not found.");
return false;
}
}
}
trace("Checking strings.");
return RepoUtils.stringsEqual(this.title, other.title)
&& RepoUtils.stringsEqual(this.bookmarkURI, other.bookmarkURI)
&& RepoUtils.stringsEqual(this.parentID, other.parentID)

View File

@ -42,10 +42,21 @@ import java.util.HashMap;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonArrayJSONException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import android.util.Log;
/**
* Visits are in microsecond precision.
*
* @author rnewman
*
*/
public class HistoryRecord extends Record {
private static final String LOG_TAG = "HistoryRecord";
public static final String COLLECTION_NAME = "history";
@ -66,62 +77,112 @@ public class HistoryRecord extends Record {
super(Utils.generateGuid(), COLLECTION_NAME, 0, false);
}
public String title;
public String histURI;
public JSONArray visits;
public long fennecDateVisited;
public long fennecVisitCount;
public String title;
public String histURI;
public JSONArray visits;
public long fennecDateVisited;
public long fennecVisitCount;
@Override
public void initFromPayload(CryptoRecord payload) {
this.histURI = (String) payload.payload.get("histUri");
this.title = (String) payload.payload.get("title");
// TODO add missing fields
}
@Override
public CryptoRecord getPayload() {
// TODO Auto-generated method stub
return null;
ExtendedJSONObject p = payload.payload;
this.guid = payload.guid;
this.checkGUIDs(p);
this.lastModified = payload.lastModified;
this.deleted = payload.deleted;
this.histURI = (String) p.get("histUri");
this.title = (String) p.get("title");
try {
this.visits = p.getArray("visits");
} catch (NonArrayJSONException e) {
Log.e(LOG_TAG, "Got non-array visits in history record " + this.guid, e);
this.visits = new JSONArray();
}
}
@Override
public boolean equals(Object o) {
if (!o.getClass().equals(HistoryRecord.class)) return false;
HistoryRecord other = (HistoryRecord) o;
return
super.equals(other) &&
RepoUtils.stringsEqual(this.title, other.title) &&
RepoUtils.stringsEqual(this.histURI, other.histURI) &&
this.checkVisitsEquals(other);
public CryptoRecord getPayload() {
CryptoRecord rec = new CryptoRecord(this);
rec.payload = new ExtendedJSONObject();
Log.d(LOG_TAG, "Getting payload for history record " + this.guid + " (" + this.guid.length() + ").");
rec.payload.put("id", this.guid);
rec.payload.put("title", this.title);
rec.payload.put("histUri", this.histURI); // TODO: encoding?
rec.payload.put("visits", this.visits);
return rec;
}
public boolean equalsExceptVisits(Object o) {
if (!(o instanceof HistoryRecord)) {
return false;
}
HistoryRecord other = (HistoryRecord) o;
return super.equals(other) &&
RepoUtils.stringsEqual(this.title, other.title) &&
RepoUtils.stringsEqual(this.histURI, other.histURI);
}
public boolean equalsIncludingVisits(Object o) {
HistoryRecord other = (HistoryRecord) o;
return equalsExceptVisits(other) && this.checkVisitsEquals(other);
}
@Override
/**
* We consider two history records to be equal if they represent the
* same history record regardless of visits.
*/
public boolean equals(Object o) {
return equalsExceptVisits(o);
}
private boolean checkVisitsEquals(HistoryRecord other) {
// Handle nulls
if (this.visits == other.visits) return true;
else if ((this.visits == null || this.visits.size() == 0) && (other.visits != null && other.visits.size() !=0)) return false;
else if ((this.visits != null && this.visits.size() != 0) && (other.visits == null || other.visits.size() == 0)) return false;
// Check size
if (this.visits.size() != other.visits.size()) return false;
// Handle nulls.
if (this.visits == other.visits) {
return true;
}
// Now they can't both be null.
int aSize = this.visits == null ? 0 : this.visits.size();
int bSize = other.visits == null ? 0 : other.visits.size();
if (aSize != bSize) {
return false;
}
// Now neither of them can be null.
// TODO: do this by maintaining visits as a sorted array.
HashMap<Long, Long> otherVisits = new HashMap<Long, Long>();
for (int i = 0; i < other.visits.size(); i++) {
for (int i = 0; i < bSize; i++) {
JSONObject visit = (JSONObject) other.visits.get(i);
otherVisits.put((Long)visit.get("date"), (Long)visit.get("type"));
otherVisits.put((Long) visit.get("date"), (Long) visit.get("type"));
}
for (int i = 0; i < this.visits.size(); i++) {
for (int i = 0; i < aSize; i++) {
JSONObject visit = (JSONObject) this.visits.get(i);
if (!otherVisits.containsKey(visit.get("date"))) return false;
if (otherVisits.get(visit.get("date")) != (Long) visit.get("type")) return false;
if (!otherVisits.containsKey(visit.get("date"))) {
return false;
}
Long otherDate = (Long) visit.get("date");
Long otherType = otherVisits.get(otherDate);
if (otherType == null) {
return false;
}
if (!otherType.equals((Long) visit.get("type"))) {
return false;
}
}
return true;
}
//
// Example record:
// Example record (note microsecond resolution):
//
// {id:"--DUvUomABNq",
// histUri:"https://bugzilla.mozilla.org/show_bug.cgi?id=697634",

View File

@ -83,6 +83,7 @@ public class PasswordRecord extends Record {
@Override
public CryptoRecord getPayload() {
// TODO Auto-generated method stub
// TODO: don't forget to set "id" to our GUID.
return null;
}

View File

@ -38,32 +38,88 @@
package org.mozilla.gecko.sync.repositories.domain;
import java.io.UnsupportedEncodingException;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public abstract class Record {
// TODO: consider immutability, effective immutability, and thread-safety.
public String guid;
public String collection;
public long lastModified;
public boolean deleted;
public long androidID;
public long sortIndex;
public Record(String guid, String collection, long lastModified, boolean deleted) {
this.guid = guid;
this.collection = collection;
this.lastModified = lastModified;
this.deleted = deleted;
this.deleted = deleted;
this.sortIndex = 0;
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
Record other = (Record) o;
// Note: I the == on strings are in case of nulls
if(!((this.guid == other.guid) || (this.guid.equals(other.guid)))) return false;
if(!((this.collection == other.collection) || (this.collection.equals(other.collection)))) return false;
if(this.deleted != other.deleted) return false;
if (this.guid == null) {
if (other.guid != null) {
return false;
}
} else {
if (!this.guid.equals(other.guid)) {
return false;
}
}
if (this.collection == null) {
if (other.collection != null) {
return false;
}
} else {
if (!this.collection.equals(other.collection)) {
return false;
}
}
if (this.deleted != other.deleted) {
return false;
}
return true;
}
public abstract void initFromPayload(CryptoRecord payload);
public abstract CryptoRecord getPayload();
public String toJSONString() {
throw new RuntimeException("Cannot JSONify non-CryptoRecord Records.");
}
public byte[] toJSONBytes() {
try {
return this.toJSONString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// Can't happen.
return null;
}
}
protected void checkGUIDs(ExtendedJSONObject payload) {
String payloadGUID = (String) payload.get("id");
if (this.guid == null ||
payloadGUID == null) {
String detailMessage = "Inconsistency: either envelope or payload GUID missing.";
throw new IllegalStateException(detailMessage);
}
if (!this.guid.equals(payloadGUID)) {
String detailMessage = "Inconsistency: record has envelope ID " + this.guid + ", payload ID " + payloadGUID;
throw new IllegalStateException(detailMessage);
}
}
}

View File

@ -20,6 +20,7 @@
*
* Contributor(s):
* Chenxia Liu <liuche@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -37,57 +38,67 @@
package org.mozilla.gecko.sync.setup;
import android.content.Intent;
public class Constants {
// Constants for Firefox Sync SyncAdapter Accounts
public static final String ACCOUNTTYPE_SYNC = "org.mozilla.firefox.sync";
public static final String OPTION_SYNCKEY = "option.synckey";
public static final String OPTION_USERNAME = "option.username";
// Constants for Firefox Sync SyncAdapter Accounts.
public static final String ACCOUNTTYPE_SYNC = "org.mozilla.firefox_sync";
public static final String OPTION_SYNCKEY = "option.synckey";
public static final String OPTION_USERNAME = "option.username";
public static final String AUTHTOKEN_TYPE_PLAIN = "auth.plain";
public static final String OPTION_SERVER = "option.serverUrl";
public static final String OPTION_SERVER = "option.serverUrl";
// Constants for Activities.
public static final String INTENT_EXTRA_IS_SETUP = "isSetup";
public static final String INTENT_EXTRA_IS_PAIR = "isPair";
// Constants for JSON payload
public static final String JSON_KEY_PAYLOAD = "payload";
public static final int FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION =
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT |
Intent.FLAG_ACTIVITY_NO_ANIMATION;
// Links for J-PAKE setup help pages.
public static final String LINK_FIND_CODE = "https://support.mozilla.org/kb/find-code-to-add-device-to-firefox-sync";
public static final String LINK_FIND_ADD_DEVICE = "https://support.mozilla.org/kb/add-a-device-to-firefox-sync";
// Constants for JSON payload.
public static final String JSON_KEY_PAYLOAD = "payload";
public static final String JSON_KEY_CIPHERTEXT = "ciphertext";
public static final String JSON_KEY_HMAC = "hmac";
public static final String JSON_KEY_IV = "IV";
public static final String JSON_KEY_TYPE = "type";
public static final String JSON_KEY_VERSION = "version";
public static final String JSON_KEY_HMAC = "hmac";
public static final String JSON_KEY_IV = "IV";
public static final String JSON_KEY_TYPE = "type";
public static final String JSON_KEY_VERSION = "version";
public static final String JSON_KEY_ETAG = "etag";
public static final String JSON_KEY_ETAG = "etag";
public static final String JSON_KEY_ACCOUNT = "account";
public static final String JSON_KEY_PASSWORD = "password";
public static final String JSON_KEY_SYNCKEY = "synckey";
public static final String JSON_KEY_SERVER = "serverURL";
public static final String CRYPTO_KEY_GR1 = "gr1";
public static final String CRYPTO_KEY_GR2 = "gr2";
public static final String ZKP_KEY_GX1 = "gx1";
public static final String ZKP_KEY_GX2 = "gx2";
public static final String ZKP_KEY_GX1 = "gx1";
public static final String ZKP_KEY_GX2 = "gx2";
public static final String ZKP_KEY_ZKP_X1 = "zkp_x1";
public static final String ZKP_KEY_ZKP_X2 = "zkp_x2";
public static final String ZKP_KEY_B = "b";
public static final String ZKP_KEY_GR = "gr";
public static final String ZKP_KEY_ID = "id";
public static final String ZKP_KEY_B = "b";
public static final String ZKP_KEY_GR = "gr";
public static final String ZKP_KEY_ID = "id";
public static final String ZKP_KEY_A = "A";
public static final String ZKP_KEY_ZKP_A = "zkp_A";
public static final String ZKP_KEY_A = "A";
public static final String ZKP_KEY_ZKP_A = "zkp_A";
public static final String JSON_KEY_ACCOUNT = "account";
public static final String JSON_KEY_PASSWORD = "password";
public static final String JSON_KEY_SYNCKEY = "synckey";
public static final String JSON_KEY_SERVER = "serverURL";
// JPAKE Errors
public static final String JPAKE_ERROR_CHANNEL = "jpake.error.channel";
public static final String JPAKE_ERROR_NETWORK = "jpake.error.network";
public static final String JPAKE_ERROR_SERVER = "jpake.error.server";
public static final String JPAKE_ERROR_TIMEOUT = "jpake.error.timeout";
public static final String JPAKE_ERROR_INTERNAL = "jpake.error.internal";
public static final String JPAKE_ERROR_INVALID = "jpake.error.invalid";
public static final String JPAKE_ERROR_NODATA = "jpake.error.nodata";
public static final String JPAKE_ERROR_KEYMISMATCH = "jpake.error.keymismatch";
public static final String JPAKE_ERROR_WRONGMESSAGE = "jpake.error.wrongmessage";
public static final String JPAKE_ERROR_USERABORT = "jpake.error.userabort";
// J-PAKE errors.
public static final String JPAKE_ERROR_CHANNEL = "jpake.error.channel";
public static final String JPAKE_ERROR_NETWORK = "jpake.error.network";
public static final String JPAKE_ERROR_SERVER = "jpake.error.server";
public static final String JPAKE_ERROR_TIMEOUT = "jpake.error.timeout";
public static final String JPAKE_ERROR_INTERNAL = "jpake.error.internal";
public static final String JPAKE_ERROR_INVALID = "jpake.error.invalid";
public static final String JPAKE_ERROR_NODATA = "jpake.error.nodata";
public static final String JPAKE_ERROR_KEYMISMATCH = "jpake.error.keymismatch";
public static final String JPAKE_ERROR_WRONGMESSAGE = "jpake.error.wrongmessage";
public static final String JPAKE_ERROR_USERABORT = "jpake.error.userabort";
public static final String JPAKE_ERROR_DELAYUNSUPPORTED = "jpake.error.delayunsupported";
}

View File

@ -19,7 +19,8 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chenxia Liu <liuche@mozilla.com>
* Chenxia Liu <liuche@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -98,6 +99,7 @@ public class SyncAuthenticatorService extends Service {
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
response);
intent.putExtra("accountType", Constants.ACCOUNTTYPE_SYNC);
intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, true);
final Bundle result = new Bundle();
result.putParcelable(AccountManager.KEY_INTENT, intent);
@ -133,6 +135,7 @@ public class SyncAuthenticatorService extends Service {
// Extract the username and password from the Account Manager, and ask
// the server for an appropriate AuthToken.
Log.d(LOG_TAG, "AccountManager.get(" + mContext + ")");
final AccountManager am = AccountManager.get(mContext);
final String password = am.getPassword(account);
if (password != null) {
@ -168,6 +171,7 @@ public class SyncAuthenticatorService extends Service {
result.putString(AccountManager.KEY_AUTHTOKEN, password);
return result;
}
Log.w(LOG_TAG, "Returning null bundle for getAuthToken.");
return null;
}

View File

@ -19,7 +19,8 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chenxia Liu <liuche@mozilla.com>
* Chenxia Liu <liuche@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -38,6 +39,7 @@
package org.mozilla.gecko.sync.setup.activities;
import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.android.Authorities;
import org.mozilla.gecko.sync.setup.Constants;
@ -48,57 +50,85 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.TextView;
public class AccountActivity extends AccountAuthenticatorActivity {
private final static String LOG_TAG = "AccountActivity";
private AccountManager mAccountManager;
private Context mContext;
private String username;
private String password;
private String key;
private String server;
private final static String LOG_TAG = "AccountActivity";
private final static String DEFAULT_SERVER = "https://auth.services.mozilla.com/";
private AccountManager mAccountManager;
private Context mContext;
private String username;
private String password;
private String key;
private String server;
// UI elements.
private EditText serverInput;
private EditText usernameInput;
private EditText passwordInput;
private EditText synckeyInput;
private CheckBox serverCheckbox;
private Button connectButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sync_account);
mContext = getApplicationContext();
mAccountManager = AccountManager.get(getApplicationContext());
}
Log.d(LOG_TAG, "AccountManager.get(" + mContext + ")");
mAccountManager = AccountManager.get(mContext);
protected void toggleServerField(boolean enabled) {
TextView serverField = (TextView) findViewById(R.id.server);
Log.i(LOG_TAG, "Toggling checkbox: " + enabled);
serverField.setFocusable(enabled);
serverField.setClickable(enabled);
// Find UI elements.
usernameInput = (EditText) findViewById(R.id.usernameInput);
passwordInput = (EditText) findViewById(R.id.passwordInput);
synckeyInput = (EditText) findViewById(R.id.keyInput);
serverInput = (EditText) findViewById(R.id.serverInput);
TextWatcher inputValidator = makeInputValidator();
usernameInput.addTextChangedListener(inputValidator);
passwordInput.addTextChangedListener(inputValidator);
synckeyInput.addTextChangedListener(inputValidator);
serverInput.addTextChangedListener(inputValidator);
connectButton = (Button) findViewById(R.id.accountConnectButton);
serverCheckbox = (CheckBox) findViewById(R.id.checkbox_server);
serverCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Log.i(LOG_TAG, "Toggling checkbox: " + isChecked);
// Hack for pre-3.0 Android: can enter text into disabled EditText.
if (!isChecked) { // Clear server input.
serverInput.setVisibility(View.GONE);
serverInput.setText("");
} else {
serverInput.setVisibility(View.VISIBLE);
}
// Activate connectButton if necessary.
activateView(connectButton, validateInputs());
}
});
}
@Override
public void onStart() {
super.onStart();
// Start with an empty form
setContentView(R.layout.sync_account);
CheckBox serverCheckbox = (CheckBox) findViewById(R.id.checkbox_server);
Log.i(LOG_TAG, "Setting onCheckedChangeListener.");
OnCheckedChangeListener listener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Log.i(LOG_TAG, "Toggling checkbox: " + isChecked);
toggleServerField(isChecked);
}
};
serverCheckbox.setOnCheckedChangeListener(listener);
// Enable or disable accordingly.
listener.onCheckedChanged(serverCheckbox, serverCheckbox.isChecked());
usernameInput.setText("");
passwordInput.setText("");
synckeyInput.setText("");
passwordInput.setText("");
}
public void cancelClickHandler(View target) {
@ -111,14 +141,13 @@ public class AccountActivity extends AccountAuthenticatorActivity {
*/
public void connectClickHandler(View target) {
Log.d(LOG_TAG, "connectClickHandler for view " + target);
username = ((EditText) findViewById(R.id.username)).getText().toString();
password = ((EditText) findViewById(R.id.password)).getText().toString();
key = ((EditText) findViewById(R.id.key)).getText().toString();
CheckBox serverCheckbox = (CheckBox) findViewById(R.id.checkbox_server);
EditText serverField = (EditText) findViewById(R.id.server);
username = usernameInput.getText().toString();
password = passwordInput.getText().toString();
key = synckeyInput.getText().toString();
if (serverCheckbox.isChecked()) {
server = serverField.getText().toString();
server = serverInput.getText().toString();
}
enableCredEntry(false);
// TODO : Authenticate with Sync Service, once implemented, with
// onAuthSuccess as callback
@ -126,19 +155,63 @@ public class AccountActivity extends AccountAuthenticatorActivity {
authCallback();
}
/* Helper UI functions */
private void enableCredEntry(boolean toEnable) {
usernameInput.setEnabled(toEnable);
passwordInput.setEnabled(toEnable);
synckeyInput.setEnabled(toEnable);
if (!toEnable) {
serverInput.setEnabled(toEnable);
} else {
serverInput.setEnabled(serverCheckbox.isChecked());
}
}
private TextWatcher makeInputValidator() {
return new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
activateView(connectButton, validateInputs());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
}
private boolean validateInputs() {
if (usernameInput.length() == 0 || passwordInput.length() == 0
|| synckeyInput.length() == 0
|| (serverCheckbox.isChecked() && serverInput.length() == 0)) {
return false;
}
return true;
}
/*
* Callback that handles auth based on success/failure
*/
private void authCallback() {
// Create and add account to AccountManager
// TODO: only allow one account to be added?
final Intent intent = createAccount(mAccountManager, username, key, password, server);
Log.d(LOG_TAG, "Using account manager " + mAccountManager);
final Intent intent = createAccount(mContext, mAccountManager,
username,
key, password, server);
setAccountAuthenticatorResult(intent.getExtras());
// Testing out the authFailure case
//authFailure();
// authFailure();
// TODO: Currently, we do not actually authenticate username/pass against Moz sync server.
// TODO: Currently, we do not actually authenticate username/pass against
// Moz sync server.
// Successful authentication result
setResult(RESULT_OK, intent);
@ -152,7 +225,12 @@ public class AccountActivity extends AccountAuthenticatorActivity {
}
// TODO: lift this out.
public static Intent createAccount(AccountManager accountManager, String username, String syncKey, String password, String serverURL) {
public static Intent createAccount(Context context,
AccountManager accountManager,
String username,
String syncKey,
String password, String serverURL) {
final Account account = new Account(username, Constants.ACCOUNTTYPE_SYNC);
final Bundle userbundle = new Bundle();
@ -161,18 +239,33 @@ public class AccountActivity extends AccountAuthenticatorActivity {
if (serverURL != null) {
Log.i(LOG_TAG, "Setting explicit server URL: " + serverURL);
userbundle.putString(Constants.OPTION_SERVER, serverURL);
} else {
userbundle.putString(Constants.OPTION_SERVER, DEFAULT_SERVER);
}
accountManager.addAccountExplicitly(account, password, userbundle);
Log.d(LOG_TAG, "Adding account for " + Constants.ACCOUNTTYPE_SYNC);
boolean result = accountManager.addAccountExplicitly(account, password, userbundle);
Log.d(LOG_TAG, "Account: " + account.toString());
Log.d(LOG_TAG, "Account: " + account.toString() + " added successfully? " + result);
if (!result) {
Log.e(LOG_TAG, "Error adding account!");
}
// Set components to sync (default: all).
ContentResolver.setMasterSyncAutomatically(true);
ContentResolver.setSyncAutomatically(account, Authorities.BROWSER_AUTHORITY, true);
// TODO: add other ContentProviders as needed (e.g. passwords)
// TODO: for each, also add to res/xml to make visible in account settings
Log.d(LOG_TAG, "Finished setting syncables.");
// TODO: correctly implement Sync Options.
Log.i(LOG_TAG, "Clearing preferences for this account.");
try {
Utils.getSharedPreferences(context, username, serverURL).edit().clear().commit();
} catch (Exception e) {
Log.e(LOG_TAG, "Could not clear prefs path!", e);
}
final Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, username);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNTTYPE_SYNC);
@ -181,13 +274,21 @@ public class AccountActivity extends AccountAuthenticatorActivity {
}
private void authFailure() {
enableCredEntry(true);
Intent intent = new Intent(mContext, SetupFailureActivity.class);
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
startActivity(intent);
}
private void authSuccess() {
Intent intent = new Intent(mContext, SetupSuccessActivity.class);
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
startActivity(intent);
finish();
}
private void activateView(View view, boolean toActivate) {
view.setEnabled(toActivate);
view.setClickable(toActivate);
}
}

View File

@ -67,7 +67,7 @@ public class SetupFailureActivity extends Activity {
}
public void cancelClickHandler(View target) {
moveTaskToBack(true);
setResult(RESULT_CANCELED);
finish();
}
}

View File

@ -41,6 +41,7 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.setup.Constants;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
@ -48,16 +49,15 @@ import android.widget.TextView;
public class SetupSuccessActivity extends Activity {
private TextView setupSubtitle;
private Context mContext;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getApplicationContext();
Bundle extras = this.getIntent().getExtras();
setContentView(R.layout.sync_setup_success);
setupSubtitle = ((TextView) findViewById(R.id.setup_success_subtitle));
// if (getIntent().getBooleanExtra(Constants.INTENT_EXTRA_IS_SETUP, false)) {
// setupSubtitle.setText(getString(R.string.sync_subtitle_manage));
// }
if (extras != null) {
boolean isSetup = extras.getBoolean(Constants.INTENT_EXTRA_IS_SETUP);
if (!isSetup) {
@ -69,8 +69,15 @@ public class SetupSuccessActivity extends Activity {
/* Click Handlers */
public void settingsClickHandler(View target) {
Intent intent = new Intent("android.settings.SYNC_SETTINGS");
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
startActivity(intent);
overridePendingTransition(0, 0);
finish();
}
public void pairClickHandler(View target) {
Intent intent = new Intent(mContext, SetupSyncActivity.class);
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, false);
startActivity(intent);
}
}

View File

@ -41,6 +41,7 @@ package org.mozilla.gecko.sync.setup.activities;
import org.json.simple.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.jpake.JPakeClient;
import org.mozilla.gecko.sync.jpake.JPakeNoActivePairingException;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.Account;
@ -48,18 +49,40 @@ import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
public class SetupSyncActivity extends AccountAuthenticatorActivity {
private final static String LOG_TAG = "SetupSync";
private final static String LOG_TAG = "SetupSync";
private boolean pairWithPin = false;
// UI elements for pairing through PIN entry.
private EditText row1;
private EditText row2;
private EditText row3;
private Button connectButton;
private LinearLayout pinError;
// UI elements for pairing through PIN generation.
private TextView setupTitleView;
private TextView setupNoDeviceLinkTitleView;
private TextView setupSubtitleView;
private TextView pinTextView;
private JPakeClient jClient;
// Android context.
private AccountManager mAccountManager;
private Context mContext;
@ -73,28 +96,12 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
public void onCreate(Bundle savedInstanceState) {
Log.i(LOG_TAG, "Called SetupSyncActivity.onCreate.");
super.onCreate(savedInstanceState);
setContentView(R.layout.sync_setup);
// Set up UI.
setupTitleView = ((TextView) findViewById(R.id.setup_title));
setupSubtitleView = (TextView) findViewById(R.id.setup_subtitle);
setupNoDeviceLinkTitleView = (TextView) findViewById(R.id.link_nodevice);
pinTextView = ((TextView) findViewById(R.id.text_pin));
// UI checks.
if (setupTitleView == null) {
Log.e(LOG_TAG, "No title view.");
}
if (setupSubtitleView == null) {
Log.e(LOG_TAG, "No subtitle view.");
}
if (setupNoDeviceLinkTitleView == null) {
Log.e(LOG_TAG, "No 'no device' link view.");
}
// Set Activity variables.
mAccountManager = AccountManager.get(getApplicationContext());
mContext = getApplicationContext();
Log.d(LOG_TAG, "AccountManager.get(" + mContext + ")");
mAccountManager = AccountManager.get(mContext);
}
@Override
@ -102,22 +109,52 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
Log.i(LOG_TAG, "Called SetupSyncActivity.onResume.");
super.onResume();
// Check whether Sync accounts exist; if so, display Pair text.
AccountManager mAccountManager = AccountManager.get(this);
Account[] accts = mAccountManager
.getAccountsByType(Constants.ACCOUNTTYPE_SYNC);
if (accts.length > 0) {
authSuccess(false);
// TODO: Change when:
// 1. enable setting up multiple accounts.
// 2. enable pair with PIN (entering pin, rather than displaying)
} else {
// Start J-PAKE for pairing if no accounts present.
if (!hasInternet()) {
setContentView(R.layout.sync_setup_nointernet);
return;
}
// Check whether Sync accounts exist; if not, display J-PAKE PIN.
Account[] accts = mAccountManager.getAccountsByType(Constants.ACCOUNTTYPE_SYNC);
if (accts.length == 0) { // Start J-PAKE for pairing if no accounts present.
displayReceiveNoPin();
jClient = new JPakeClient(this);
jClient.receiveNoPin();
return;
}
// Set layout based on starting Intent.
Bundle extras = this.getIntent().getExtras();
if (extras != null) {
boolean isSetup = extras.getBoolean(Constants.INTENT_EXTRA_IS_SETUP);
if (!isSetup) {
pairWithPin = true;
displayPairWithPin();
return;
}
}
// Display toast for "Only one account supported."
Toast toast = Toast.makeText(mContext, R.string.sync_notification_oneaccount, Toast.LENGTH_LONG);
toast.show();
finish();
}
@Override
public void onPause() {
super.onPause();
if (jClient != null) {
jClient.abort(Constants.JPAKE_ERROR_USERABORT);
}
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
}
/* Click Handlers */
public void manualClickHandler(View target) {
Intent accountIntent = new Intent(this, AccountActivity.class);
@ -130,7 +167,34 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
finish();
}
// Controller methods
public void connectClickHandler(View target) {
// Set UI feedback.
pinError.setVisibility(View.INVISIBLE);
enablePinEntry(false);
connectButton.requestFocus();
activateButton(connectButton, false);
// Extract PIN.
String pin = row1.getText().toString();
pin += row2.getText().toString() + row3.getText().toString();
// Start J-PAKE.
jClient = new JPakeClient(this);
jClient.pairWithPin(pin, false);
}
public void showClickHandler(View target) {
Uri uri = null;
// TODO: fetch these from fennec
if (pairWithPin) {
uri = Uri.parse(Constants.LINK_FIND_CODE);
} else {
uri = Uri.parse(Constants.LINK_FIND_ADD_DEVICE);
}
startActivity(new Intent(Intent.ACTION_VIEW, uri));
}
/* Controller methods */
public void displayPin(String pin) {
if (pin == null) {
Log.w(LOG_TAG, "Asked to display null pin.");
@ -157,36 +221,95 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
}
public void displayAbort(String error) {
// Start new JPakeClient for restarting J-PAKE.
jClient = new JPakeClient(this);
if (!Constants.JPAKE_ERROR_USERABORT.equals(error) && !hasInternet()) {
setContentView(R.layout.sync_setup_nointernet);
return;
}
if (pairWithPin) {
runOnUiThread(new Runnable() {
@Override
public void run() {
enablePinEntry(true);
row1.setText("");
row2.setText("");
row3.setText("");
row1.requestFocus();
runOnUiThread(new Runnable() {
@Override
public void run() {
// Restart pairing process.
jClient.receiveNoPin();
}
});
// Display error.
pinError.setVisibility(View.VISIBLE);
}
});
return;
}
// Start new JPakeClient for restarting J-PAKE.
Log.d(LOG_TAG, "abort reason: " + error);
if (!Constants.JPAKE_ERROR_USERABORT.equals(error)) {
jClient = new JPakeClient(this);
runOnUiThread(new Runnable() {
@Override
public void run() {
displayReceiveNoPin();
jClient.receiveNoPin();
}
});
}
}
/**
* Device has finished key exchange, waiting for remote device to set up or
* link to a Sync account. Display "waiting for other device" dialog.
*/
@SuppressWarnings("unchecked")
public void onPaired() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(mContext, SetupWaitingActivity.class);
// TODO: respond with abort if canceled.
startActivityForResult(intent, 0);
}
});
if (!pairWithPin) {
runOnUiThread(new Runnable() {
@Override
public void run() {
setContentView(R.layout.sync_setup_jpake_waiting);
}
});
return;
}
// Extract Sync account data.
Account[] accts = mAccountManager.getAccountsByType(Constants.ACCOUNTTYPE_SYNC);
if (accts.length == 0) {
// Error, no account present.
Log.e(LOG_TAG, "No accounts present.");
displayAbort(Constants.JPAKE_ERROR_INVALID);
return;
}
// TODO: Single account supported. Create account selection if spec changes.
Account account = accts[0];
String username = account.name;
String password = mAccountManager.getPassword(account);
String syncKey = mAccountManager.getUserData(account, Constants.OPTION_SYNCKEY);
String serverURL = mAccountManager.getUserData(account, Constants.OPTION_SERVER);
JSONObject jAccount = new JSONObject();
jAccount.put(Constants.JSON_KEY_SYNCKEY, syncKey);
jAccount.put(Constants.JSON_KEY_ACCOUNT, username);
jAccount.put(Constants.JSON_KEY_PASSWORD, password);
jAccount.put(Constants.JSON_KEY_SERVER, serverURL);
Log.d(LOG_TAG, "Extracted account data: " + jAccount.toJSONString());
try {
jClient.sendAndComplete(jAccount);
} catch (JPakeNoActivePairingException e) {
Log.e(LOG_TAG, "No active J-PAKE pairing.", e);
// TODO: some user-visible action!
}
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_CANCELED) {
displayAbort(Constants.JPAKE_ERROR_USERABORT);
/**
* J-PAKE pairing has started, but when this device has generated the PIN for
* pairing, does not require UI feedback to user.
*/
public void onPairingStart() {
if (pairWithPin) {
// TODO: add in functionality if/when adding pairWithPIN.
}
}
@ -197,37 +320,172 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
* @param jCreds
*/
public void onComplete(JSONObject jCreds) {
String accountName = (String) jCreds.get(Constants.JSON_KEY_ACCOUNT);
String password = (String) jCreds.get(Constants.JSON_KEY_PASSWORD);
String syncKey = (String) jCreds.get(Constants.JSON_KEY_SYNCKEY);
String serverURL = (String) jCreds.get(Constants.JSON_KEY_SERVER);
if (!pairWithPin) {
String accountName = (String) jCreds.get(Constants.JSON_KEY_ACCOUNT);
String password = (String) jCreds.get(Constants.JSON_KEY_PASSWORD);
String syncKey = (String) jCreds.get(Constants.JSON_KEY_SYNCKEY);
String serverURL = (String) jCreds.get(Constants.JSON_KEY_SERVER);
final Intent intent = AccountActivity.createAccount(mAccountManager, accountName, syncKey, password, serverURL);
setAccountAuthenticatorResult(intent.getExtras());
Log.d(LOG_TAG, "Using account manager " + mAccountManager);
final Intent intent = AccountActivity.createAccount(mContext, mAccountManager,
accountName,
syncKey, password, serverURL);
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
setResult(RESULT_OK, intent);
}
jClient = null; // Sync is set up. Kill reference to JPakeClient object.
runOnUiThread(new Runnable() {
@Override
public void run() {
authSuccess(true);
displayAccount(true);
}
});
}
private void authSuccess(boolean isSetup) {
/*
* Helper functions
*/
private void activateButton(Button button, boolean toActivate) {
button.setEnabled(toActivate);
button.setClickable(toActivate);
}
private void enablePinEntry(boolean toEnable) {
row1.setEnabled(toEnable);
row2.setEnabled(toEnable);
row3.setEnabled(toEnable);
}
/**
* Displays Sync account setup completed feedback to user.
*
* @param isSetup
* boolean for whether success screen is reached during setup
* completion, or otherwise.
*/
private void displayAccount(boolean isSetup) {
Intent intent = new Intent(mContext, SetupSuccessActivity.class);
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, isSetup);
startActivity(intent);
finish();
}
/**
* J-PAKE pairing has started, but when this device has generated the PIN for
* pairing, does not require UI feedback to user.
* Validate PIN entry fields to check if the three PIN entry fields are all
* filled in.
*
* @return true, if all PIN fields have 4 characters, false otherwise
*/
public void onPairingStart() {
// Do nothing.
// TODO: add in functionality if/when adding pairWithPIN.
private boolean pinEntryCompleted() {
if (row1.length() == 4 &&
row2.length() == 4 &&
row3.length() == 4) {
return true;
}
return false;
}
private boolean hasInternet() {
Log.d(LOG_TAG, "Checking internet connectivity.");
ConnectivityManager connManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
NetworkInfo mobile = connManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
if (wifi.isConnected() || mobile.isConnected()) {
Log.d(LOG_TAG, "Internet connected.");
return true;
}
return false;
}
private void displayPairWithPin() {
setContentView(R.layout.sync_setup_pair);
connectButton = (Button) findViewById(R.id.pair_button_connect);
pinError = (LinearLayout) findViewById(R.id.pair_error);
row1 = (EditText) findViewById(R.id.pair_row1);
row2 = (EditText) findViewById(R.id.pair_row2);
row3 = (EditText) findViewById(R.id.pair_row3);
row1.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (s.length() == 4) {
row2.requestFocus();
}
activateButton(connectButton, pinEntryCompleted());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
row2.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (s.length() == 4) {
row3.requestFocus();
}
activateButton(connectButton, pinEntryCompleted());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
row3.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
activateButton(connectButton, pinEntryCompleted());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
}
private void displayReceiveNoPin() {
setContentView(R.layout.sync_setup);
// Set up UI.
setupTitleView = ((TextView) findViewById(R.id.setup_title));
setupSubtitleView = (TextView) findViewById(R.id.setup_subtitle);
setupNoDeviceLinkTitleView = (TextView) findViewById(R.id.link_nodevice);
pinTextView = ((TextView) findViewById(R.id.text_pin));
// UI checks.
if (setupTitleView == null) {
Log.e(LOG_TAG, "No title view.");
}
if (setupSubtitleView == null) {
Log.e(LOG_TAG, "No subtitle view.");
}
if (setupNoDeviceLinkTitleView == null) {
Log.e(LOG_TAG, "No 'no device' link view.");
}
}
}

View File

@ -139,7 +139,7 @@ public class EnsureClusterURLStage implements GlobalSyncStage {
public void execute(final GlobalSession session) throws NoSuchStageException {
if (session.config.clusterURL != null) {
if (session.config.getClusterURL() != null) {
Log.i(LOG_TAG, "Cluster URL already set. Continuing with sync.");
session.advance();
return;
@ -178,14 +178,18 @@ public class EnsureClusterURLStage implements GlobalSyncStage {
Log.w(LOG_TAG, "Got HTTP failure fetching node assignment: " + statusCode);
if (statusCode == 404) {
URI serverURL = session.config.serverURL;
if (serverURL == null) {
Log.w(LOG_TAG, "No serverURL set to use as fallback cluster URL. Aborting sync.");
session.abort(new Exception("HTTP failure."), "Got failure fetching cluster URL.");
if (serverURL != null) {
Log.i(LOG_TAG, "Using serverURL <" + serverURL.toASCIIString() + "> as clusterURL.");
session.config.setClusterURL(serverURL);
session.advance();
return;
}
Log.i(LOG_TAG, "Using serverURL <" + serverURL.toASCIIString() + "> as clusterURL.");
session.config.clusterURL = serverURL;
Log.w(LOG_TAG, "No serverURL set to use as fallback cluster URL. Aborting sync.");
// Fallthrough to abort.
} else {
session.interpretHTTPFailure(response);
}
session.abort(new Exception("HTTP failure."), "Got failure fetching cluster URL.");
}
@Override

View File

@ -54,7 +54,7 @@ public class FetchMetaGlobalStage implements GlobalSyncStage {
}
@Override
public void handleSuccess(MetaGlobal global) {
public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
session.processMetaGlobal(global);
}
@ -69,7 +69,7 @@ public class FetchMetaGlobalStage implements GlobalSyncStage {
}
@Override
public void handleMissing(MetaGlobal global) {
public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
session.processMissingMetaGlobal(global);
}

View File

@ -37,11 +37,16 @@
package org.mozilla.gecko.sync.stage;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import java.io.IOException;
import java.net.URISyntaxException;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.MetaGlobalException;
import org.mozilla.gecko.sync.NoCollectionKeysSetException;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.SynchronizerConfiguration;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository;
import org.mozilla.gecko.sync.repositories.RecordFactory;
import org.mozilla.gecko.sync.repositories.Repository;
@ -87,11 +92,12 @@ public abstract class ServerSyncStage implements
* @param collection
* @return
* @throws NoCollectionKeysSetException
* @throws URISyntaxException
*/
protected Repository wrappedServerRepo() throws NoCollectionKeysSetException {
protected Repository wrappedServerRepo() throws NoCollectionKeysSetException, URISyntaxException {
String collection = this.getCollection();
KeyBundle collectionKey = session.keyForCollection(collection);
Server11Repository serverRepo = new Server11Repository(session.config.clusterURL.toASCIIString(),
Server11Repository serverRepo = new Server11Repository(session.config.getClusterURLString(),
session.config.username,
collection,
session);
@ -100,18 +106,22 @@ public abstract class ServerSyncStage implements
return cryptoRepo;
}
public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException {
protected String bundlePrefix() {
return this.getCollection() + ".";
}
public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException, URISyntaxException, NonObjectJSONException, IOException, ParseException {
Repository remote = wrappedServerRepo();
Synchronizer synchronizer = new Synchronizer();
synchronizer.repositoryA = remote;
synchronizer.repositoryB = this.getLocalRepository();
SynchronizerConfiguration config = session.configForEngine(this.getEngineName());
synchronizer.bundleA = config.remoteBundle;
synchronizer.bundleB = config.localBundle;
SynchronizerConfiguration config = new SynchronizerConfiguration(session.config.getBranch(bundlePrefix()));
synchronizer.load(config);
// TODO: should wipe in either direction?
// TODO: syncID?!
return synchronizer;
}
@ -138,6 +148,18 @@ public abstract class ServerSyncStage implements
} catch (NoCollectionKeysSetException e) {
session.abort(e, "No CollectionKeys.");
return;
} catch (URISyntaxException e) {
session.abort(e, "Invalid URI syntax for server repository.");
return;
} catch (NonObjectJSONException e) {
session.abort(e, "Invalid persisted JSON for config.");
return;
} catch (IOException e) {
session.abort(e, "Invalid persisted JSON for config.");
return;
} catch (ParseException e) {
session.abort(e, "Invalid persisted JSON for config.");
return;
}
Log.d(LOG_TAG, "Invoking synchronizer.");
synchronizer.synchronize(session.getContext(), this);
@ -147,6 +169,7 @@ public abstract class ServerSyncStage implements
@Override
public void onSynchronized(Synchronizer synchronizer) {
Log.d(LOG_TAG, "onSynchronized.");
synchronizer.save().persist(session.config.getBranch(bundlePrefix()));
session.advance();
}

View File

@ -19,7 +19,8 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Chenxia Liu <liuche@mozilla.com>
* Chenxia Liu <liuche@mozilla.com>
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -38,17 +39,20 @@
package org.mozilla.gecko.sync.syncadapter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.AlreadySyncingException;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.SyncConfigurationException;
import org.mozilla.gecko.sync.SyncException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.Cryptographer;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
@ -62,44 +66,107 @@ import android.accounts.OperationCanceledException;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SyncResult;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteException;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSessionCallback {
private static final String LOG_TAG = "SyncAdapter";
private static final String PREFS_EARLIEST_NEXT_SYNC = "earliestnextsync";
private static final String PREFS_INVALIDATE_AUTH_TOKEN = "invalidateauthtoken";
private static final int SHARED_PREFERENCES_MODE = 0;
private static final int BACKOFF_PAD_SECONDS = 5;
private final AccountManager mAccountManager;
private final Context mContext;
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
mContext = context;
Log.d(LOG_TAG, "AccountManager.get(" + mContext + ")");
mAccountManager = AccountManager.get(context);
}
/**
* Backoff.
*/
public synchronized long getEarliestNextSync() {
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
return sharedPreferences.getLong(PREFS_EARLIEST_NEXT_SYNC, 0);
}
public synchronized void setEarliestNextSync(long next) {
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
Editor edit = sharedPreferences.edit();
edit.putLong(PREFS_EARLIEST_NEXT_SYNC, next);
edit.commit();
}
public synchronized void extendEarliestNextSync(long next) {
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
if (sharedPreferences.getLong(PREFS_EARLIEST_NEXT_SYNC, 0) >= next) {
return;
}
Editor edit = sharedPreferences.edit();
edit.putLong(PREFS_EARLIEST_NEXT_SYNC, next);
edit.commit();
}
public synchronized boolean getShouldInvalidateAuthToken() {
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
return sharedPreferences.getBoolean(PREFS_INVALIDATE_AUTH_TOKEN, false);
}
public synchronized void clearShouldInvalidateAuthToken() {
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
Editor edit = sharedPreferences.edit();
edit.remove(PREFS_INVALIDATE_AUTH_TOKEN);
edit.commit();
}
public synchronized void setShouldInvalidateAuthToken() {
SharedPreferences sharedPreferences = mContext.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
Editor edit = sharedPreferences.edit();
edit.putBoolean(PREFS_INVALIDATE_AUTH_TOKEN, true);
edit.commit();
}
private void handleException(Exception e, SyncResult syncResult) {
if (e instanceof OperationCanceledException) {
Log.e(LOG_TAG, "Operation canceled. Aborting sync.");
e.printStackTrace();
return;
}
if (e instanceof AuthenticatorException) {
syncResult.stats.numParseExceptions++;
Log.e(LOG_TAG, "AuthenticatorException. Aborting sync.");
e.printStackTrace();
return;
}
if (e instanceof IOException) {
setShouldInvalidateAuthToken();
try {
if (e instanceof SQLiteConstraintException) {
Log.e(LOG_TAG, "Constraint exception. Aborting sync.", e);
syncResult.stats.numParseExceptions++; // This is as good as we can do.
return;
}
if (e instanceof SQLiteException) {
Log.e(LOG_TAG, "Couldn't open database (locked?). Aborting sync.", e);
syncResult.stats.numIoExceptions++;
return;
}
if (e instanceof OperationCanceledException) {
Log.e(LOG_TAG, "Operation canceled. Aborting sync.", e);
return;
}
if (e instanceof AuthenticatorException) {
syncResult.stats.numParseExceptions++;
Log.e(LOG_TAG, "AuthenticatorException. Aborting sync.", e);
return;
}
if (e instanceof IOException) {
syncResult.stats.numIoExceptions++;
Log.e(LOG_TAG, "IOException. Aborting sync.", e);
e.printStackTrace();
return;
}
syncResult.stats.numIoExceptions++;
Log.e(LOG_TAG, "IOException. Aborting sync.");
e.printStackTrace();
return;
Log.e(LOG_TAG, "Unknown exception. Aborting sync.", e);
} finally {
notifyMonitor();
}
syncResult.stats.numIoExceptions++;
Log.e(LOG_TAG, "Unknown exception. Aborting sync.");
e.printStackTrace();
}
private AccountManagerFuture<Bundle> getAuthToken(final Account account,
@ -117,7 +184,6 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
} catch (Exception e) {
Log.e(LOG_TAG, "Couldn't invalidate auth token: " + e);
}
}
@Override
@ -132,7 +198,23 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
public Object syncMonitor = new Object();
private SyncResult syncResult;
public boolean shouldPerformSync = false;
/**
* Return the number of milliseconds until we're allowed to sync again,
* or 0 if now is fine.
*/
public long delayMilliseconds() {
long earliestNextSync = getEarliestNextSync();
if (earliestNextSync <= 0) {
return 0;
}
long now = System.currentTimeMillis();
return Math.max(0, earliestNextSync - now);
}
@Override
public boolean shouldBackOff() {
return delayMilliseconds() > 0;
}
@Override
public void onPerformSync(final Account account,
@ -141,55 +223,84 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
final ContentProviderClient provider,
final SyncResult syncResult) {
long delay = delayMilliseconds();
if (delay > 0) {
Log.i(LOG_TAG, "Not syncing: must wait another " + delay + "ms.");
long remainingSeconds = delay / 1000;
syncResult.delayUntil = remainingSeconds + BACKOFF_PAD_SECONDS;
return;
}
// TODO: don't clear the auth token unless we have a sync error.
Log.i(LOG_TAG, "Got onPerformSync. Extras bundle is " + extras);
Log.d(LOG_TAG, "Extras clusterURL: " + extras.getString("clusterURL"));
Log.i(LOG_TAG, "Account name: " + account.name);
if (!shouldPerformSync) {
Log.i(LOG_TAG, "Not performing sync.");
return;
}
Log.i(LOG_TAG, "XXX CLEARING AUTH TOKEN XXX");
// TODO: don't always invalidate; use getShouldInvalidateAuthToken.
// However, this fixes Bug 716815, so it'll do for now.
Log.d(LOG_TAG, "Invalidating auth token.");
invalidateAuthToken(account);
final SyncAdapter self = this;
AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
final AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
Log.i(LOG_TAG, "AccountManagerCallback invoked.");
// TODO: N.B.: Future must not be used on the main thread.
try {
Bundle bundle = future.getResult(60L, TimeUnit.SECONDS);
String username = bundle.getString(Constants.OPTION_USERNAME);
String syncKey = bundle.getString(Constants.OPTION_SYNCKEY);
String serverURL = bundle.getString(Constants.OPTION_SERVER);
String password = bundle.getString(AccountManager.KEY_AUTHTOKEN);
Bundle bundle = future.getResult(60L, TimeUnit.SECONDS);
if (bundle.containsKey("KEY_INTENT")) {
Log.w(LOG_TAG, "KEY_INTENT included in AccountManagerFuture bundle. Problem?");
}
String username = bundle.getString(Constants.OPTION_USERNAME);
String syncKey = bundle.getString(Constants.OPTION_SYNCKEY);
String serverURL = bundle.getString(Constants.OPTION_SERVER);
String password = bundle.getString(AccountManager.KEY_AUTHTOKEN);
Log.d(LOG_TAG, "Username: " + username);
Log.d(LOG_TAG, "Server: " + serverURL);
Log.d(LOG_TAG, "Password: " + password); // TODO: remove
Log.d(LOG_TAG, "Key: " + syncKey); // TODO: remove
Log.d(LOG_TAG, "Password? " + (password != null));
Log.d(LOG_TAG, "Key? " + (syncKey != null));
if (password == null) {
Log.e(LOG_TAG, "No password: aborting sync.");
syncResult.stats.numAuthExceptions++;
notifyMonitor();
return;
}
if (syncKey == null) {
Log.e(LOG_TAG, "No Sync Key: aborting sync.");
syncResult.stats.numAuthExceptions++;
notifyMonitor();
return;
}
KeyBundle keyBundle = new KeyBundle(username, syncKey);
// Support multiple accounts by mapping each server/account pair to a branch of the
// shared preferences space.
String prefsPath = Utils.getPrefsPath(username, serverURL);
self.performSync(account, extras, authority, provider, syncResult,
username, password, serverURL, keyBundle);
username, password, prefsPath, serverURL, keyBundle);
} catch (Exception e) {
self.handleException(e, syncResult);
return;
}
}
};
Handler handler = null;
getAuthToken(account, callback, handler);
final Handler handler = null;
final Runnable fetchAuthToken = new Runnable() {
@Override
public void run() {
getAuthToken(account, callback, handler);
}
};
synchronized (syncMonitor) {
// Perform the work in a new thread from within this synchronized block,
// which allows us to be waiting on the monitor before the callback can
// notify us in a failure case. Oh, concurrent programming.
new Thread(fetchAuthToken).start();
Log.i(LOG_TAG, "Waiting on sync monitor.");
try {
Log.i(LOG_TAG, "Waiting on sync monitor.");
syncMonitor.wait();
} catch (InterruptedException e) {
Log.i(LOG_TAG, "Waiting on sync monitor interrupted.", e);
@ -200,6 +311,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
/**
* Now that we have a sync key and password, go ahead and do the work.
* @param prefsPath TODO
* @throws NoSuchAlgorithmException
* @throws IllegalArgumentException
* @throws SyncConfigurationException
@ -212,8 +324,8 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
ContentProviderClient provider,
SyncResult syncResult,
String username, String password,
String serverURL,
KeyBundle keyBundle)
String prefsPath,
String serverURL, KeyBundle keyBundle)
throws NoSuchAlgorithmException,
SyncConfigurationException,
IllegalArgumentException,
@ -224,8 +336,8 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
this.syncResult = syncResult;
// TODO: default serverURL.
GlobalSession globalSession = new GlobalSession(SyncConfiguration.DEFAULT_USER_API,
serverURL, username, password, keyBundle,
this, this.mContext, null);
serverURL, username, password, prefsPath,
keyBundle, this, this.mContext, extras);
globalSession.start();
@ -234,18 +346,25 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
private void notifyMonitor() {
synchronized (syncMonitor) {
Log.i(LOG_TAG, "Notifying sync monitor.");
syncMonitor.notify();
syncMonitor.notifyAll();
}
}
// Implementing GlobalSession callbacks.
@Override
public void handleError(GlobalSession globalSession, Exception ex) {
Log.i(LOG_TAG, "GlobalSession indicated error.");
Log.i(LOG_TAG, "GlobalSession indicated error. Flagging auth token as invalid, just in case.");
setShouldInvalidateAuthToken();
this.updateStats(globalSession, ex);
notifyMonitor();
}
@Override
public void handleAborted(GlobalSession globalSession, String reason) {
Log.w(LOG_TAG, "Sync aborted: " + reason);
notifyMonitor();
}
/**
* Introspect the exception, incrementing the appropriate stat counters.
* TODO: increment number of inserts, deletes, conflicts.
@ -265,6 +384,8 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
@Override
public void handleSuccess(GlobalSession globalSession) {
Log.i(LOG_TAG, "GlobalSession indicated success.");
Log.i(LOG_TAG, "Prefs target: " + globalSession.config.prefsPath);
globalSession.config.persistToPrefs();
notifyMonitor();
}
@ -273,82 +394,11 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
GlobalSession globalSession) {
Log.i(LOG_TAG, "Stage completed: " + currentState);
}
}
// try {
// // see if we already have a sync-state attached to this account. By handing
// // This value to the server, we can just get the contacts that have
// // been updated on the server-side since our last sync-up
// long lastSyncMarker = getServerSyncMarker(account);
//
// // By default, contacts from a 3rd party provider are hidden in the contacts
// // list. So let's set the flag that causes them to be visible, so that users
// // can actually see these contacts.
// if (lastSyncMarker == 0) {
// ContactManager.setAccountContactsVisibility(getContext(), account, true);
// }
//
// List<RawContact> dirtyContacts;
// List<RawContact> updatedContacts;
//
// // Use the account manager to request the AuthToken we'll need
// // to talk to our sample server. If we don't have an AuthToken
// // yet, this could involve a round-trip to the server to request
// // and AuthToken.
// final String authtoken = mAccountManager.blockingGetAuthToken(account,
// Constants.AUTHTOKEN_TYPE, NOTIFY_AUTH_FAILURE);
//
// // Make sure that the sample group exists
// final long groupId = ContactManager.ensureSampleGroupExists(mContext, account);
//
// // Find the local 'dirty' contacts that we need to tell the server about...
// // Find the local users that need to be sync'd to the server...
// dirtyContacts = ContactManager.getDirtyContacts(mContext, account);
//
// // Send the dirty contacts to the server, and retrieve the server-side changes
// updatedContacts = NetworkUtilities.syncContacts(account, authtoken,
// lastSyncMarker, dirtyContacts);
//
// // Update the local contacts database with the changes. updateContacts()
// // returns a syncState value that indicates the high-water-mark for
// // the changes we received.
// Log.d(TAG, "Calling contactManager's sync contacts");
// long newSyncState = ContactManager.updateContacts(mContext,
// account.name,
// updatedContacts,
// groupId,
// lastSyncMarker);
//
// // This is a demo of how you can update IM-style status messages
// // for contacts on the client. This probably won't apply to
// // 2-way contact sync providers - it's more likely that one-way
// // sync providers (IM clients, social networking apps, etc) would
// // use this feature.
// ContactManager.updateStatusMessages(mContext, updatedContacts);
//
// // Save off the new sync marker. On our next sync, we only want to receive
// // contacts that have changed since this sync...
// setServerSyncMarker(account, newSyncState);
//
// if (dirtyContacts.size() > 0) {
// ContactManager.clearSyncFlags(mContext, dirtyContacts);
// }
//
// } catch (final AuthenticatorException e) {
// Log.e(TAG, "AuthenticatorException", e);
// syncResult.stats.numParseExceptions++;
// } catch (final OperationCanceledException e) {
// Log.e(TAG, "OperationCanceledExcetpion", e);
// } catch (final IOException e) {
// Log.e(TAG, "IOException", e);
// syncResult.stats.numIoExceptions++;
// } catch (final AuthenticationException e) {
// Log.e(TAG, "AuthenticationException", e);
// syncResult.stats.numAuthExceptions++;
// } catch (final ParseException e) {
// Log.e(TAG, "ParseException", e);
// syncResult.stats.numParseExceptions++;
// } catch (final JSONException e) {
// Log.e(TAG, "JSONException", e);
// syncResult.stats.numParseExceptions++;
// }
@Override
public void requestBackoff(long backoff) {
if (backoff > 0) {
this.extendEarliestNextSync(System.currentTimeMillis() + backoff);
}
}
}

View File

@ -0,0 +1,168 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.synchronizer;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.util.Log;
/**
* Consume records from a queue inside a RecordsChannel, as fast as we can.
* TODO: rewrite this in terms of an ExecutorService and a CompletionService.
* See Bug 713483.
*
* @author rnewman
*
*/
class ConcurrentRecordConsumer extends RecordConsumer {
private static final String LOG_TAG = "ConcurrentRecordConsumer";
/**
* When this is true and all records have been processed, the consumer
* will notify its delegate.
*/
protected boolean allRecordsQueued = false;
private long counter = 0;
public ConcurrentRecordConsumer(RecordsConsumerDelegate delegate) {
this.delegate = delegate;
}
private static void info(String message) {
Utils.logToStdout(LOG_TAG, "::INFO: ", message);
Log.i(LOG_TAG, message);
}
private static void debug(String message) {
Utils.logToStdout(LOG_TAG, ":: DEBUG: ", message);
Log.d(LOG_TAG, message);
}
private static void trace(String message) {
if (!Utils.ENABLE_TRACE_LOGGING) {
return;
}
Utils.logToStdout(LOG_TAG, ":: TRACE: ", message);
Log.d(LOG_TAG, message);
}
private Object monitor = new Object();
@Override
public void doNotify() {
synchronized (monitor) {
monitor.notify();
}
}
@Override
public void queueFilled() {
debug("Queue filled.");
synchronized (monitor) {
this.allRecordsQueued = true;
monitor.notify();
}
}
@Override
public void halt() {
synchronized (monitor) {
this.stopImmediately = true;
monitor.notify();
}
}
private Object countMonitor = new Object();
@Override
public void stored() {
debug("Record stored. Notifying.");
synchronized (countMonitor) {
counter++;
}
}
private void consumerIsDone() {
info("Consumer is done. Processed " + counter + ((counter == 1) ? " record." : " records."));
delegate.consumerIsDone(!allRecordsQueued);
}
@Override
public void run() {
while (true) {
synchronized (monitor) {
trace("run() took monitor.");
if (stopImmediately) {
debug("Stopping immediately. Clearing queue.");
delegate.getQueue().clear();
debug("Notifying consumer.");
consumerIsDone();
return;
}
debug("run() dropped monitor.");
}
// The queue is concurrent-safe.
while (!delegate.getQueue().isEmpty()) {
trace("Grabbing record...");
Record record = delegate.getQueue().remove();
delegate.store(record);
trace("Done with record.");
}
synchronized (monitor) {
trace("run() took monitor.");
if (allRecordsQueued) {
debug("Done with records and no more to come. Notifying consumerIsDone.");
consumerIsDone();
return;
}
if (stopImmediately) {
debug("Done with records and told to stop immediately. Notifying consumerIsDone.");
consumerIsDone();
return;
}
try {
debug("Not told to stop but no records. Waiting.");
monitor.wait(10000);
} catch (InterruptedException e) {
// TODO
}
trace("run() dropped monitor.");
}
}
}
}

View File

@ -37,134 +37,24 @@
package org.mozilla.gecko.sync.synchronizer;
import org.mozilla.gecko.sync.repositories.domain.Record;
public abstract class RecordConsumer implements Runnable {
import android.util.Log;
public abstract void stored();
/**
* Consume records from a queue inside a RecordsChannel, storing them serially.
* @author rnewman
*
*/
class RecordConsumer implements Runnable {
private static final String LOG_TAG = "RecordConsumer";
private boolean stopEventually = false;
private boolean stopImmediately = false;
private RecordsConsumerDelegate delegate;
private long counter = 0;
/**
* There are no more store items to arrive at the delegate.
* When you're done, take care of finishing up.
*/
public abstract void queueFilled();
public abstract void halt();
public RecordConsumer(RecordsConsumerDelegate delegate) {
this.delegate = delegate;
public abstract void doNotify();
protected boolean stopImmediately = false;
protected RecordsConsumerDelegate delegate;
public RecordConsumer() {
super();
}
private Object monitor = new Object();
public void doNotify() {
synchronized (monitor) {
monitor.notify();
}
}
private static void info(String message) {
System.out.println("INFO: " + message);
Log.i(LOG_TAG, message);
}
private static void warn(String message, Exception ex) {
System.out.println("WARN: " + message);
Log.w(LOG_TAG, message, ex);
}
private static void debug(String message) {
System.out.println("DEBUG: " + message);
Log.d(LOG_TAG, message);
}
public void stop(boolean immediately) {
debug("Called stop(" + immediately + ").");
synchronized (monitor) {
debug("stop() took monitor.");
this.stopEventually = true;
this.stopImmediately = immediately;
monitor.notify();
debug("stop() dropped monitor.");
}
}
private Object storeSerializer = new Object();
public void stored() {
debug("Record stored. Notifying.");
synchronized (storeSerializer) {
debug("stored() took storeSerializer.");
counter++;
storeSerializer.notify();
debug("stored() dropped storeSerializer.");
}
}
private void storeSerially(Record record) {
debug("New record to store.");
synchronized (storeSerializer) {
debug("storeSerially() took storeSerializer.");
debug("Storing...");
try {
this.delegate.store(record);
} catch (Exception e) {
warn("Got exception in store. Not waiting.", e);
return; // So we don't block for a stored() that never comes.
}
try {
storeSerializer.wait();
} catch (InterruptedException e) {
// TODO
}
debug("storeSerially() dropped storeSerializer.");
}
}
private void consumerIsDone() {
info("Consumer is done. Processed " + counter + ((counter == 1) ? " record." : " records."));
delegate.consumerIsDone();
}
@Override
public void run() {
while (true) {
synchronized (monitor) {
debug("run() took monitor.");
if (stopImmediately) {
debug("Stopping immediately. Clearing queue.");
delegate.getQueue().clear();
debug("Notifying consumer.");
consumerIsDone();
return;
}
debug("run() dropped monitor.");
}
// The queue is concurrent-safe.
while (!delegate.getQueue().isEmpty()) {
debug("Grabbing record...");
Record record = delegate.getQueue().remove();
// Block here, allowing us to process records
// serially.
debug("Invoking storeSerially...");
this.storeSerially(record);
debug("Done with record.");
}
synchronized (monitor) {
debug("run() took monitor.");
if (stopEventually) {
debug("Done with records and told to stop. Notifying consumer.");
consumerIsDone();
return;
}
try {
debug("Not told to stop but no records. Waiting.");
monitor.wait(10000);
} catch (InterruptedException e) {
// TODO
}
debug("run() dropped monitor.");
}
}
}
}

View File

@ -38,9 +38,14 @@
package org.mozilla.gecko.sync.synchronizer;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionBeginDelegate;
import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionStoreDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
@ -52,10 +57,48 @@ import android.util.Log;
* Pulls records from `source`, applying them to `sink`.
* Notifies its delegate of errors and completion.
*
* All stores (initiated by a fetch) must have been completed before storeDone
* is invoked on the sink. This is to avoid the existing stored items being
* considered as the total set, with onStoreCompleted being called when they're
* done:
*
* store(A) store(B)
* store(C) storeDone()
* store(A) finishes. Store job begins.
* store(C) finishes. Store job begins.
* storeDone() finishes.
* Storing of A complete.
* Storing of C complete.
* We're done! Call onStoreCompleted.
* store(B) finishes... uh oh.
*
* In other words, storeDone must be gated on the synchronous invocation of every store.
*
* Similarly, we require that every store callback have returned before onStoreCompleted is invoked.
*
* This whole set of guarantees should be achievable thusly:
*
* * The fetch process must run in a single thread, and invoke store()
* synchronously. After processing every incoming record, storeDone is called,
* setting a flag.
* If the fetch cannot be implicitly queued, it must be explicitly queued.
* In this implementation, we assume that fetch callbacks are strictly ordered in this way.
*
* * The store process must be (implicitly or explicitly) queued. When the
* queue empties, the consumer checks the storeDone flag. If it's set, and the
* queue is exhausted, invoke onStoreCompleted.
*
* RecordsChannel exists to enforce this ordering of operations.
*
* @author rnewman
*
*/
class RecordsChannel implements RepositorySessionFetchRecordsDelegate, RepositorySessionStoreDelegate, RecordsConsumerDelegate, RepositorySessionBeginDelegate {
class RecordsChannel implements
RepositorySessionFetchRecordsDelegate,
RepositorySessionStoreDelegate,
RecordsConsumerDelegate,
RepositorySessionBeginDelegate {
private static final String LOG_TAG = "RecordsChannel";
public RepositorySession source;
public RepositorySession sink;
@ -64,44 +107,58 @@ class RecordsChannel implements RepositorySessionFetchRecordsDelegate, Repositor
private long end = -1; // Oo er, missus.
public RecordsChannel(RepositorySession source, RepositorySession sink, RecordsChannelDelegate delegate) {
this.source = source;
this.sink = sink;
this.delegate = delegate;
this.source = source;
this.sink = sink;
this.delegate = delegate;
this.timestamp = source.lastSyncTimestamp;
}
/*
* We push fetched records into a queue.
* A separate thread is waiting for us to notify it of work to do.
* When we tell it to stop, it'll stop.
* When it stops, we notify our delegate of completion.
* When we tell it to stop, it'll stop. We do that when the fetch
* is completed.
* When it stops, we tell the sink that there are no more records,
* and wait for the sink to tell us that storing is done.
* Then we notify our delegate of completion.
*/
private boolean waitingForQueueDone = false;
ConcurrentLinkedQueue<Record> toProcess = new ConcurrentLinkedQueue<Record>();
private RecordConsumer consumer;
private void enqueue(Record record) {
toProcess.add(record);
}
private boolean waitingForQueueDone = false;
private ConcurrentLinkedQueue<Record> toProcess = new ConcurrentLinkedQueue<Record>();
@Override
public ConcurrentLinkedQueue<Record> getQueue() {
return toProcess;
}
@Override
public void consumerIsDone() {
Log.d(LOG_TAG, "Consumer is done. Are we waiting for it? " + waitingForQueueDone);
if (waitingForQueueDone) {
waitingForQueueDone = false;
delegate.onFlowCompleted(this, end);
}
}
protected boolean isReady() {
return source.isActive() && sink.isActive();
}
private static void info(String message) {
Utils.logToStdout(LOG_TAG, "::INFO: ", message);
Log.i(LOG_TAG, message);
}
private static void trace(String message) {
if (!Utils.ENABLE_TRACE_LOGGING) {
return;
}
Utils.logToStdout(LOG_TAG, "::TRACE: ", message);
Log.d(LOG_TAG, message);
}
private static void error(String message, Exception e) {
Utils.logToStdout(LOG_TAG, "::ERROR: ", message);
Log.e(LOG_TAG, message, e);
}
private static void warn(String message, Exception e) {
Utils.logToStdout(LOG_TAG, "::WARN: ", message);
Log.w(LOG_TAG, message, e);
}
/**
* Attempt to abort an outstanding fetch. Finish both sessions.
*/
@ -125,8 +182,9 @@ class RecordsChannel implements RepositorySessionFetchRecordsDelegate, Repositor
}
this.delegate.onFlowBeginFailed(this, new SessionNotBegunException(failed));
}
sink.setStoreDelegate(this);
// Start a consumer thread.
this.consumer = new RecordConsumer(this);
this.consumer = new ConcurrentRecordConsumer(this);
ThreadPool.run(this.consumer);
waitingForQueueDone = true;
source.fetchSince(timestamp, this);
@ -136,51 +194,77 @@ class RecordsChannel implements RepositorySessionFetchRecordsDelegate, Repositor
* Begin both sessions, invoking flow() when done.
*/
public void beginAndFlow() {
info("Beginning source.");
source.begin(this);
}
@Override
public void store(Record record) {
sink.store(record, this);
try {
sink.store(record);
} catch (NoStoreDelegateException e) {
error("Got NoStoreDelegateException in RecordsChannel.store(). This should not occur. Aborting.", e);
delegate.onFlowStoreFailed(this, e);
this.abort();
}
}
@Override
public void onFetchFailed(Exception ex, Record record) {
Log.w(LOG_TAG, "onFetchFailed. Calling for immediate stop.", ex);
this.consumer.stop(true);
warn("onFetchFailed. Calling for immediate stop.", ex);
this.consumer.halt();
}
@Override
public void onFetchedRecord(Record record) {
this.enqueue(record);
this.toProcess.add(record);
this.consumer.doNotify();
}
@Override
public void onFetchCompleted(long end) {
Log.i(LOG_TAG, "onFetchCompleted. Stopping consumer once stores are done.");
this.end = end;
this.consumer.stop(false);
}
@Override
public void onStoreFailed(Exception ex) {
this.consumer.stored();
delegate.onFlowStoreFailed(this, ex);
// TODO: abort?
}
@Override
public void onStoreSucceeded(Record record) {
this.consumer.stored();
}
@Override
public void onFetchSucceeded(Record[] records, long end) {
for (Record record : records) {
this.toProcess.add(record);
}
this.consumer.doNotify();
this.onFetchCompleted(end);
}
@Override
public void onFetchCompleted(long end) {
info("onFetchCompleted. Stopping consumer once stores are done.");
info("Fetch timestamp is " + end);
this.end = end;
this.consumer.queueFilled();
}
@Override
public void onRecordStoreFailed(Exception ex) {
this.consumer.stored();
delegate.onFlowStoreFailed(this, ex);
// TODO: abort?
}
@Override
public void onRecordStoreSucceeded(Record record) {
this.consumer.stored();
}
@Override
public void consumerIsDone(boolean allRecordsQueued) {
trace("Consumer is done. Are we waiting for it? " + waitingForQueueDone);
if (waitingForQueueDone) {
waitingForQueueDone = false;
this.sink.storeDone(); // Now we'll be waiting for onStoreCompleted.
}
}
@Override
public void onStoreCompleted() {
info("onStoreCompleted. Notifying delegate of onFlowCompleted. End is " + end);
// TODO: synchronize on consumer callback?
delegate.onFlowCompleted(this, end);
}
@Override
@ -191,9 +275,11 @@ class RecordsChannel implements RepositorySessionFetchRecordsDelegate, Repositor
@Override
public void onBeginSucceeded(RepositorySession session) {
if (session == source) {
info("Source session began. Beginning sink session.");
sink.begin(this);
}
if (session == sink) {
info("Sink session began. Beginning flow.");
this.flow();
return;
}
@ -202,65 +288,18 @@ class RecordsChannel implements RepositorySessionFetchRecordsDelegate, Repositor
}
@Override
public RepositorySessionStoreDelegate deferredStoreDelegate() {
final RepositorySessionStoreDelegate self = this;
return new RepositorySessionStoreDelegate() {
@Override
public void onStoreSucceeded(final Record record) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.onStoreSucceeded(record);
}
});
}
@Override
public void onStoreFailed(final Exception ex) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.onStoreFailed(ex);
}
});
}
@Override
public RepositorySessionStoreDelegate deferredStoreDelegate() {
return this;
}
};
public RepositorySessionStoreDelegate deferredStoreDelegate(final ExecutorService executor) {
return new DeferredRepositorySessionStoreDelegate(this, executor);
}
@Override
public RepositorySessionBeginDelegate deferredBeginDelegate() {
final RepositorySessionBeginDelegate self = this;
return new RepositorySessionBeginDelegate() {
public RepositorySessionBeginDelegate deferredBeginDelegate(final ExecutorService executor) {
return new DeferredRepositorySessionBeginDelegate(this, executor);
}
@Override
public void onBeginSucceeded(final RepositorySession session) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.onBeginSucceeded(session);
}
});
}
@Override
public void onBeginFailed(final Exception ex) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.onBeginFailed(ex);
}
});
}
@Override
public RepositorySessionBeginDelegate deferredBeginDelegate() {
return this;
}
};
@Override
public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
// Lie outright. We know that all of our fetch methods are safe.
return this;
}
}

View File

@ -43,6 +43,14 @@ import org.mozilla.gecko.sync.repositories.domain.Record;
interface RecordsConsumerDelegate {
public abstract ConcurrentLinkedQueue<Record> getQueue();
public abstract void consumerIsDone();
/**
* Called when no more items will be processed.
* If forced is true, the consumer is terminating because it was told to halt;
* not all items will necessarily have been processed.
* If forced is false, the consumer has invoked store and received an onStoreCompleted callback.
* @param forced
*/
public abstract void consumerIsDone(boolean forced);
public abstract void store(Record record);
}

View File

@ -0,0 +1,179 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is 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):
* Richard Newman <rnewman@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.mozilla.gecko.sync.synchronizer;
import org.mozilla.gecko.sync.repositories.domain.Record;
import android.util.Log;
/**
* Consume records from a queue inside a RecordsChannel, storing them serially.
* @author rnewman
*
*/
class SerialRecordConsumer extends RecordConsumer {
private static final String LOG_TAG = "SerialRecordConsumer";
protected boolean stopEventually = false;
private long counter = 0;
public SerialRecordConsumer(RecordsConsumerDelegate delegate) {
this.delegate = delegate;
}
private static void info(String message) {
System.out.println("INFO: " + message);
Log.i(LOG_TAG, message);
}
private static void warn(String message, Exception ex) {
System.out.println("WARN: " + message);
Log.w(LOG_TAG, message, ex);
}
private static void debug(String message) {
System.out.println("DEBUG: " + message);
Log.d(LOG_TAG, message);
}
private Object monitor = new Object();
@Override
public void doNotify() {
synchronized (monitor) {
monitor.notify();
}
}
@Override
public void queueFilled() {
debug("Queue filled.");
synchronized (monitor) {
this.stopEventually = true;
monitor.notify();
}
}
@Override
public void halt() {
debug("Halting.");
synchronized (monitor) {
this.stopEventually = true;
this.stopImmediately = true;
monitor.notify();
}
}
private Object storeSerializer = new Object();
@Override
public void stored() {
debug("Record stored. Notifying.");
synchronized (storeSerializer) {
debug("stored() took storeSerializer.");
counter++;
storeSerializer.notify();
debug("stored() dropped storeSerializer.");
}
}
private void storeSerially(Record record) {
debug("New record to store.");
synchronized (storeSerializer) {
debug("storeSerially() took storeSerializer.");
debug("Storing...");
try {
this.delegate.store(record);
} catch (Exception e) {
warn("Got exception in store. Not waiting.", e);
return; // So we don't block for a stored() that never comes.
}
try {
debug("Waiting...");
storeSerializer.wait();
} catch (InterruptedException e) {
// TODO
}
debug("storeSerially() dropped storeSerializer.");
}
}
private void consumerIsDone() {
info("Consumer is done. Processed " + counter + ((counter == 1) ? " record." : " records."));
delegate.consumerIsDone(stopImmediately);
}
@Override
public void run() {
while (true) {
synchronized (monitor) {
debug("run() took monitor.");
if (stopImmediately) {
debug("Stopping immediately. Clearing queue.");
delegate.getQueue().clear();
debug("Notifying consumer.");
consumerIsDone();
return;
}
debug("run() dropped monitor.");
}
// The queue is concurrent-safe.
while (!delegate.getQueue().isEmpty()) {
debug("Grabbing record...");
Record record = delegate.getQueue().remove();
// Block here, allowing us to process records
// serially.
debug("Invoking storeSerially...");
this.storeSerially(record);
debug("Done with record.");
}
synchronized (monitor) {
debug("run() took monitor.");
if (stopEventually) {
debug("Done with records and told to stop. Notifying consumer.");
consumerIsDone();
return;
}
try {
debug("Not told to stop but no records. Waiting.");
monitor.wait(10000);
} catch (InterruptedException e) {
// TODO
}
debug("run() dropped monitor.");
}
}
}
}

View File

@ -37,6 +37,7 @@
package org.mozilla.gecko.sync.synchronizer;
import org.mozilla.gecko.sync.SynchronizerConfiguration;
import org.mozilla.gecko.sync.repositories.Repository;
import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
@ -77,10 +78,17 @@ public class Synchronizer {
}
@Override
public void onSynchronized(SynchronizerSession session) {
public void onSynchronized(SynchronizerSession synchronizerSession) {
Log.d(LOG_TAG, "Got onSynchronized.");
Log.d(LOG_TAG, "Notifying SynchronizerDelegate.");
this.synchronizerDelegate.onSynchronized(session.getSynchronizer());
this.synchronizerDelegate.onSynchronized(synchronizerSession.getSynchronizer());
}
@Override
public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) {
Log.d(LOG_TAG, "Got onSynchronizeSkipped.");
Log.d(LOG_TAG, "Notifying SynchronizerDelegate as if on success.");
this.synchronizerDelegate.onSynchronized(synchronizerSession.getSynchronizer());
}
@Override
@ -123,4 +131,16 @@ public class Synchronizer {
SynchronizerSession session = new SynchronizerSession(this, sessionDelegate);
session.init(context, bundleA, bundleB);
}
public SynchronizerConfiguration save() {
String syncID = null; // TODO: syncID.
return new SynchronizerConfiguration(syncID, bundleA, bundleB);
}
// Not thread-safe.
public void load(SynchronizerConfiguration config) {
bundleA = config.remoteBundle;
bundleB = config.localBundle;
// TODO: syncID.
}
}

View File

@ -38,10 +38,12 @@
package org.mozilla.gecko.sync.synchronizer;
import org.mozilla.gecko.sync.ThreadPool;
import java.util.concurrent.ExecutorService;
import org.mozilla.gecko.sync.repositories.RepositorySession;
import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
import org.mozilla.gecko.sync.repositories.delegates.DeferrableRepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionFinishDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
import android.content.Context;
@ -116,14 +118,23 @@ implements RecordsChannelDelegate,
* Please don't call this until you've been notified with onInitialized.
*/
public void synchronize() {
// TODO: pull timestamps from somewhere...
// First thing: decide whether we should.
if (!sessionA.dataAvailable() &&
!sessionB.dataAvailable()) {
info("Neither session reports data available. Short-circuiting sync.");
sessionA.abort();
sessionB.abort();
this.delegate.onSynchronizeSkipped(this);
return;
}
final SynchronizerSession session = this;
// TODO: failed record handling.
final RecordsChannel channelBToA = new RecordsChannel(this.sessionB, this.sessionA, this);
RecordsChannelDelegate channelDelegate = new RecordsChannelDelegate() {
public void onFlowCompleted(RecordsChannel recordsChannel, long end) {
info("First RecordsChannel flow completed. Starting next.");
info("First RecordsChannel flow completed. End is " + end + ". Starting next.");
pendingATimestamp = end;
flowAToBCompleted = true;
channelBToA.flow();
@ -155,7 +166,7 @@ implements RecordsChannelDelegate,
@Override
public void onFlowCompleted(RecordsChannel channel, long end) {
info("Second RecordsChannel (" + channel + ") flow completed. Notifying onSynchronized.");
info("Second RecordsChannel flow completed. End is " + end + ". Finishing.");
pendingBTimestamp = end;
flowBToACompleted = true;
@ -171,7 +182,6 @@ implements RecordsChannelDelegate,
@Override
public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex) {
// TODO Auto-generated method stub
warn("Second RecordsChannel flow failed.");
}
@ -256,7 +266,8 @@ implements RecordsChannelDelegate,
warn("Got exception cleaning up first after second session creation failed.", ex);
return;
}
// TODO
String session = (this.sessionA == null) ? "B" : "A";
this.delegate.onSynchronizeFailed(this, ex, "Finish of session " + session + " failed.");
}
@Override
@ -272,6 +283,7 @@ implements RecordsChannelDelegate,
this.synchronizer.bundleA = bundle;
}
if (this.sessionB != null) {
info("Finishing session B.");
// On to the next.
this.sessionB.finish(this);
}
@ -280,6 +292,7 @@ implements RecordsChannelDelegate,
info("onFinishSucceeded: bumping session B's timestamp to " + pendingBTimestamp);
bundle.bumpTimestamp(pendingBTimestamp);
this.synchronizer.bundleB = bundle;
info("Notifying delegate.onSynchronized.");
this.delegate.onSynchronized(this);
}
} else {
@ -292,34 +305,7 @@ implements RecordsChannelDelegate,
}
@Override
public RepositorySessionFinishDelegate deferredFinishDelegate() {
final SynchronizerSession self = this;
return new RepositorySessionFinishDelegate() {
@Override
public void onFinishSucceeded(final RepositorySession session,
final RepositorySessionBundle bundle) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.onFinishSucceeded(session, bundle);
}
});
}
@Override
public void onFinishFailed(final Exception ex) {
ThreadPool.run(new Runnable() {
@Override
public void run() {
self.onFinishFailed(ex);
}
});
}
@Override
public RepositorySessionFinishDelegate deferredFinishDelegate() {
return this;
}
};
public RepositorySessionFinishDelegate deferredFinishDelegate(final ExecutorService executor) {
return new DeferredRepositorySessionFinishDelegate(this, executor);
}
}

View File

@ -43,9 +43,11 @@ public interface SynchronizerSessionDelegate {
public void onSynchronized(SynchronizerSession session);
public void onSynchronizeFailed(SynchronizerSession session, Exception lastException, String reason);
public void onSynchronizeAborted(SynchronizerSession synchronizerSession);
public void onSynchronizeSkipped(SynchronizerSession synchronizerSession);
// TODO: return value?
public void onFetchError(Exception e);
public void onStoreError(Exception e);
public void onSessionError(Exception e);
}

View File

@ -2,5 +2,7 @@ res/layout/sync_account.xml
res/layout/sync_setup.xml
res/layout/sync_setup_failure.xml
res/layout/sync_setup_jpake_waiting.xml
res/layout/sync_setup_nointernet.xml
res/layout/sync_setup_pair.xml
res/layout/sync_setup_success.xml
res/layout/sync_stub.xml

View File

@ -1 +1,3 @@
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

View File

@ -1,11 +1,28 @@
<activity
android:icon="@drawable/sync_ic_launcher"
android:label="@string/sync_app_name"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:configChanges="orientation"
android:windowSoftInputMode="adjustResize|stateHidden"
android:name="org.mozilla.gecko.sync.setup.activities.SetupSyncActivity" >
<!-- android:configChanges: SetupSyncActivity will handle orientation changes; no longer restarts activity (default) -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="sync" android:host="org.mozilla.android" android:path="/setup"/>
</intent-filter>
</activity>
<activity
android:clearTaskOnLaunch="true"
android:launchMode="singleTask"
android:name="org.mozilla.gecko.sync.setup.activities.AccountActivity"
android:windowSoftInputMode="adjustResize"/>
android:windowSoftInputMode="adjustPan|stateHidden"/>
<activity
android:name="org.mozilla.gecko.sync.setup.activities.SetupFailureActivity" />
<activity
android:name="org.mozilla.gecko.sync.setup.activities.SetupSuccessActivity" />
<activity
android:name="org.mozilla.gecko.sync.setup.activities.SetupWaitingActivity" />
android:name="org.mozilla.gecko.sync.setup.activities.SetupSuccessActivity"
android:launchMode="singleTask" />

View File

@ -1,4 +1,5 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.MANAGE_CREDENTIALS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />

View File

@ -37,6 +37,9 @@
<!-- Pair Device -->
<string name="sync_pair_tryagain">&sync.pair.tryagain.label;</string>
<!-- Firefox SyncAdapter Settings Screen -->
<string name="sync_settings_options">&sync.settings.options.label;</string>
<string name="sync_settings_summary_pair">&sync.summary.pair.label;</string>
<!-- Common text -->
<string name="sync_button_cancel">&sync.button.cancel.label;</string>
<string name="sync_button_connect">&sync.button.connect.label;</string>
@ -53,3 +56,6 @@
<string name="bookmarks_folder_unfiled">&bookmarks.folder.unfiled.label;</string>
<string name="bookmarks_folder_desktop">&bookmarks.folder.desktop.label;</string>
<string name="bookmarks_folder_mobile">&bookmarks.folder.mobile.label;</string>
<!-- Notification strings -->
<string name="sync_notification_oneaccount">&sync.notification.oneaccount.label;</string>