diff options
author | Ascrod <32915892+Ascrod@users.noreply.github.com> | 2019-04-18 20:35:10 -0400 |
---|---|---|
committer | Ascrod <32915892+Ascrod@users.noreply.github.com> | 2019-04-18 20:35:10 -0400 |
commit | af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec (patch) | |
tree | 4aac6c4383fb9e279fccb13c65a4e44595fd4cf6 /mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories | |
parent | 40fc72376411587e7bf9985fb9545eca1c9aaa8e (diff) | |
parent | 51722cd4fecb5c8c79a302f2771cad71535df5ea (diff) | |
download | UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.tar UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.tar.gz UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.tar.lz UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.tar.xz UXP-af7e140d4ed8f5bc9a69da2f0338ad3cb1319dec.zip |
Merge branch 'master' into default-pref
Diffstat (limited to 'mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories')
84 files changed, 0 insertions, 11419 deletions
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java deleted file mode 100644 index 5fe3dc9fa..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarkNeedsReparentingException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class BookmarkNeedsReparentingException extends SyncException { - - private static final long serialVersionUID = -7018336108709392800L; - - public BookmarkNeedsReparentingException(Exception ex) { - super(ex); - } - -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java deleted file mode 100644 index 289fc48ec..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/BookmarksRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -/** - * Shared interface for repositories that consume and produce - * bookmark records. - * - * @author rnewman - * - */ -public interface BookmarksRepository { - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java deleted file mode 100644 index a6dc3f6b8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ConstrainedServer11Repository.java +++ /dev/null @@ -1,51 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.net.URISyntaxException; - -import org.mozilla.gecko.sync.InfoCollections; -import org.mozilla.gecko.sync.InfoConfiguration; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; - -/** - * A kind of Server11Repository that supports explicit setting of total fetch limit, per-batch fetch limit, and a sort order. - * - * @author rnewman - * - */ -public class ConstrainedServer11Repository extends Server11Repository { - - private final String sort; - private final long batchLimit; - private final long totalLimit; - - public ConstrainedServer11Repository(String collection, String storageURL, - AuthHeaderProvider authHeaderProvider, - InfoCollections infoCollections, - InfoConfiguration infoConfiguration, - long batchLimit, long totalLimit, String sort) - throws URISyntaxException { - super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration); - this.batchLimit = batchLimit; - this.totalLimit = totalLimit; - this.sort = sort; - } - - @Override - public String getDefaultSort() { - return sort; - } - - @Override - public long getDefaultBatchLimit() { - return batchLimit; - } - - @Override - public long getDefaultTotalLimit() { - return totalLimit; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java deleted file mode 100644 index 8b29a37ba..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/FetchFailedException.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class FetchFailedException extends SyncException { - private static final long serialVersionUID = -7533105300182522946L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java deleted file mode 100644 index 3b6facc31..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HashSetStoreTracker.java +++ /dev/null @@ -1,61 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.util.HashSet; -import java.util.Iterator; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public class HashSetStoreTracker implements StoreTracker { - - // Guarded by `this`. - // Used to store GUIDs that were not locally modified but - // have been modified by a call to `store`, and thus - // should not be returned by a subsequent fetch. - private final HashSet<String> guids; - - public HashSetStoreTracker() { - guids = new HashSet<String>(); - } - - @Override - public String toString() { - return "#<Tracker: " + guids.size() + " guids tracked.>"; - } - - @Override - public synchronized boolean trackRecordForExclusion(String guid) { - return (guid != null) && guids.add(guid); - } - - @Override - public synchronized boolean isTrackedForExclusion(String guid) { - return (guid != null) && guids.contains(guid); - } - - @Override - public synchronized boolean untrackStoredForExclusion(String guid) { - return (guid != null) && guids.remove(guid); - } - - @Override - public RecordFilter getFilter() { - if (guids.size() == 0) { - return null; - } - return new RecordFilter() { - @Override - public boolean excludeRecord(Record r) { - return isTrackedForExclusion(r.guid); - } - }; - } - - @Override - public Iterator<String> recordsTrackedForExclusion() { - return this.guids.iterator(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java deleted file mode 100644 index eddc32102..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/HistoryRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -/** - * Shared interface for repositories that consume and produce - * history records. - * - * @author rnewman - * - */ -public interface HistoryRepository { - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java deleted file mode 100644 index acedc66e2..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/IdentityRecordFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public class IdentityRecordFactory extends RecordFactory { - - @Override - public Record createRecord(Record record) { - return record; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java deleted file mode 100644 index 185f0d724..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InactiveSessionException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class InactiveSessionException extends SyncException { - - private static final long serialVersionUID = 537241160815940991L; - - public InactiveSessionException(Exception ex) { - super(ex); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java deleted file mode 100644 index 3597276a4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidBookmarkTypeException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class InvalidBookmarkTypeException extends SyncException { - - private static final long serialVersionUID = -6098516814844387449L; - - public InvalidBookmarkTypeException(Exception e) { - super(e); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java deleted file mode 100644 index 3f761e540..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidRequestException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class InvalidRequestException extends SyncException { - - private static final long serialVersionUID = 4502951350743608243L; - - public InvalidRequestException(Exception ex) { - super(ex); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java deleted file mode 100644 index 0963892c9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/InvalidSessionTransitionException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class InvalidSessionTransitionException extends SyncException { - - private static final long serialVersionUID = 4157729859314427281L; - - public InvalidSessionTransitionException(Exception ex) { - super(ex); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java deleted file mode 100644 index 58cca4a49..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/MultipleRecordsForGuidException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class MultipleRecordsForGuidException extends SyncException { - - private static final long serialVersionUID = 7426987323485324741L; - - public MultipleRecordsForGuidException(Exception ex) { - super(ex); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java deleted file mode 100644 index 85d119a5d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoContentProviderException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -import android.net.Uri; - -/** - * Raised when a Content Provider cannot be retrieved. - * - * @author rnewman - * - */ -public class NoContentProviderException extends SyncException { - private static final long serialVersionUID = 1L; - - public final Uri requestedProvider; - public NoContentProviderException(Uri requested) { - super(); - this.requestedProvider = requested; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java deleted file mode 100644 index 3681deffd..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoGuidForIdException.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class NoGuidForIdException extends SyncException { - - private static final long serialVersionUID = -675614284405829041L; - - public NoGuidForIdException(Exception ex) { - super(ex); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java deleted file mode 100644 index 5747039aa..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NoStoreDelegateException.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class NoStoreDelegateException extends SyncException { - private static final long serialVersionUID = 6631689468978422074L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java deleted file mode 100644 index 4d9057992..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/NullCursorException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class NullCursorException extends SyncException { - - private static final long serialVersionUID = 3146506225701104661L; - - public NullCursorException(Exception e) { - super(e); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java deleted file mode 100644 index 991fd7426..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ParentNotFoundException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class ParentNotFoundException extends SyncException { - - private static final long serialVersionUID = -2687003621705922982L; - - public ParentNotFoundException(Exception ex) { - super(ex); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java deleted file mode 100644 index 0f8075133..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/ProfileDatabaseException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class ProfileDatabaseException extends SyncException { - - private static final long serialVersionUID = -4916908502042261602L; - - public ProfileDatabaseException(Exception ex) { - super(ex); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java deleted file mode 100644 index 6a8d81a77..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFactory.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -// Take a record retrieved from some middleware, producing -// some concrete record type for application to some local repository. -public abstract class RecordFactory { - public abstract Record createRecord(Record record); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java deleted file mode 100644 index 733448ded..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RecordFilter.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public interface RecordFilter { - public boolean excludeRecord(Record r); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java deleted file mode 100644 index 3dd3fd2c4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Repository.java +++ /dev/null @@ -1,18 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -public abstract class Repository { - public abstract void createSession(RepositorySessionCreationDelegate delegate, Context context); - - public void clean(boolean success, RepositorySessionCleanDelegate delegate, Context context) { - delegate.onCleaned(this); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java deleted file mode 100644 index 84fca1379..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySession.java +++ /dev/null @@ -1,384 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -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; - -/** - * A <code>RepositorySession</code> is created and used thusly: - * - *<ul> - * <li>Construct, with a reference to its parent {@link Repository}, by calling - * {@link Repository#createSession(RepositorySessionCreationDelegate, android.content.Context)}.</li> - * <li>Populate with saved information by calling {@link #unbundle(RepositorySessionBundle)}.</li> - * <li>Begin a sync by calling {@link #begin(RepositorySessionBeginDelegate)}. <code>begin()</code> - * is an appropriate place to initialize expensive resources.</li> - * <li>Perform operations such as {@link #fetchSince(long, RepositorySessionFetchRecordsDelegate)} and - * {@link #store(Record)}.</li> - * <li>Finish by calling {@link #finish(RepositorySessionFinishDelegate)}, retrieving and storing - * the current bundle.</li> - *</ul> - * - * If <code>finish()</code> is not called, {@link #abort()} must be called. These calls must - * <em>always</em> be paired with <code>begin()</code>. - * - */ -public abstract class RepositorySession { - - public enum SessionStatus { - UNSTARTED, - ACTIVE, - ABORTED, - DONE - } - - private static final String LOG_TAG = "RepositorySession"; - - protected static void trace(String message) { - Logger.trace(LOG_TAG, message); - } - - private 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. - private long lastSyncTimestamp = 0; - - public long getLastSyncTimestamp() { - return lastSyncTimestamp; - } - - public static long now() { - return System.currentTimeMillis(); - } - - public RepositorySession(Repository repository) { - this.repository = repository; - } - - public abstract void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate); - public abstract void fetchSince(long timestamp, RepositorySessionFetchRecordsDelegate delegate); - public abstract void fetch(String[] guids, RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException; - public abstract void fetchAll(RepositorySessionFetchRecordsDelegate 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; - } - - /** - * @return true if we cannot safely sync from this <code>RepositorySession</code>. - */ - public boolean shouldSkip() { - return false; - } - - /* - * 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) { - Logger.debug(LOG_TAG, "Setting store delegate to " + delegate); - this.delegate = delegate; - } - public abstract void store(Record record) throws NoStoreDelegateException; - - public void storeDone() { - // Our default behavior will be to assume that the Runnable is - // executed as soon as all the stores synchronously finish, so - // our end timestamp can just be… now. - storeDone(now()); - } - - public void storeDone(final long end) { - Logger.debug(LOG_TAG, "Scheduling onStoreCompleted for after storing is done: " + end); - Runnable command = new Runnable() { - @Override - public void run() { - delegate.onStoreCompleted(end); - } - }; - storeWorkQueue.execute(command); - } - - public abstract void wipe(RepositorySessionWipeDelegate delegate); - - /** - * Synchronously perform the shared work of beginning. Throws on failure. - * @throws InvalidSessionTransitionException - * - */ - protected void sharedBegin() throws InvalidSessionTransitionException { - Logger.debug(LOG_TAG, "Shared begin."); - if (delegateQueue.isShutdown()) { - throw new InvalidSessionTransitionException(null); - } - if (storeWorkQueue.isShutdown()) { - throw new InvalidSessionTransitionException(null); - } - this.transitionFrom(SessionStatus.UNSTARTED, SessionStatus.ACTIVE); - } - - /** - * Start the session. This is an appropriate place to initialize - * data access components such as database handles. - * - * @param delegate - * @throws InvalidSessionTransitionException - */ - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - sharedBegin(); - delegate.deferredBeginDelegate(delegateQueue).onBeginSucceeded(this); - } - - public void unbundle(RepositorySessionBundle bundle) { - this.lastSyncTimestamp = bundle == null ? 0 : bundle.getTimestamp(); - } - - /** - * Override this in your subclasses to return values to save between sessions. - * Note that RepositorySession automatically bumps the timestamp to the time - * the last sync began. If unbundled but not begun, this will be the same as the - * value in the input bundle. - * - * The Synchronizer most likely wants to bump the bundle timestamp to be a value - * return from a fetch call. - */ - protected RepositorySessionBundle getBundle() { - // Why don't we just persist the old bundle? - long timestamp = getLastSyncTimestamp(); - RepositorySessionBundle bundle = new RepositorySessionBundle(timestamp); - Logger.debug(LOG_TAG, "Setting bundle timestamp to " + timestamp + "."); - - return bundle; - } - - /** - * Just like finish(), but doesn't do any work that should only be performed - * at the end of a successful sync, and can be called any time. - */ - public void abort(RepositorySessionFinishDelegate delegate) { - this.abort(); - delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle()); - } - - /** - * Abnormally terminate the repository session, freeing or closing - * any resources that were opened during the lifetime of the session. - */ - public void abort() { - // TODO: do something here. - this.setStatus(SessionStatus.ABORTED); - try { - storeWorkQueue.shutdownNow(); - } catch (Exception e) { - Logger.error(LOG_TAG, "Caught exception shutting down store work queue.", e); - } - try { - delegateQueue.shutdown(); - } catch (Exception e) { - Logger.error(LOG_TAG, "Caught exception shutting down delegate queue.", e); - } - } - - /** - * End the repository session, freeing or closing any resources - * that were opened during the lifetime of the session. - * - * @param delegate notified of success or failure. - * @throws InactiveSessionException - */ - public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - try { - this.transitionFrom(SessionStatus.ACTIVE, SessionStatus.DONE); - delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle()); - } catch (InvalidSessionTransitionException e) { - Logger.error(LOG_TAG, "Tried to finish() an unstarted or already finished session"); - throw new InactiveSessionException(e); - } - - Logger.trace(LOG_TAG, "Shutting down work queues."); - storeWorkQueue.shutdown(); - delegateQueue.shutdown(); - } - - /** - * Run the provided command if we're active and our delegate queue - * is not shut down. - */ - protected synchronized void executeDelegateCommand(Runnable command) - throws InactiveSessionException { - if (!isActive() || delegateQueue.isShutdown()) { - throw new InactiveSessionException(null); - } - delegateQueue.execute(command); - } - - public synchronized void ensureActive() throws InactiveSessionException { - if (!isActive()) { - throw new InactiveSessionException(null); - } - } - - public synchronized boolean isActive() { - return status == SessionStatus.ACTIVE; - } - - public synchronized SessionStatus getStatus() { - return status; - } - - public synchronized void setStatus(SessionStatus status) { - this.status = status; - } - - public synchronized void transitionFrom(SessionStatus from, SessionStatus to) throws InvalidSessionTransitionException { - if (from == null || this.status == from) { - Logger.trace(LOG_TAG, "Successfully transitioning from " + this.status + " to " + to); - - this.status = to; - return; - } - Logger.warn(LOG_TAG, "Wanted to transition from " + from + " but in state " + this.status); - throw new InvalidSessionTransitionException(null); - } - - /** - * Produce a record that is some combination of the remote and local records - * provided. - * - * The returned record must be produced without mutating either remoteRecord - * or localRecord. It is acceptable to return either remoteRecord or localRecord - * if no modifications are to be propagated. - * - * The returned record *should* have the local androidID and the remote GUID, - * and some optional merge of data from the two records. - * - * This method can be called with records that are identical, or differ in - * any regard. - * - * This method will not be called if: - * - * * either record is marked as deleted, or - * * there is no local mapping for a new remote record. - * - * Otherwise, it will be called precisely once. - * - * Side-effects (e.g., for transactional storage) can be hooked in here. - * - * @param remoteRecord - * The record retrieved from upstream, already adjusted for clock skew. - * @param localRecord - * The record retrieved from local storage. - * @param lastRemoteRetrieval - * The timestamp of the last retrieved set of remote records, adjusted for - * clock skew. - * @param lastLocalRetrieval - * The timestamp of the last retrieved set of local records. - * @return - * A Record instance to apply, or null to apply nothing. - */ - protected Record reconcileRecords(final Record remoteRecord, - final Record localRecord, - final long lastRemoteRetrieval, - final long lastLocalRetrieval) { - Logger.debug(LOG_TAG, "Reconciling remote " + remoteRecord.guid + " against local " + localRecord.guid); - - if (localRecord.equalPayloads(remoteRecord)) { - if (remoteRecord.lastModified > localRecord.lastModified) { - Logger.debug(LOG_TAG, "Records are equal. No record application needed."); - return null; - } - - // Local wins. - return null; - } - - // TODO: Decide what to do based on: - // * Which of the two records is modified; - // * Whether they are equal or congruent; - // * The modified times of each record (interpreted through the lens of clock skew); - // * ... - boolean localIsMoreRecent = localRecord.lastModified > remoteRecord.lastModified; - Logger.debug(LOG_TAG, "Local record is more recent? " + localIsMoreRecent); - Record donor = localIsMoreRecent ? localRecord : remoteRecord; - - // Modify the local record to match the remote record's GUID and values. - // Preserve the local Android ID, and merge data where possible. - // It sure would be nice if copyWithIDs didn't give a shit about androidID, mm? - Record out = donor.copyWithIDs(remoteRecord.guid, localRecord.androidID); - - // We don't want to upload the record if the remote record was - // applied without changes. - // This logic will become more complicated as reconciling becomes smarter. - if (!localIsMoreRecent) { - trackGUID(out.guid); - } - return out; - } - - /** - * Depending on the RepositorySession implementation, track - * that a record — most likely a brand-new record that has been - * applied unmodified — should be tracked so as to not be uploaded - * redundantly. - * - * The default implementations do nothing. - */ - protected void trackGUID(String guid) { - } - - protected synchronized void untrackGUIDs(Collection<String> guids) { - } - - protected void untrackGUID(String guid) { - } - - // Ah, Java. You wretched creature. - public Iterator<String> getTrackedRecordIDs() { - return new ArrayList<String>().iterator(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java deleted file mode 100644 index 7908ec797..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java +++ /dev/null @@ -1,55 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; - -import java.io.IOException; - -public class RepositorySessionBundle { - public static final String LOG_TAG = RepositorySessionBundle.class.getSimpleName(); - - protected static final String JSON_KEY_TIMESTAMP = "timestamp"; - - protected final ExtendedJSONObject object; - - public RepositorySessionBundle(String jsonString) throws IOException, NonObjectJSONException { - - object = new ExtendedJSONObject(jsonString); - } - - public RepositorySessionBundle(long lastSyncTimestamp) { - object = new ExtendedJSONObject(); - this.setTimestamp(lastSyncTimestamp); - } - - public long getTimestamp() { - if (object.containsKey(JSON_KEY_TIMESTAMP)) { - return object.getLong(JSON_KEY_TIMESTAMP); - } - - return -1; - } - - public void setTimestamp(long timestamp) { - Logger.debug(LOG_TAG, "Setting timestamp to " + timestamp + "."); - object.put(JSON_KEY_TIMESTAMP, timestamp); - } - - public void bumpTimestamp(long timestamp) { - long existing = this.getTimestamp(); - if (timestamp > existing) { - this.setTimestamp(timestamp); - } else { - Logger.debug(LOG_TAG, "Timestamp " + timestamp + " not greater than " + existing + "; not bumping."); - } - } - - public String toJSONString() { - return object.toJSONString(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java deleted file mode 100644 index 4404fda25..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11Repository.java +++ /dev/null @@ -1,144 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; - -import org.mozilla.gecko.sync.InfoCollections; -import org.mozilla.gecko.sync.InfoConfiguration; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -/** - * A Server11Repository implements fetching and storing against the Sync 1.1 API. - * It doesn't do crypto: that's the job of the middleware. - * - * @author rnewman - */ -public class Server11Repository extends Repository { - protected String collection; - protected URI collectionURI; - protected final AuthHeaderProvider authHeaderProvider; - protected final InfoCollections infoCollections; - - private final InfoConfiguration infoConfiguration; - - /** - * Construct a new repository that fetches and stores against the Sync 1.1. API. - * - * @param collection name. - * @param storageURL full URL to storage endpoint. - * @param authHeaderProvider to use in requests; may be null. - * @param infoCollections instance; must not be null. - * @throws URISyntaxException - */ - public Server11Repository(@NonNull String collection, @NonNull String storageURL, AuthHeaderProvider authHeaderProvider, @NonNull InfoCollections infoCollections, @NonNull InfoConfiguration infoConfiguration) throws URISyntaxException { - if (collection == null) { - throw new IllegalArgumentException("collection must not be null"); - } - if (storageURL == null) { - throw new IllegalArgumentException("storageURL must not be null"); - } - if (infoCollections == null) { - throw new IllegalArgumentException("infoCollections must not be null"); - } - this.collection = collection; - this.collectionURI = new URI(storageURL + (storageURL.endsWith("/") ? collection : "/" + collection)); - this.authHeaderProvider = authHeaderProvider; - this.infoCollections = infoCollections; - this.infoConfiguration = infoConfiguration; - } - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - delegate.onSessionCreated(new Server11RepositorySession(this)); - } - - public URI collectionURI() { - return this.collectionURI; - } - - public URI collectionURI(boolean full, long newer, long limit, String sort, String ids, String offset) throws URISyntaxException { - ArrayList<String> params = new ArrayList<String>(); - if (full) { - params.add("full=1"); - } - if (newer >= 0) { - // Translate local millisecond timestamps into server decimal seconds. - String newerString = Utils.millisecondsToDecimalSecondsString(newer); - params.add("newer=" + newerString); - } - if (limit > 0) { - params.add("limit=" + limit); - } - if (sort != null) { - params.add("sort=" + sort); // We trust these values. - } - if (ids != null) { - params.add("ids=" + ids); // We trust these values. - } - if (offset != null) { - // Offset comes straight out of HTTP headers and it is the responsibility of the caller to URI-escape it. - params.add("offset=" + offset); - } - if (params.size() == 0) { - return this.collectionURI; - } - - StringBuilder out = new StringBuilder(); - char indicator = '?'; - for (String param : params) { - out.append(indicator); - indicator = '&'; - out.append(param); - } - String uri = this.collectionURI + out.toString(); - return new URI(uri); - } - - public URI wboURI(String id) throws URISyntaxException { - return new URI(this.collectionURI + "/" + id); - } - - // Override these. - @SuppressWarnings("static-method") - public long getDefaultBatchLimit() { - return -1; - } - - @SuppressWarnings("static-method") - public String getDefaultSort() { - return null; - } - - public long getDefaultTotalLimit() { - return -1; - } - - public AuthHeaderProvider getAuthHeaderProvider() { - return authHeaderProvider; - } - - public boolean updateNeeded(long lastSyncTimestamp) { - return infoCollections.updateNeeded(collection, lastSyncTimestamp); - } - - @Nullable - public Long getCollectionLastModified() { - return infoCollections.getTimestamp(collection); - } - - public InfoConfiguration getInfoConfiguration() { - return infoConfiguration; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java deleted file mode 100644 index 20c735a6b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/Server11RepositorySession.java +++ /dev/null @@ -1,104 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.background.common.log.Logger; -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 org.mozilla.gecko.sync.repositories.downloaders.BatchingDownloader; -import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader; - -public class Server11RepositorySession extends RepositorySession { - public static final String LOG_TAG = "Server11Session"; - - Server11Repository serverRepository; - private BatchingUploader uploader; - private final BatchingDownloader downloader; - - public Server11RepositorySession(Repository repository) { - super(repository); - serverRepository = (Server11Repository) repository; - this.downloader = new BatchingDownloader(serverRepository, this); - } - - public Server11Repository getServerRepository() { - return serverRepository; - } - - @Override - public void setStoreDelegate(RepositorySessionStoreDelegate delegate) { - this.delegate = delegate; - - // Now that we have the delegate, we can initialize our uploader. - this.uploader = new BatchingUploader(this, storeWorkQueue, delegate); - } - - @Override - public void guidsSince(long timestamp, - RepositorySessionGuidsSinceDelegate delegate) { - // TODO Auto-generated method stub - - } - - @Override - public void fetchSince(long timestamp, - RepositorySessionFetchRecordsDelegate delegate) { - this.downloader.fetchSince(timestamp, delegate); - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - this.fetchSince(-1, delegate); - } - - @Override - public void fetch(String[] guids, - RepositorySessionFetchRecordsDelegate delegate) { - this.downloader.fetch(guids, delegate); - } - - @Override - public void wipe(RepositorySessionWipeDelegate delegate) { - if (!isActive()) { - delegate.onWipeFailed(new InactiveSessionException(null)); - return; - } - // TODO: implement wipe. - } - - @Override - public void store(Record record) throws NoStoreDelegateException { - if (delegate == null) { - throw new NoStoreDelegateException(); - } - - // If delegate was set, this shouldn't happen. - if (uploader == null) { - throw new IllegalStateException("Uploader haven't been initialized"); - } - - uploader.process(record); - } - - @Override - public void storeDone() { - Logger.debug(LOG_TAG, "storeDone()."); - - // If delegate was set, this shouldn't happen. - if (uploader == null) { - throw new IllegalStateException("Uploader haven't been initialized"); - } - - uploader.noMoreRecordsToUpload(); - } - - @Override - public boolean dataAvailable() { - return serverRepository.updateNeeded(getLastSyncTimestamp()); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java deleted file mode 100644 index fcb09e32e..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreFailedException.java +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import org.mozilla.gecko.sync.SyncException; - -public class StoreFailedException extends SyncException { - private static final long serialVersionUID = 6080340122855859752L; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java deleted file mode 100644 index b6a3071a9..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTracker.java +++ /dev/null @@ -1,82 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.util.Iterator; - -/** - * Our hacky version of transactional semantics. The goal is to prevent - * the following situation: - * - * * AAA is not modified locally. - * * A modified AAA is downloaded during the storing phase. Its local - * timestamp is advanced. - * * The direction of syncing changes, and AAA is now uploaded to the server. - * - * The following situation should still be supported: - * - * * AAA is not modified locally. - * * A modified AAA is downloaded and merged with the local AAA. - * * The merged AAA is uploaded to the server. - * - * As should: - * - * * AAA is modified locally. - * * A modified AAA is downloaded, and discarded or merged. - * * The current version of AAA is uploaded to the server. - * - * We achieve this by tracking GUIDs during the storing phase. If we - * apply a record such that the local copy is substantially the same - * as the record we just downloaded, we add it to a list of records - * to avoid uploading. The definition of "substantially the same" - * depends on the particular repository. The only consideration is "do we - * want to upload this record in this sync?". - * - * Note that items are removed from this list when a fetch that - * considers them for upload completes successfully. The entire list - * is discarded when the session is completed. - * - * This interface exposes methods to: - * - * * During a store, recording that a record has been stored, and should - * thus not be returned in subsequent fetches; - * * During a fetch, checking whether a record should be returned. - * - * In the future this might also grow self-persistence. - * - * See also RepositorySession.trackRecord. - * - * @author rnewman - * - */ -public interface StoreTracker { - - /** - * @param guid - * The GUID of the item to track. - * @return - * Whether the GUID was a newly tracked value. - */ - public boolean trackRecordForExclusion(String guid); - - /** - * @param guid - * The GUID of the item to check. - * @return - * true if the item is already tracked. - */ - public boolean isTrackedForExclusion(String guid); - - /** - * - * @param guid - * @return true if the specified GUID was removed from the tracked set. - */ - public boolean untrackStoredForExclusion(String guid); - - public RecordFilter getFilter(); - - public Iterator<String> recordsTrackedForExclusion(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java deleted file mode 100644 index 1a5c1e96a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/StoreTrackingRepositorySession.java +++ /dev/null @@ -1,102 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories; - -import java.util.Collection; -import java.util.Iterator; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -public abstract class StoreTrackingRepositorySession extends RepositorySession { - private static final String LOG_TAG = "StoreTrackSession"; - protected StoreTracker storeTracker; - - protected static StoreTracker createStoreTracker() { - return new HashSetStoreTracker(); - } - - public StoreTrackingRepositorySession(Repository repository) { - super(repository); - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue); - try { - super.sharedBegin(); - } catch (InvalidSessionTransitionException e) { - deferredDelegate.onBeginFailed(e); - return; - } - // Or do this in your own subclass. - storeTracker = createStoreTracker(); - deferredDelegate.onBeginSucceeded(this); - } - - @Override - protected synchronized void trackGUID(String guid) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - this.storeTracker.trackRecordForExclusion(guid); - } - - @Override - protected synchronized void untrackGUID(String guid) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - this.storeTracker.untrackStoredForExclusion(guid); - } - - @Override - protected synchronized void untrackGUIDs(Collection<String> guids) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - if (guids == null) { - return; - } - for (String guid : guids) { - this.storeTracker.untrackStoredForExclusion(guid); - } - } - - protected void trackRecord(Record record) { - - Logger.debug(LOG_TAG, "Tracking record " + record.guid + - " (" + record.lastModified + ") to avoid re-upload."); - // Future: we care about the timestamp… - trackGUID(record.guid); - } - - protected void untrackRecord(Record record) { - Logger.debug(LOG_TAG, "Un-tracking record " + record.guid + "."); - untrackGUID(record.guid); - } - - @Override - public Iterator<String> getTrackedRecordIDs() { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - return this.storeTracker.recordsTrackedForExclusion(); - } - - @Override - public void abort(RepositorySessionFinishDelegate delegate) { - this.storeTracker = null; - super.abort(delegate); - } - - @Override - public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - super.finish(delegate); - this.storeTracker = null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java deleted file mode 100644 index fd3c35da0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java +++ /dev/null @@ -1,326 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -public class AndroidBrowserBookmarksDataAccessor extends AndroidBrowserRepositoryDataAccessor { - - private static final String LOG_TAG = "BookmarksDataAccessor"; - - /* - * Fragments of SQL to make our lives easier. - */ - private static final String BOOKMARK_IS_FOLDER = BrowserContract.Bookmarks.TYPE + " = " + - BrowserContract.Bookmarks.TYPE_FOLDER; - - // SQL fragment to retrieve GUIDs whose ID mappings should be tracked by this session. - // Exclude folders we don't want to sync. - private static final String GUID_SHOULD_TRACK = BrowserContract.SyncColumns.GUID + " NOT IN ('" + - BrowserContract.Bookmarks.TAGS_FOLDER_GUID + "', '" + - BrowserContract.Bookmarks.PLACES_FOLDER_GUID + "', '" + - BrowserContract.Bookmarks.PINNED_FOLDER_GUID + "')"; - - private static final String EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE; - static { - if (AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS.length > 0) { - StringBuilder b = new StringBuilder(BrowserContract.SyncColumns.GUID + " NOT IN ("); - - int remaining = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS.length - 1; - for (String specialGuid : AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS) { - b.append('"'); - b.append(specialGuid); - b.append('"'); - if (remaining-- > 0) { - b.append(", "); - } - } - b.append(')'); - EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE = b.toString(); - } else { - EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE = null; // null is a valid WHERE clause. - } - } - - 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 - protected Uri getUri() { - return BrowserContractHelpers.BOOKMARKS_CONTENT_URI; - } - - protected static Uri getPositionsUri() { - return BrowserContractHelpers.BOOKMARKS_POSITIONS_CONTENT_URI; - } - - @Override - public void wipe() { - Uri uri = getUri(); - Logger.info(LOG_TAG, "wiping (except for special guids): " + uri); - context.getContentResolver().delete(uri, EXCLUDE_SPECIAL_GUIDS_WHERE_CLAUSE, null); - } - - private final String[] GUID_AND_ID = new String[] { BrowserContract.Bookmarks.GUID, - BrowserContract.Bookmarks._ID }; - - protected Cursor getGuidsIDsForFolders() throws NullCursorException { - // Exclude items that we don't want to sync (pinned items, reading list, - // tags, the places root), in case they've ended up in the DB. - String where = BOOKMARK_IS_FOLDER + " AND " + GUID_SHOULD_TRACK; - return queryHelper.safeQuery(".getGuidsIDsForFolders", GUID_AND_ID, where, null, null); - } - - /** - * Issue a request to the Content Provider to update the positions of the - * records named by the provided GUIDs to the index of their GUID in the - * provided array. - * - * @param childArray - * A sequence of GUID strings. - */ - public int updatePositions(ArrayList<String> childArray) { - final int size = childArray.size(); - if (size == 0) { - return 0; - } - - Logger.debug(LOG_TAG, "Updating positions for " + size + " items."); - String[] args = childArray.toArray(new String[size]); - return context.getContentResolver().update(getPositionsUri(), new ContentValues(), null, args); - } - - public int bumpModifiedByGUID(Collection<String> ids, long modified) { - final int size = ids.size(); - if (size == 0) { - return 0; - } - - Logger.debug(LOG_TAG, "Bumping modified for " + size + " items to " + modified); - String where = RepoUtils.computeSQLInClause(size, BrowserContract.Bookmarks.GUID); - String[] selectionArgs = ids.toArray(new String[size]); - ContentValues values = new ContentValues(); - values.put(BrowserContract.Bookmarks.DATE_MODIFIED, modified); - - return context.getContentResolver().update(getUri(), values, where, selectionArgs); - } - - /** - * Bump the modified time of a record by ID. - */ - public int bumpModified(long id, long modified) { - Logger.debug(LOG_TAG, "Bumping modified for " + id + " to " + modified); - String where = BrowserContract.Bookmarks._ID + " = ?"; - String[] selectionArgs = new String[] { String.valueOf(id) }; - ContentValues values = new ContentValues(); - values.put(BrowserContract.Bookmarks.DATE_MODIFIED, modified); - - return context.getContentResolver().update(getUri(), values, where, selectionArgs); - } - - protected void updateParentAndPosition(String guid, long newParentId, long position) { - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.Bookmarks.PARENT, newParentId); - if (position >= 0) { - cv.put(BrowserContract.Bookmarks.POSITION, position); - } - updateByGuid(guid, cv); - } - - protected Map<String, Long> idsForGUIDs(String[] guids) throws NullCursorException { - final String where = RepoUtils.computeSQLInClause(guids.length, BrowserContract.Bookmarks.GUID); - Cursor c = queryHelper.safeQuery(".idsForGUIDs", GUID_AND_ID, where, guids, null); - try { - HashMap<String, Long> out = new HashMap<String, Long>(); - if (!c.moveToFirst()) { - return out; - } - final int guidIndex = c.getColumnIndexOrThrow(BrowserContract.Bookmarks.GUID); - final int idIndex = c.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID); - while (!c.isAfterLast()) { - out.put(c.getString(guidIndex), c.getLong(idIndex)); - c.moveToNext(); - } - return out; - } finally { - c.close(); - } - } - - /** - * Move the children of each source folder to the destination folder. - * Bump the modified time of each child. - * The caller should bump the modified time of the destination if desired. - * - * @param fromIDs the Android IDs of the source folders. - * @param to the Android ID of the destination folder. - * @return the number of updated rows. - */ - protected int moveChildren(String[] fromIDs, long to) { - long now = System.currentTimeMillis(); - long pos = -1; - - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.Bookmarks.PARENT, to); - cv.put(BrowserContract.Bookmarks.DATE_MODIFIED, now); - cv.put(BrowserContract.Bookmarks.POSITION, pos); - - final String where = RepoUtils.computeSQLInClause(fromIDs.length, BrowserContract.Bookmarks.PARENT); - return context.getContentResolver().update(getUri(), cv, where, fromIDs); - } - - /* - * 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 { - final String[] specialGUIDs = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS; - Cursor cur = fetch(specialGUIDs); - long placesRoot = 0; - - // Map from GUID to whether deleted. Non-presence implies just that. - HashMap<String, Boolean> statuses = new HashMap<String, Boolean>(specialGUIDs.length); - try { - if (cur.moveToFirst()) { - while (!cur.isAfterLast()) { - String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); - if ("places".equals(guid)) { - placesRoot = 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(); - } - } - } finally { - cur.close(); - } - - // Insert or undelete them if missing. - for (String guid : specialGUIDs) { - if (statuses.containsKey(guid)) { - if (statuses.get(guid)) { - // Undelete. - Logger.info(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("places")) { - // This is awkward. - Logger.info(LOG_TAG, "No places root. Inserting one."); - placesRoot = insertSpecialFolder("places", 0); - } else if (guid.equals("mobile")) { - Logger.info(LOG_TAG, "No mobile folder. Inserting one under the places root."); - insertSpecialFolder("mobile", placesRoot); - } else { - // unfiled, menu, toolbar. - Logger.info(LOG_TAG, "No " + guid + " root. Inserting one under places (" + placesRoot + ")."); - insertSpecialFolder(guid, placesRoot); - } - } - } - } - - private long insertSpecialFolder(String guid, long parentId) { - BookmarkRecord record = new BookmarkRecord(guid); - record.title = AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.get(guid); - record.type = "folder"; - record.androidParentID = parentId; - return ContentUris.parseId(insert(record)); - } - - @Override - protected ContentValues getContentValues(Record record) { - BookmarkRecord rec = (BookmarkRecord) record; - - if (rec.deleted) { - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.SyncColumns.GUID, rec.guid); - cv.put(BrowserContract.Bookmarks.IS_DELETED, 1); - return cv; - } - - final int recordType = BrowserContractHelpers.typeCodeForString(rec.type); - if (recordType == -1) { - throw new IllegalStateException("Unexpected record type " + rec.type); - } - - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.SyncColumns.GUID, rec.guid); - cv.put(BrowserContract.Bookmarks.TYPE, recordType); - cv.put(BrowserContract.Bookmarks.TITLE, rec.title); - cv.put(BrowserContract.Bookmarks.URL, rec.bookmarkURI); - 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); - - // Note that we don't set the modified timestamp: we allow the - // content provider to do that for us. - return cv; - } - - /** - * Returns a cursor over non-deleted records that list the given androidID as a parent. - */ - public Cursor getChildren(long androidID) throws NullCursorException { - return getChildren(androidID, false); - } - - /** - * Returns a cursor with any records that list the given androidID as a parent. - * Excludes 'places', and optionally any deleted records. - */ - public Cursor getChildren(long androidID, boolean includeDeleted) throws NullCursorException { - final String where = BrowserContract.Bookmarks.PARENT + " = ? AND " + - BrowserContract.SyncColumns.GUID + " <> ? " + - (!includeDeleted ? ("AND " + BrowserContract.SyncColumns.IS_DELETED + " = 0") : ""); - - final String[] args = new String[] { String.valueOf(androidID), "places" }; - - // Order by position, falling back on creation date and ID. - final String order = BrowserContract.Bookmarks.POSITION + ", " + - BrowserContract.SyncColumns.DATE_CREATED + ", " + - BrowserContract.Bookmarks._ID; - return queryHelper.safeQuery(".getChildren", getAllColumns(), where, args, order); - } - - - @Override - protected String[] getAllColumns() { - return BrowserContractHelpers.BookmarkColumns; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java deleted file mode 100644 index 38520fd7a..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import org.mozilla.gecko.sync.repositories.BookmarksRepository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -public class AndroidBrowserBookmarksRepository extends AndroidBrowserRepository implements BookmarksRepository { - - @Override - protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) { - AndroidBrowserBookmarksRepositorySession session = new AndroidBrowserBookmarksRepositorySession(AndroidBrowserBookmarksRepository.this, context); - final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate(); - deferredCreationDelegate.onSessionCreated(session); - } - - @Override - protected AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context) { - return new AndroidBrowserBookmarksDataAccessor(context); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java deleted file mode 100644 index fb79901a1..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java +++ /dev/null @@ -1,1107 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.R; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -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.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepositorySession - implements BookmarksInsertionManager.BookmarkInserter { - - public static final int DEFAULT_DELETION_FLUSH_THRESHOLD = 50; - public static final int DEFAULT_INSERTION_FLUSH_THRESHOLD = 50; - - // TODO: synchronization for these. - private final HashMap<String, Long> parentGuidToIDMap = new HashMap<String, Long>(); - private final HashMap<Long, String> parentIDToGuidMap = new HashMap<Long, String>(); - - /** - * Some notes on reparenting/reordering. - * - * Fennec stores new items with a high-negative position, because it doesn't care. - * On the other hand, it also doesn't give us any help managing positions. - * - * We can process records and folders in any order, though we'll usually see folders - * first because their sortindex is larger. - * - * We can also see folders that refer to children we haven't seen, and children we - * won't see (perhaps due to a TTL, perhaps due to a limit on our fetch). - * - * And of course folders can refer to local children (including ones that might - * be reconciled into oblivion!), or local children in other folders. And the local - * version of a folder -- which might be a reconciling target, or might not -- can - * have local additions or removals. (That causes complications with on-the-fly - * reordering: we don't know in advance which records will even exist by the end - * of the sync.) - * - * We opt to leave records in a reasonable state as we go, applying reordering/ - * reparenting operations whenever possible. A final sequence is applied after all - * incoming records have been handled. - * - * As such, we need to track a bunch of stuff as we go: - * - * • For each downloaded folder, the array of children. These will be server GUIDs, - * but not necessarily identical to the remote list: if we download a record and - * it's been locally moved, it must be removed from this child array. - * - * This mapping can be discarded when final reordering has occurred, either on - * store completion or when every child has been seen within this session. - * - * • A list of orphans: records whose parent folder does not yet exist. This can be - * trimmed as orphans are reparented. - * - * • Mappings from folder GUIDs to folder IDs, so that we can parent items without - * having to look in the DB. Of course, this must be kept up-to-date as we - * reconcile. - * - * Reordering also needs to occur during fetch. That is, a folder might have been - * created locally, or modified locally without any remote changes. An order must - * be generated for the folder's children array, and it must be persisted into the - * database to act as a starting point for future changes. But of course we don't - * want to incur a database write if the children already have a satisfactory order. - * - * Do we also need a list of "adopters", parents that are still waiting for children? - * As items get picked out of the orphans list, we can do on-the-fly ordering, until - * we're left with lonely records at the end. - * - * As we modify local folders, perhaps by moving children out of their purview, we - * must bump their modification time so as to cause them to be uploaded on the next - * stage of syncing. The same applies to simple reordering. - */ - - // TODO: can we guarantee serial access to these? - private final HashMap<String, ArrayList<String>> missingParentToChildren = new HashMap<String, ArrayList<String>>(); - private final HashMap<String, JSONArray> parentToChildArray = new HashMap<String, JSONArray>(); - private int needsReparenting = 0; - - private final AndroidBrowserBookmarksDataAccessor dataAccessor; - - protected BookmarksDeletionManager deletionManager; - protected BookmarksInsertionManager insertionManager; - - /** - * An array of known-special GUIDs. - */ - public static final String[] SPECIAL_GUIDS = new String[] { - // Mobile and desktop places roots have to come first. - "places", - "mobile", - "toolbar", - "menu", - "unfiled" - }; - - /** - * = 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. - * - * We need to consume records with these various GUIDs, producing a local - * representation which we are able to stably map upstream. - * - * Android Sync skips over the contents of some special GUIDs -- `places`, `tags`, - * etc. -- when finding IDs. - * Some of these special GUIDs are part of desktop structure (places, tags). Some - * are part of Fennec's custom data (readinglist, pinned). - * - * We don't want to upload or apply these records. - * - * That is: - * - * * We should not upload a `places`,`tags`, `readinglist`, or `pinned` record. - * * We can stably _store_ menu/toolbar/unfiled/mobile as special GUIDs, and set - * their parent ID as appropriate on upload. - * - * Fortunately, Fennec stores our representation of the data, not Places: that is, - * there's a "places" root, containing "mobile", "menu", "toolbar", etc. - * - * These are guaranteed to exist when the database is created. - * - * = 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 - * ---------- ---------- ---------- - * places 0 0 - * mobile 1 0 - * menu 2 0 - * etc. - * - */ - 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; - - /** - * Return true if the provided record GUID should be skipped - * in child lists or fetch results. - * - * @param recordGUID the GUID of the record to check. - * @return true if the record should be skipped. - */ - public static boolean forbiddenGUID(final String recordGUID) { - return recordGUID == null || - BrowserContract.Bookmarks.PINNED_FOLDER_GUID.equals(recordGUID) || - BrowserContract.Bookmarks.PLACES_FOLDER_GUID.equals(recordGUID) || - BrowserContract.Bookmarks.TAGS_FOLDER_GUID.equals(recordGUID); - } - - /** - * Return true if the provided parent GUID's children should - * be skipped in child lists or fetch results. - * This differs from {@link #forbiddenGUID(String)} in that we're skipping - * part of the hierarchy. - * - * @param parentGUID the GUID of parent of the record to check. - * @return true if the record should be skipped. - */ - public static boolean forbiddenParent(final String parentGUID) { - return parentGUID == null || - BrowserContract.Bookmarks.PINNED_FOLDER_GUID.equals(parentGUID); - } - - public AndroidBrowserBookmarksRepositorySession(Repository repository, Context context) { - super(repository); - - if (SPECIAL_GUIDS_MAP == null) { - HashMap<String, String> m = new HashMap<String, String>(); - - // Note that we always use the literal name "mobile" for the Mobile Bookmarks - // folder, regardless of its actual name in the database or the Fennec UI. - // This is to match desktop (working around Bug 747699) and to avoid a similar - // issue locally. See Bug 748898. - m.put("mobile", "mobile"); - - // Other folders use their contextualized names, and we simply rely on - // these not changing, matching desktop, and such to avoid issues. - 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)); - - SPECIAL_GUIDS_MAP = Collections.unmodifiableMap(m); - } - - dbHelper = new AndroidBrowserBookmarksDataAccessor(context); - dataAccessor = (AndroidBrowserBookmarksDataAccessor) dbHelper; - } - - private static int getTypeFromCursor(Cursor cur) { - return RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks.TYPE); - } - - private static boolean rowIsFolder(Cursor cur) { - return getTypeFromCursor(cur) == BrowserContract.Bookmarks.TYPE_FOLDER; - } - - private String getGUIDForID(long androidID) { - String guid = parentIDToGuidMap.get(androidID); - trace(" " + androidID + " => " + guid); - return guid; - } - - private long getIDForGUID(String guid) { - Long id = parentGuidToIDMap.get(guid); - if (id == null) { - Logger.warn(LOG_TAG, "Couldn't find local ID for GUID " + guid); - return -1; - } - return id; - } - - private String getGUID(Cursor cur) { - return RepoUtils.getStringFromCursor(cur, "guid"); - } - - private long getParentID(Cursor cur) { - return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.PARENT); - } - - // More efficient for bulk operations. - private long getPosition(Cursor cur, int positionIndex) { - return cur.getLong(positionIndex); - } - private long getPosition(Cursor cur) { - return RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION); - } - - private String getParentName(String parentGUID) throws ParentNotFoundException, NullCursorException { - if (parentGUID == null) { - return ""; - } - if (SPECIAL_GUIDS_MAP.containsKey(parentGUID)) { - return 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 { - Logger.error(LOG_TAG, "Couldn't find record with guid '" + parentGUID + "' when looking for parent name."); - throw new ParentNotFoundException(null); - } - } finally { - name.close(); - } - return parentName; - } - - /** - * Retrieve the child array for a record, repositioning and updating the database as necessary. - * - * @param folderID - * The database ID of the folder. - * @param persist - * True if generated positions should be written to the database. The modified - * time of the parent folder is only bumped if this is true. - * @param childArray - * A new, empty JSONArray which will be populated with an array of GUIDs. - * @return - * True if the resulting array is "clean" (i.e., reflects the content of the database). - * @throws NullCursorException - */ - @SuppressWarnings("unchecked") - private boolean getChildrenArray(long folderID, boolean persist, JSONArray childArray) throws NullCursorException { - trace("Calling getChildren for androidID " + folderID); - Cursor children = dataAccessor.getChildren(folderID); - try { - if (!children.moveToFirst()) { - trace("No children: empty cursor."); - return true; - } - final int positionIndex = children.getColumnIndex(BrowserContract.Bookmarks.POSITION); - final int count = children.getCount(); - Logger.debug(LOG_TAG, "Expecting " + count + " children."); - - // Sorted by requested position. - TreeMap<Long, ArrayList<String>> guids = new TreeMap<Long, ArrayList<String>>(); - - while (!children.isAfterLast()) { - final String childGuid = getGUID(children); - final long childPosition = getPosition(children, positionIndex); - trace(" Child GUID: " + childGuid); - trace(" Child position: " + childPosition); - Utils.addToIndexBucketMap(guids, Math.abs(childPosition), childGuid); - children.moveToNext(); - } - - // This will suffice for taking a jumble of records and indices and - // producing a sorted sequence that preserves some kind of order -- - // from the abs of the position, falling back on cursor order (that - // is, creation time and ID). - // Note that this code is not intended to merge values from two sources! - boolean changed = false; - int i = 0; - for (Entry<Long, ArrayList<String>> entry : guids.entrySet()) { - long pos = entry.getKey(); - int atPos = entry.getValue().size(); - - // If every element has a different index, and the indices are - // in strict natural order, then changed will be false. - if (atPos > 1 || pos != i) { - changed = true; - } - - ++i; - - for (String guid : entry.getValue()) { - if (!forbiddenGUID(guid)) { - childArray.add(guid); - } - } - } - - if (Logger.shouldLogVerbose(LOG_TAG)) { - // Don't JSON-encode unless we're logging. - Logger.trace(LOG_TAG, "Output child array: " + childArray.toJSONString()); - } - - if (!changed) { - Logger.debug(LOG_TAG, "Nothing moved! Database reflects child array."); - return true; - } - - if (!persist) { - Logger.debug(LOG_TAG, "Returned array does not match database, and not persisting."); - return false; - } - - Logger.debug(LOG_TAG, "Generating child array required moving records. Updating DB."); - final long time = now(); - if (0 < dataAccessor.updatePositions(childArray)) { - Logger.debug(LOG_TAG, "Bumping parent time to " + time + "."); - dataAccessor.bumpModified(folderID, time); - } - return true; - } finally { - children.close(); - } - } - - protected static boolean isDeleted(Cursor cur) { - return RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) != 0; - } - - @Override - protected Record retrieveDuringStore(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - // During storing of a retrieved record, we never care about the children - // array that's already present in the database -- we don't use it for - // reconciling. Skip all that effort for now. - return retrieveRecord(cur, false); - } - - @Override - protected Record retrieveDuringFetch(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - return retrieveRecord(cur, true); - } - - /** - * Build a record from a cursor, with a flag to dictate whether the - * children array should be computed and written back into the database. - */ - protected BookmarkRecord retrieveRecord(Cursor cur, boolean computeAndPersistChildren) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - String recordGUID = getGUID(cur); - Logger.trace(LOG_TAG, "Record from mirror cursor: " + recordGUID); - - if (forbiddenGUID(recordGUID)) { - Logger.debug(LOG_TAG, "Ignoring " + recordGUID + " record in recordFromMirrorCursor."); - return null; - } - - // Short-cut for deleted items. - if (isDeleted(cur)) { - return AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(cur, null, null, null); - } - - long androidParentID = getParentID(cur); - - // Ensure special folders stay in the right place. - String androidParentGUID = SPECIAL_GUID_PARENTS.get(recordGUID); - if (androidParentGUID == null) { - androidParentGUID = getGUIDForID(androidParentID); - } - - boolean needsReparenting = false; - - if (androidParentGUID == null) { - Logger.debug(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 (parentIDToGuidMap.containsKey(androidParentID)) { - Logger.error(LOG_TAG, "Have the parent android ID for the record but the parent's GUID wasn't found."); - throw new NoGuidForIdException(null); - } - - // We have a parent ID but it's wrong. If the record is deleted, - // we'll just say that it was in the Unsorted Bookmarks folder. - // If not, we'll move it into Mobile Bookmarks. - needsReparenting = true; - } - - // If record is a folder, and we want to see children at this time, then build out the children array. - final JSONArray childArray; - if (computeAndPersistChildren) { - childArray = getChildrenArrayForRecordCursor(cur, recordGUID, true); - } else { - childArray = null; - } - String parentName = getParentName(androidParentGUID); - BookmarkRecord bookmark = AndroidBrowserBookmarksRepositorySession.bookmarkFromMirrorCursor(cur, androidParentGUID, parentName, childArray); - - if (bookmark == null) { - Logger.warn(LOG_TAG, "Unable to extract bookmark from cursor. Record GUID " + recordGUID + - ", parent " + androidParentGUID + "/" + androidParentID); - return null; - } - - if (needsReparenting) { - Logger.warn(LOG_TAG, "Bookmark record " + recordGUID + " has a bad parent pointer. Reparenting now."); - - String destination = bookmark.deleted ? "unfiled" : "mobile"; - bookmark.androidParentID = getIDForGUID(destination); - bookmark.androidPosition = getPosition(cur); - bookmark.parentID = destination; - bookmark.parentName = getParentName(destination); - if (!bookmark.deleted) { - // Actually move it. - // TODO: compute position. Persist. - relocateBookmark(bookmark); - } - } - - return bookmark; - } - - /** - * Ensure that the local database row for the provided bookmark - * reflects this record's parent information. - * - * @param bookmark - */ - private void relocateBookmark(BookmarkRecord bookmark) { - dataAccessor.updateParentAndPosition(bookmark.guid, bookmark.androidParentID, bookmark.androidPosition); - } - - protected JSONArray getChildrenArrayForRecordCursor(Cursor cur, String recordGUID, boolean persist) throws NullCursorException { - boolean isFolder = rowIsFolder(cur); - if (!isFolder) { - return null; - } - - long androidID = parentGuidToIDMap.get(recordGUID); - JSONArray childArray = new JSONArray(); - getChildrenArray(androidID, persist, childArray); - - Logger.debug(LOG_TAG, "Fetched " + childArray.size() + " children for " + recordGUID); - return childArray; - } - - @Override - public boolean shouldIgnore(Record record) { - if (!(record instanceof BookmarkRecord)) { - return true; - } - if (record.deleted) { - return false; - } - - BookmarkRecord bmk = (BookmarkRecord) record; - - if (forbiddenGUID(bmk.guid)) { - Logger.debug(LOG_TAG, "Ignoring forbidden record with guid: " + bmk.guid); - return true; - } - - if (forbiddenParent(bmk.parentID)) { - Logger.debug(LOG_TAG, "Ignoring child " + bmk.guid + " of forbidden parent folder " + bmk.parentID); - return true; - } - - if (BrowserContractHelpers.isSupportedType(bmk.type)) { - return false; - } - - Logger.debug(LOG_TAG, "Ignoring record with guid: " + bmk.guid + " and type: " + bmk.type); - return true; - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - // Check for the existence of special folders - // and insert them if they don't exist. - Cursor cur; - try { - Logger.debug(LOG_TAG, "Check and build special GUIDs."); - dataAccessor.checkAndBuildSpecialGuids(); - cur = dataAccessor.getGuidsIDsForFolders(); - Logger.debug(LOG_TAG, "Got GUIDs for folders."); - } catch (android.database.sqlite.SQLiteConstraintException e) { - Logger.error(LOG_TAG, "Got sqlite constraint exception working with Fennec bookmark DB.", e); - delegate.onBeginFailed(e); - return; - } catch (Exception e) { - delegate.onBeginFailed(e); - return; - } - - // To deal with parent mapping of bookmarks we have to do some - // hairy stuff. Here's the setup for it. - - Logger.debug(LOG_TAG, "Preparing folder ID mappings."); - - // Fake our root. - Logger.debug(LOG_TAG, "Tracking places root as ID 0."); - parentIDToGuidMap.put(0L, "places"); - parentGuidToIDMap.put("places", 0L); - try { - cur.moveToFirst(); - while (!cur.isAfterLast()) { - String guid = getGUID(cur); - long id = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID); - parentGuidToIDMap.put(guid, id); - parentIDToGuidMap.put(id, guid); - Logger.debug(LOG_TAG, "GUID " + guid + " maps to " + id); - cur.moveToNext(); - } - } finally { - cur.close(); - } - deletionManager = new BookmarksDeletionManager(dataAccessor, DEFAULT_DELETION_FLUSH_THRESHOLD); - - // We just crawled the database enumerating all folders; we'll start the - // insertion manager with exactly these folders as the known parents (the - // collection is copied) in the manager constructor. - insertionManager = new BookmarksInsertionManager(DEFAULT_INSERTION_FLUSH_THRESHOLD, parentGuidToIDMap.keySet(), this); - - Logger.debug(LOG_TAG, "Done with initial setup of bookmarks session."); - super.begin(delegate); - } - - /** - * Implement method of BookmarksInsertionManager.BookmarkInserter. - */ - @Override - public boolean insertFolder(BookmarkRecord record) { - // A folder that is *not* deleted needs its androidID updated, so that - // updateBookkeeping can re-parent, etc. - Record toStore = prepareRecord(record); - try { - Uri recordURI = dbHelper.insert(toStore); - if (recordURI == null) { - delegate.onRecordStoreFailed(new RuntimeException("Got null URI inserting folder with guid " + toStore.guid + "."), record.guid); - return false; - } - toStore.androidID = ContentUris.parseId(recordURI); - Logger.debug(LOG_TAG, "Inserted folder with guid " + toStore.guid + " as androidID " + toStore.androidID); - - updateBookkeeping(toStore); - } catch (Exception e) { - delegate.onRecordStoreFailed(e, record.guid); - return false; - } - trackRecord(toStore); - delegate.onRecordStoreSucceeded(record.guid); - return true; - } - - /** - * Implement method of BookmarksInsertionManager.BookmarkInserter. - */ - @Override - public void bulkInsertNonFolders(Collection<BookmarkRecord> records) { - // All of these records are *not* deleted and *not* folders, so we don't - // need to update androidID at all! - // TODO: persist records that fail to insert for later retry. - ArrayList<Record> toStores = new ArrayList<Record>(records.size()); - for (Record record : records) { - toStores.add(prepareRecord(record)); - } - - try { - int stored = dataAccessor.bulkInsert(toStores); - if (stored != toStores.size()) { - // Something failed; most pessimistic action is to declare that all insertions failed. - // TODO: perform the bulkInsert in a transaction and rollback unless all insertions succeed? - for (Record failed : toStores) { - delegate.onRecordStoreFailed(new RuntimeException("Possibly failed to bulkInsert non-folder with guid " + failed.guid + "."), failed.guid); - } - return; - } - } catch (NullCursorException e) { - for (Record failed : toStores) { - delegate.onRecordStoreFailed(e, failed.guid); - } - return; - } - - // Success For All! - for (Record succeeded : toStores) { - try { - updateBookkeeping(succeeded); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception updating bookkeeping of non-folder with guid " + succeeded.guid + ".", e); - } - trackRecord(succeeded); - delegate.onRecordStoreSucceeded(succeeded.guid); - } - } - - @Override - public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - // Allow these to be GCed. - deletionManager = null; - insertionManager = null; - - // Override finish to do this check; make sure all records - // needing re-parenting have been re-parented. - if (needsReparenting != 0) { - Logger.error(LOG_TAG, "Finish called but " + needsReparenting + - " 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); - }; - - @Override - public void setStoreDelegate(RepositorySessionStoreDelegate delegate) { - super.setStoreDelegate(delegate); - - if (deletionManager != null) { - deletionManager.setDelegate(delegate); - } - } - - @Override - protected Record reconcileRecords(Record remoteRecord, Record localRecord, - long lastRemoteRetrieval, - long lastLocalRetrieval) { - - BookmarkRecord reconciled = (BookmarkRecord) super.reconcileRecords(remoteRecord, localRecord, - lastRemoteRetrieval, - lastLocalRetrieval); - - // For now we *always* use the remote record's children array as a starting point. - // We won't write it into the database yet; we'll record it and process as we go. - reconciled.children = ((BookmarkRecord) remoteRecord).children; - - // *Always* track folders, though: if we decide we need to reposition items, we'll - // untrack later. - if (reconciled.isFolder()) { - trackRecord(reconciled); - } - return reconciled; - } - - /** - * Rename mobile folders to "mobile", both in and out. The other half of - * this logic lives in {@link #computeParentFields(BookmarkRecord, String, String)}, where - * the parent name of a record is set from {@link #SPECIAL_GUIDS_MAP} rather than - * from source data. - * - * Apply this approach generally for symmetry. - */ - @Override - protected void fixupRecord(Record record) { - final BookmarkRecord r = (BookmarkRecord) record; - final String parentName = SPECIAL_GUIDS_MAP.get(r.parentID); - if (parentName == null) { - return; - } - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, "Replacing parent name \"" + r.parentName + "\" with \"" + parentName + "\"."); - } - r.parentName = parentName; - } - - @Override - protected Record prepareRecord(Record record) { - if (record.deleted) { - Logger.debug(LOG_TAG, "No need to prepare deleted record " + record.guid); - return record; - } - - BookmarkRecord bmk = (BookmarkRecord) record; - - if (!isSpecialRecord(record)) { - // We never want to reparent special records. - handleParenting(bmk); - } - - if (Logger.LOG_PERSONAL_INFORMATION) { - if (bmk.isFolder()) { - Logger.pii(LOG_TAG, "Inserting folder " + bmk.guid + ", " + bmk.title + - " with parent " + bmk.androidParentID + - " (" + bmk.parentID + ", " + bmk.parentName + - ", " + bmk.androidPosition + ")"); - } else { - Logger.pii(LOG_TAG, "Inserting bookmark " + bmk.guid + ", " + bmk.title + ", " + - bmk.bookmarkURI + " with parent " + bmk.androidParentID + - " (" + bmk.parentID + ", " + bmk.parentName + - ", " + bmk.androidPosition + ")"); - } - } else { - if (bmk.isFolder()) { - Logger.debug(LOG_TAG, "Inserting folder " + bmk.guid + ", parent " + - bmk.androidParentID + - " (" + bmk.parentID + ", " + bmk.androidPosition + ")"); - } else { - Logger.debug(LOG_TAG, "Inserting bookmark " + bmk.guid + " with parent " + - bmk.androidParentID + - " (" + bmk.parentID + ", " + ", " + bmk.androidPosition + ")"); - } - } - return bmk; - } - - /** - * If the provided record doesn't have correct parent information, - * update appropriate bookkeeping to improve the situation. - * - * @param bmk - */ - private void handleParenting(BookmarkRecord bmk) { - if (parentGuidToIDMap.containsKey(bmk.parentID)) { - bmk.androidParentID = parentGuidToIDMap.get(bmk.parentID); - - // Might as well set a basic position from the downloaded children array. - JSONArray children = parentToChildArray.get(bmk.parentID); - if (children != null) { - int index = children.indexOf(bmk.guid); - if (index >= 0) { - bmk.androidPosition = index; - } - } - } - else { - bmk.androidParentID = parentGuidToIDMap.get("unfiled"); - ArrayList<String> children; - if (missingParentToChildren.containsKey(bmk.parentID)) { - children = missingParentToChildren.get(bmk.parentID); - } else { - children = new ArrayList<String>(); - } - children.add(bmk.guid); - needsReparenting++; - missingParentToChildren.put(bmk.parentID, children); - } - } - - private boolean isSpecialRecord(Record record) { - return SPECIAL_GUID_PARENTS.containsKey(record.guid); - } - - @Override - protected void updateBookkeeping(Record record) throws NoGuidForIdException, - NullCursorException, - ParentNotFoundException { - super.updateBookkeeping(record); - BookmarkRecord bmk = (BookmarkRecord) record; - - // If record is folder, update maps and re-parent children if necessary. - if (!bmk.isFolder()) { - Logger.debug(LOG_TAG, "Not a folder. No bookkeeping."); - return; - } - - Logger.debug(LOG_TAG, "Updating bookkeeping for folder " + record.guid); - - // Mappings between ID and GUID. - // TODO: update our persisted children arrays! - // TODO: if our Android ID just changed, replace parents for all of our children. - parentGuidToIDMap.put(bmk.guid, bmk.androidID); - parentIDToGuidMap.put(bmk.androidID, bmk.guid); - - JSONArray childArray = bmk.children; - - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, bmk.guid + " has children " + childArray.toJSONString()); - } - parentToChildArray.put(bmk.guid, childArray); - - // Re-parent. - if (missingParentToChildren.containsKey(bmk.guid)) { - for (String child : missingParentToChildren.get(bmk.guid)) { - // This might return -1; that's OK, the bookmark will - // be properly repositioned later. - long position = childArray.indexOf(child); - dataAccessor.updateParentAndPosition(child, bmk.androidID, position); - needsReparenting--; - } - missingParentToChildren.remove(bmk.guid); - } - } - - @Override - protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - try { - insertionManager.enqueueRecord((BookmarkRecord) record); - } catch (Exception e) { - throw new NullCursorException(e); - } - } - - @Override - protected void storeRecordDeletion(final Record record, final Record existingRecord) { - if (SPECIAL_GUIDS_MAP.containsKey(record.guid)) { - Logger.debug(LOG_TAG, "Told to delete record " + record.guid + ". Ignoring."); - return; - } - final BookmarkRecord bookmarkRecord = (BookmarkRecord) record; - final BookmarkRecord existingBookmark = (BookmarkRecord) existingRecord; - final boolean isFolder = existingBookmark.isFolder(); - final String parentGUID = existingBookmark.parentID; - deletionManager.deleteRecord(bookmarkRecord.guid, isFolder, parentGUID); - } - - protected void flushQueues() { - long now = now(); - Logger.debug(LOG_TAG, "Applying remaining insertions."); - try { - insertionManager.finishUp(); - Logger.debug(LOG_TAG, "Done applying remaining insertions."); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Unable to apply remaining insertions.", e); - } - - Logger.debug(LOG_TAG, "Applying deletions."); - try { - untrackGUIDs(deletionManager.flushAll(getIDForGUID("unfiled"), now)); - Logger.debug(LOG_TAG, "Done applying deletions."); - } catch (Exception e) { - Logger.error(LOG_TAG, "Unable to apply deletions.", e); - } - } - - @SuppressWarnings("unchecked") - private void finishUp() { - try { - flushQueues(); - Logger.debug(LOG_TAG, "Have " + parentToChildArray.size() + " folders whose children might need repositioning."); - for (Entry<String, JSONArray> entry : parentToChildArray.entrySet()) { - String guid = entry.getKey(); - JSONArray onServer = entry.getValue(); - try { - final long folderID = getIDForGUID(guid); - final JSONArray inDB = new JSONArray(); - final boolean clean = getChildrenArray(folderID, false, inDB); - final boolean sameArrays = Utils.sameArrays(onServer, inDB); - - // If the local children and the remote children are already - // the same, then we don't need to bump the modified time of the - // parent: we wouldn't upload a different record, so avoid the cycle. - if (!sameArrays) { - int added = 0; - for (Object o : inDB) { - if (!onServer.contains(o)) { - onServer.add(o); - added++; - } - } - Logger.debug(LOG_TAG, "Added " + added + " items locally."); - Logger.debug(LOG_TAG, "Untracking and bumping " + guid + "(" + folderID + ")"); - dataAccessor.bumpModified(folderID, now()); - untrackGUID(guid); - } - - // If the arrays are different, or they're the same but not flushed to disk, - // write them out now. - if (!sameArrays || !clean) { - dataAccessor.updatePositions(new ArrayList<String>(onServer)); - } - } catch (Exception e) { - Logger.warn(LOG_TAG, "Error repositioning children for " + guid, e); - } - } - } finally { - super.storeDone(); - } - } - - /** - * Hook into the deletion manager on wipe. - */ - class BookmarkWipeRunnable extends WipeRunnable { - public BookmarkWipeRunnable(RepositorySessionWipeDelegate delegate) { - super(delegate); - } - - @Override - public void run() { - try { - // Clear our queued deletions. - deletionManager.clear(); - insertionManager.clear(); - super.run(); - } catch (Exception ex) { - delegate.onWipeFailed(ex); - return; - } - } - } - - @Override - protected WipeRunnable getWipeRunnable(RepositorySessionWipeDelegate delegate) { - return new BookmarkWipeRunnable(delegate); - } - - @Override - public void storeDone() { - Runnable command = new Runnable() { - @Override - public void run() { - finishUp(); - } - }; - storeWorkQueue.execute(command); - } - - @Override - protected String buildRecordString(Record record) { - BookmarkRecord bmk = (BookmarkRecord) record; - String parent = bmk.parentName + "/"; - if (bmk.isBookmark()) { - return "b" + parent + bmk.bookmarkURI + ":" + bmk.title; - } - if (bmk.isFolder()) { - return "f" + parent + bmk.title; - } - if (bmk.isSeparator()) { - return "s" + parent + bmk.androidPosition; - } - if (bmk.isQuery()) { - return "q" + parent + bmk.bookmarkURI; - } - return null; - } - - public static BookmarkRecord computeParentFields(BookmarkRecord rec, String suggestedParentGUID, String suggestedParentName) { - final String guid = rec.guid; - if (guid == null) { - // Oh dear. - Logger.error(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 = suggestedParentGUID; - } else { - Logger.debug(LOG_TAG, "Ignoring suggested parent ID " + suggestedParentGUID + - " for " + guid + "; using " + realParent); - } - - if (realParent == null) { - // Oh dear. - Logger.error(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; - } - - private static BookmarkRecord logBookmark(BookmarkRecord rec) { - try { - Logger.debug(LOG_TAG, "Returning " + (rec.deleted ? "deleted " : "") + - "bookmark record " + rec.guid + " (" + rec.androidID + - ", parent " + rec.parentID + ")"); - if (!rec.deleted && Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "> Parent name: " + rec.parentName); - Logger.pii(LOG_TAG, "> Title: " + rec.title); - Logger.pii(LOG_TAG, "> Type: " + rec.type); - Logger.pii(LOG_TAG, "> URI: " + rec.bookmarkURI); - Logger.pii(LOG_TAG, "> Position: " + rec.androidPosition); - if (rec.isFolder()) { - Logger.pii(LOG_TAG, "FOLDER: Children are " + - (rec.children == null ? - "null" : - rec.children.toJSONString())); - } - } - } catch (Exception e) { - Logger.debug(LOG_TAG, "Exception logging bookmark record " + rec, e); - } - return rec; - } - - // Create a BookmarkRecord object from a cursor on a row containing a Fennec bookmark. - public static BookmarkRecord bookmarkFromMirrorCursor(Cursor cur, String parentGUID, String parentName, JSONArray children) { - final String collection = "bookmarks"; - final String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); - final long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED); - final boolean deleted = isDeleted(cur); - BookmarkRecord rec = new BookmarkRecord(guid, collection, lastModified, deleted); - - // No point in populating it. - if (deleted) { - return logBookmark(rec); - } - - int rowType = getTypeFromCursor(cur); - String typeString = BrowserContractHelpers.typeStringForCode(rowType); - - if (typeString == null) { - Logger.warn(LOG_TAG, "Unsupported type code " + rowType); - return null; - } - - Logger.trace(LOG_TAG, "Record " + guid + " has type " + typeString); - - rec.type = typeString; - rec.title = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.TITLE); - rec.bookmarkURI = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.URL); - rec.description = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.DESCRIPTION); - rec.tags = RepoUtils.getJSONArrayFromCursor(cur, BrowserContract.Bookmarks.TAGS); - rec.keyword = RepoUtils.getStringFromCursor(cur, BrowserContract.Bookmarks.KEYWORD); - - rec.androidID = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks._ID); - rec.androidPosition = RepoUtils.getLongFromCursor(cur, BrowserContract.Bookmarks.POSITION); - rec.children = children; - - // 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. - BookmarkRecord withParentFields = computeParentFields(rec, parentGUID, parentName); - if (withParentFields == null) { - // Oh dear. Something went wrong. - return null; - } - return logBookmark(withParentFields); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java deleted file mode 100644 index c09d64708..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java +++ /dev/null @@ -1,188 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; - -public class AndroidBrowserHistoryDataAccessor extends - AndroidBrowserRepositoryDataAccessor { - - public AndroidBrowserHistoryDataAccessor(Context context) { - super(context); - } - - @Override - protected Uri getUri() { - return BrowserContractHelpers.HISTORY_CONTENT_URI; - } - - @Override - protected ContentValues getContentValues(Record record) { - ContentValues cv = new ContentValues(); - HistoryRecord rec = (HistoryRecord) record; - cv.put(BrowserContract.History.GUID, rec.guid); - cv.put(BrowserContract.History.TITLE, rec.title); - cv.put(BrowserContract.History.URL, rec.histURI); - if (rec.visits != null) { - JSONArray visits = rec.visits; - long mostRecent = getLastVisited(visits); - - // Fennec stores history timestamps in milliseconds, and visit timestamps in microseconds. - // The rest of Sync works in microseconds. This is the conversion point for records coming form Sync. - cv.put(BrowserContract.History.DATE_LAST_VISITED, mostRecent / 1000); - cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, mostRecent / 1000); - cv.put(BrowserContract.History.VISITS, Long.toString(visits.size())); - } - return cv; - } - - @Override - protected String[] getAllColumns() { - return BrowserContractHelpers.HistoryColumns; - } - - @Override - public Uri insert(Record record) { - HistoryRecord rec = (HistoryRecord) record; - - Logger.debug(LOG_TAG, "Storing record " + record.guid); - Uri newRecordUri = super.insert(record); - - Logger.debug(LOG_TAG, "Storing visits for " + record.guid); - context.getContentResolver().bulkInsert( - BrowserContract.Visits.CONTENT_URI, - VisitsHelper.getVisitsContentValues(rec.guid, rec.visits) - ); - - return newRecordUri; - } - - /** - * Given oldGUID, first updates corresponding history record with new values (super operation), - * and then inserts visits from the new record. - * Existing visits from the old record are updated on database level to point to new GUID if necessary. - * - * @param oldGUID GUID of old <code>HistoryRecord</code> - * @param newRecord new <code>HistoryRecord</code> to replace old one with, and insert visits from - */ - @Override - public void update(String oldGUID, Record newRecord) { - // First, update existing history records with new values. This might involve changing history GUID, - // and thanks to ON UPDATE CASCADE clause on Visits.HISTORY_GUID foreign key, visits will be "ported over" - // to the new GUID. - super.update(oldGUID, newRecord); - - // Now we need to insert any visits from the new record - HistoryRecord rec = (HistoryRecord) newRecord; - String newGUID = newRecord.guid; - Logger.debug(LOG_TAG, "Storing visits for " + newGUID + ", replacing " + oldGUID); - - context.getContentResolver().bulkInsert( - BrowserContract.Visits.CONTENT_URI, - VisitsHelper.getVisitsContentValues(newGUID, rec.visits) - ); - } - - /** - * Insert records. - * <p> - * This inserts all the records (using <code>ContentProvider.bulkInsert</code>), - * then inserts all the visit information (also using <code>ContentProvider.bulkInsert</code>). - * - * @param records - * the records to insert. - * @return - * the number of records actually inserted. - * @throws NullCursorException - */ - public int bulkInsert(ArrayList<HistoryRecord> records) throws NullCursorException { - if (records.isEmpty()) { - Logger.debug(LOG_TAG, "No records to insert, returning."); - } - - int size = records.size(); - ContentValues[] cvs = new ContentValues[size]; - int index = 0; - for (Record record : records) { - if (record.guid == null) { - throw new IllegalArgumentException("Record with null GUID passed in to bulkInsert."); - } - cvs[index] = getContentValues(record); - index += 1; - } - - // First update the history records. - int inserted = context.getContentResolver().bulkInsert(getUri(), cvs); - if (inserted == size) { - Logger.debug(LOG_TAG, "Inserted " + inserted + " records, as expected."); - } else { - Logger.debug(LOG_TAG, "Inserted " + - inserted + " records but expected " + - size + " records; continuing to update visits."); - } - - final ContentValues remoteVisitAggregateValues = new ContentValues(); - final Uri historyIncrementRemoteAggregateUri = getUri().buildUpon() - .appendQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES, "true") - .build(); - for (Record record : records) { - HistoryRecord rec = (HistoryRecord) record; - if (rec.visits != null && rec.visits.size() != 0) { - int remoteVisitsInserted = context.getContentResolver().bulkInsert( - BrowserContract.Visits.CONTENT_URI, - VisitsHelper.getVisitsContentValues(rec.guid, rec.visits) - ); - - // If we just inserted any visits, update remote visit aggregate values. - // While inserting visits, we might not insert all of rec.visits - if we already have a local - // visit record with matching (guid,date), we will skip that visit. - // Remote visits aggregate value will be incremented by number of visits inserted. - // Note that we don't need to set REMOTE_DATE_LAST_VISITED, because it already gets set above. - if (remoteVisitsInserted > 0) { - // Note that REMOTE_VISITS must be set before calling cr.update(...) with a URI - // that has PARAM_INCREMENT_REMOTE_AGGREGATES=true. - remoteVisitAggregateValues.put(BrowserContract.History.REMOTE_VISITS, remoteVisitsInserted); - context.getContentResolver().update( - historyIncrementRemoteAggregateUri, - remoteVisitAggregateValues, - BrowserContract.History.GUID + " = ?", new String[] {rec.guid} - ); - } - } - } - - return inserted; - } - - /** - * Helper method used to find largest <code>VisitsHelper.SYNC_DATE_KEY</code> value in a provided JSONArray. - * - * @param visits Array of objects which will be searched. - * @return largest value of <code>VisitsHelper.SYNC_DATE_KEY</code>. - */ - private long getLastVisited(JSONArray visits) { - long mostRecent = 0; - for (int i = 0; i < visits.size(); i++) { - final JSONObject visit = (JSONObject) visits.get(i); - long visitDate = (Long) visit.get(VisitsHelper.SYNC_DATE_KEY); - if (visitDate > mostRecent) { - mostRecent = visitDate; - } - } - return mostRecent; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java deleted file mode 100644 index bd2b5d31f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import org.mozilla.gecko.sync.repositories.HistoryRepository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -public class AndroidBrowserHistoryRepository extends AndroidBrowserRepository implements HistoryRepository { - - @Override - protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) { - AndroidBrowserHistoryRepositorySession session = new AndroidBrowserHistoryRepositorySession(AndroidBrowserHistoryRepository.this, context); - delegate.onSessionCreated(session); - } - - @Override - protected AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context) { - return new AndroidBrowserHistoryDataAccessor(context); - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java deleted file mode 100644 index 7c462abc3..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java +++ /dev/null @@ -1,208 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; -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.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentProviderClient; -import android.content.Context; -import android.database.Cursor; -import android.os.RemoteException; - -public class AndroidBrowserHistoryRepositorySession extends AndroidBrowserRepositorySession { - public static final String LOG_TAG = "ABHistoryRepoSess"; - - /** - * The number of records to queue for insertion before writing to databases. - */ - public static final int INSERT_RECORD_THRESHOLD = 50; - public static final int RECENT_VISITS_LIMIT = 20; - - public AndroidBrowserHistoryRepositorySession(Repository repository, Context context) { - super(repository); - dbHelper = new AndroidBrowserHistoryDataAccessor(context); - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - // HACK: Fennec creates history records without a GUID. Mercilessly drop - // them on the floor. See Bug 739514. - try { - dbHelper.delete(BrowserContract.History.GUID + " IS NULL", null); - } catch (Exception e) { - // Ignore. - } - super.begin(delegate); - } - - @Override - protected Record retrieveDuringStore(Cursor cur) { - return RepoUtils.historyFromMirrorCursor(cur); - } - - @Override - protected Record retrieveDuringFetch(Cursor cur) { - return RepoUtils.historyFromMirrorCursor(cur); - } - - @Override - protected String buildRecordString(Record record) { - HistoryRecord hist = (HistoryRecord) record; - return hist.histURI; - } - - @Override - public boolean shouldIgnore(Record record) { - if (super.shouldIgnore(record)) { - return true; - } - if (!(record instanceof HistoryRecord)) { - return true; - } - HistoryRecord r = (HistoryRecord) record; - return !RepoUtils.isValidHistoryURI(r.histURI); - } - - @Override - protected Record transformRecord(Record record) throws NullCursorException { - return addVisitsToRecord(record); - } - - private Record addVisitsToRecord(Record record) throws NullCursorException { - Logger.debug(LOG_TAG, "Adding visits for GUID " + record.guid); - - // Sync is an object store, so what we attach here will replace what's already present on the Sync servers. - // We upload just a recent subset of visits for each history record for space and bandwidth reasons. - // We chose 20 to be conservative. See Bug 1164660 for details. - ContentProviderClient visitsClient = dbHelper.context.getContentResolver().acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI); - if (visitsClient == null) { - throw new IllegalStateException("Could not obtain a ContentProviderClient for Visits URI"); - } - - try { - ((HistoryRecord) record).visits = VisitsHelper.getRecentHistoryVisitsForGUID( - visitsClient, record.guid, RECENT_VISITS_LIMIT); - } catch (RemoteException e) { - throw new IllegalStateException("Error while obtaining visits for a record", e); - } finally { - visitsClient.release(); - } - - return record; - } - - @Override - protected Record prepareRecord(Record record) { - return record; - } - - protected final Object recordsBufferMonitor = new Object(); - protected ArrayList<HistoryRecord> recordsBuffer = new ArrayList<HistoryRecord>(); - - /** - * Queue record for insertion, possibly flushing the queue. - * <p> - * Must be called on <code>storeWorkQueue</code> thread! But this is only - * called from <code>store</code>, which is called on the queue thread. - * - * @param record - * A <code>Record</code> with a GUID that is not present locally. - */ - @Override - protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - enqueueNewRecord((HistoryRecord) prepareRecord(record)); - } - - /** - * Batch incoming records until some reasonable threshold is hit or storeDone - * is received. - * <p> - * Must be called on <code>storeWorkQueue</code> thread! - * - * @param record A <code>Record</code> with a GUID that is not present locally. - * @throws NullCursorException - */ - protected void enqueueNewRecord(HistoryRecord record) throws NullCursorException { - synchronized (recordsBufferMonitor) { - if (recordsBuffer.size() >= INSERT_RECORD_THRESHOLD) { - flushNewRecords(); - } - Logger.debug(LOG_TAG, "Enqueuing new record with GUID " + record.guid); - recordsBuffer.add(record); - } - } - - /** - * Flush queue of incoming records to database. - * <p> - * Must be called on <code>storeWorkQueue</code> thread! - * <p> - * Must be locked by recordsBufferMonitor! - * @throws NullCursorException - */ - protected void flushNewRecords() throws NullCursorException { - if (recordsBuffer.size() < 1) { - Logger.debug(LOG_TAG, "No records to flush, returning."); - return; - } - - final ArrayList<HistoryRecord> outgoing = recordsBuffer; - recordsBuffer = new ArrayList<HistoryRecord>(); - Logger.debug(LOG_TAG, "Flushing " + outgoing.size() + " records to database."); - // TODO: move bulkInsert to AndroidBrowserDataAccessor? - int inserted = ((AndroidBrowserHistoryDataAccessor) dbHelper).bulkInsert(outgoing); - if (inserted != outgoing.size()) { - // Something failed; most pessimistic action is to declare that all insertions failed. - // TODO: perform the bulkInsert in a transaction and rollback unless all insertions succeed? - for (HistoryRecord failed : outgoing) { - delegate.onRecordStoreFailed(new RuntimeException("Failed to insert history item with guid " + failed.guid + "."), failed.guid); - } - return; - } - - // All good, everybody succeeded. - for (HistoryRecord succeeded : outgoing) { - try { - // Does not use androidID -- just GUID -> String map. - updateBookkeeping(succeeded); - } catch (NoGuidForIdException | ParentNotFoundException e) { - // Should not happen. - throw new NullCursorException(e); - } catch (NullCursorException e) { - throw e; - } - trackRecord(succeeded); - delegate.onRecordStoreSucceeded(succeeded.guid); // At this point, we are really inserted. - } - } - - @Override - public void storeDone() { - storeWorkQueue.execute(new Runnable() { - @Override - public void run() { - synchronized (recordsBufferMonitor) { - try { - flushNewRecords(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Error flushing records to database.", e); - } - } - storeDone(System.currentTimeMillis()); - } - }); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java deleted file mode 100644 index 6c5c661ee..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepository.java +++ /dev/null @@ -1,74 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; - -import android.content.Context; - -public abstract class AndroidBrowserRepository extends Repository { - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, Context context) { - new CreateSessionThread(delegate, context).start(); - } - - @Override - public void clean(boolean success, RepositorySessionCleanDelegate delegate, Context context) { - // Only clean deleted records if success - if (success) { - new CleanThread(delegate, context).start(); - } - } - - class CleanThread extends Thread { - private final RepositorySessionCleanDelegate delegate; - private final Context context; - - public CleanThread(RepositorySessionCleanDelegate delegate, Context context) { - if (context == null) { - throw new IllegalArgumentException("context is null"); - } - this.delegate = delegate; - this.context = context; - } - - @Override - public void run() { - try { - getDataAccessor(context).purgeDeleted(); - } catch (Exception e) { - delegate.onCleanFailed(AndroidBrowserRepository.this, e); - return; - } - delegate.onCleaned(AndroidBrowserRepository.this); - } - } - - protected abstract AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context); - protected abstract void sessionCreator(RepositorySessionCreationDelegate delegate, Context context); - - class CreateSessionThread extends Thread { - private final RepositorySessionCreationDelegate delegate; - private final Context context; - - public CreateSessionThread(RepositorySessionCreationDelegate delegate, Context context) { - if (context == null) { - throw new IllegalArgumentException("context is null."); - } - this.delegate = delegate; - this.context = context; - } - - @Override - public void run() { - sessionCreator(delegate, context); - } - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java deleted file mode 100644 index 138d63d4c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java +++ /dev/null @@ -1,232 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.List; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.db.CursorDumper; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -public abstract class AndroidBrowserRepositoryDataAccessor { - - private static final String[] GUID_COLUMNS = new String[] { BrowserContract.SyncColumns.GUID }; - protected Context context; - protected static String LOG_TAG = "BrowserDataAccessor"; - protected final RepoUtils.QueryHelper queryHelper; - - public AndroidBrowserRepositoryDataAccessor(Context context) { - this.context = context; - this.queryHelper = new RepoUtils.QueryHelper(context, getUri(), LOG_TAG); - } - - protected abstract String[] getAllColumns(); - - /** - * Produce a <code>ContentValues</code> instance that represents the provided <code>Record</code>. - * - * @param record The <code>Record</code> to be converted. - * @return The <code>ContentValues</code> corresponding to <code>record</code>. - */ - protected abstract ContentValues getContentValues(Record record); - - protected abstract Uri getUri(); - - /** - * Dump all the records in raw format. - */ - public void dumpDB() { - Cursor cur = null; - try { - cur = queryHelper.safeQuery(".dumpDB", null, null, null, null); - CursorDumper.dumpCursor(cur); - } catch (NullCursorException e) { - } finally { - if (cur != null) { - cur.close(); - } - } - } - - public String dateModifiedWhere(long timestamp) { - return BrowserContract.SyncColumns.DATE_MODIFIED + " >= " + Long.toString(timestamp); - } - - public void delete(String where, String[] args) { - Uri uri = getUri(); - context.getContentResolver().delete(uri, where, args); - } - - public void wipe() { - Logger.debug(LOG_TAG, "Wiping."); - delete(null, null); - } - - public void purgeDeleted() throws NullCursorException { - String where = BrowserContract.SyncColumns.IS_DELETED + "= 1"; - Uri uri = getUri(); - Logger.info(LOG_TAG, "Purging deleted from: " + uri); - context.getContentResolver().delete(uri, where, null); - } - - /** - * Remove matching records from the database entirely, i.e., do not set a - * deleted flag, delete entirely. - * - * @param guid - * The GUID of the record to be deleted. - * @return The number of records deleted. - */ - public int purgeGuid(String guid) { - String where = BrowserContract.SyncColumns.GUID + " = ?"; - String[] args = new String[] { guid }; - - int deleted = context.getContentResolver().delete(getUri(), where, args); - if (deleted != 1) { - Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " records for guid " + guid); - } - return deleted; - } - - public void update(String guid, Record newRecord) { - String where = BrowserContract.SyncColumns.GUID + " = ?"; - String[] args = new String[] { guid }; - ContentValues cv = getContentValues(newRecord); - int updated = context.getContentResolver().update(getUri(), cv, where, args); - if (updated != 1) { - Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid); - } - } - - public Uri insert(Record record) { - ContentValues cv = getContentValues(record); - return context.getContentResolver().insert(getUri(), cv); - } - - /** - * Fetch all records. - * <p> - * The caller is responsible for closing the cursor. - * - * @return A cursor. You </b>must</b> close this when you're done with it. - * @throws NullCursorException - */ - public Cursor fetchAll() throws NullCursorException { - return queryHelper.safeQuery(".fetchAll", getAllColumns(), null, null, null); - } - - /** - * Fetch GUIDs for records modified since the provided timestamp. - * <p> - * The caller is responsible for closing the cursor. - * - * @param timestamp A timestamp in milliseconds. - * @return A cursor. You <b>must</b> close this when you're done with it. - * @throws NullCursorException - */ - public Cursor getGUIDsSince(long timestamp) throws NullCursorException { - return queryHelper.safeQuery(".getGUIDsSince", - GUID_COLUMNS, - dateModifiedWhere(timestamp), - null, null); - } - - /** - * Fetch records modified since the provided timestamp. - * <p> - * The caller is responsible for closing the cursor. - * - * @param timestamp A timestamp in milliseconds. - * @return A cursor. You <b>must</b> close this when you're done with it. - * @throws NullCursorException - */ - public Cursor fetchSince(long timestamp) throws NullCursorException { - return queryHelper.safeQuery(".fetchSince", - getAllColumns(), - dateModifiedWhere(timestamp), - null, null); - } - - /** - * Fetch records for the provided GUIDs. - * <p> - * The caller is responsible for closing the cursor. - * - * @param guids The GUIDs of the records to fetch. - * @return A cursor. You <b>must</b> close this when you're done with it. - * @throws NullCursorException - */ - public Cursor fetch(String guids[]) throws NullCursorException { - String where = RepoUtils.computeSQLInClause(guids.length, "guid"); - return queryHelper.safeQuery(".fetch", getAllColumns(), where, guids, null); - } - - public void updateByGuid(String guid, ContentValues cv) { - String where = BrowserContract.SyncColumns.GUID + " = ?"; - String[] args = new String[] { guid }; - - int updated = context.getContentResolver().update(getUri(), cv, where, args); - if (updated == 1) { - return; - } - Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid); - } - - /** - * Insert records. - * <p> - * This inserts all the records (using <code>ContentProvider.bulkInsert</code>), - * but does <b>not</b> update the <code>androidID</code> of each record. - * - * @param records - * the records to insert. - * @return - * the number of records actually inserted. - * @throws NullCursorException - */ - public int bulkInsert(List<Record> records) throws NullCursorException { - if (records.isEmpty()) { - Logger.debug(LOG_TAG, "No records to insert, returning."); - } - - int size = records.size(); - ContentValues[] cvs = new ContentValues[size]; - int index = 0; - for (Record record : records) { - try { - cvs[index] = getContentValues(record); - index += 1; - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception in getContentValues for record with guid " + record.guid, e); - } - } - - if (index != size) { - // bulkInsert treats null ContentValues as blank rows, which we don't want - // to insert into the database. - // We expect exceptions in getContentValues to be exceedingly rare, so we - // re-allocate in the (rare) error case and maintain a fast path for the - // success case. - size = index; - } - - int inserted = context.getContentResolver().bulkInsert(getUri(), cvs); - if (inserted == size) { - Logger.debug(LOG_TAG, "Inserted " + inserted + " records, as expected."); - } else { - Logger.debug(LOG_TAG, "Inserted " + - inserted + " records but expected " + - size + " records."); - } - return inserted; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java deleted file mode 100644 index 4f0da0bcc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/AndroidBrowserRepositorySession.java +++ /dev/null @@ -1,792 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -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; -import org.mozilla.gecko.sync.repositories.RecordFilter; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentUris; -import android.database.Cursor; -import android.net.Uri; -import android.util.SparseArray; - -/** - * 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 StoreTrackingRepositorySession { - public static final String LOG_TAG = "BrowserRepoSession"; - - protected AndroidBrowserRepositoryDataAccessor dbHelper; - - /** - * In order to reconcile the "same record" with two *different* GUIDs (for - * example, the same bookmark created by two different clients), we maintain a - * mapping for each local record from a "record string" to - * "local record GUID". - * <p> - * The "record string" above is a "record identifying unique key" produced by - * <code>buildRecordString</code>. - * <p> - * Since we hash each "record string", this map may produce a false positive. - * In this case, we search the database for a matching record explicitly using - * <code>findByRecordString</code>. - */ - protected SparseArray<String> recordToGuid; - - public AndroidBrowserRepositorySession(Repository repository) { - super(repository); - } - - /** - * Retrieve a record from a cursor. Act as if we don't know the final contents of - * the record: for example, a folder's child array might change. - * - * Return null if this record should not be processed. - * - * @throws NoGuidForIdException - * @throws NullCursorException - * @throws ParentNotFoundException - */ - protected abstract Record retrieveDuringStore(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException; - - /** - * Retrieve a record from a cursor. Ensure that the contents of the database are - * updated to match the record that we're constructing: for example, the children - * of a folder might be repositioned as we generate the folder's record. - * - * @throws NoGuidForIdException - * @throws NullCursorException - * @throws ParentNotFoundException - */ - protected abstract Record retrieveDuringFetch(Cursor cur) throws NoGuidForIdException, NullCursorException, ParentNotFoundException; - - /** - * Override this to allow records to be skipped during insertion. - * - * For example, a session subclass might skip records of an unsupported type. - */ - @SuppressWarnings("static-method") - public boolean shouldIgnore(Record record) { - return false; - } - - /** - * Perform any necessary transformation of a record prior to searching by - * any field other than GUID. - * - * Example: translating remote folder names into local names. - */ - @SuppressWarnings("static-method") - protected void fixupRecord(Record record) { - return; - } - - /** - * Override in subclass to implement record extension. - * - * Populate any fields of the record that are expensive to calculate, - * prior to reconciling. - * - * Example: computing children arrays. - * - * 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 - */ - @SuppressWarnings("static-method") - protected Record transformRecord(Record record) throws NullCursorException { - return record; - } - - @Override - public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { - RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue); - super.sharedBegin(); - - try { - // We do this check here even though it results in one extra call to the DB - // because if we didn't, we have to do a check on every other call since there - // is no way of knowing which call would be hit first. - checkDatabase(); - } catch (ProfileDatabaseException e) { - Logger.error(LOG_TAG, "ProfileDatabaseException from begin. Fennec must be launched once until this error is fixed"); - deferredDelegate.onBeginFailed(e); - return; - } catch (Exception e) { - deferredDelegate.onBeginFailed(e); - return; - } - storeTracker = createStoreTracker(); - deferredDelegate.onBeginSucceeded(this); - } - - @Override - public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - dbHelper = null; - recordToGuid = null; - super.finish(delegate); - } - - /** - * Produce a "record string" (record identifying unique key). - * - * @param record - * the <code>Record</code> to identify. - * @return a <code>String</code> instance. - */ - protected abstract String buildRecordString(Record record); - - protected void checkDatabase() throws ProfileDatabaseException, NullCursorException { - Logger.debug(LOG_TAG, "BEGIN: checking database."); - try { - dbHelper.fetch(new String[] { "none" }).close(); - Logger.debug(LOG_TAG, "END: checking database."); - } catch (NullPointerException e) { - throw new ProfileDatabaseException(e); - } - } - - @Override - public void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate) { - GuidsSinceRunnable command = new GuidsSinceRunnable(timestamp, delegate); - delegateQueue.execute(command); - } - - class GuidsSinceRunnable implements Runnable { - - private final RepositorySessionGuidsSinceDelegate delegate; - private final long timestamp; - - public GuidsSinceRunnable(long timestamp, - RepositorySessionGuidsSinceDelegate delegate) { - this.timestamp = timestamp; - this.delegate = delegate; - } - - @Override - public void run() { - if (!isActive()) { - delegate.onGuidsSinceFailed(new InactiveSessionException(null)); - return; - } - - Cursor cur; - try { - cur = dbHelper.getGUIDsSince(timestamp); - } catch (Exception e) { - delegate.onGuidsSinceFailed(e); - return; - } - - 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 { - Logger.debug(LOG_TAG, "Closing cursor after guidsSince."); - cur.close(); - } - - String guidsArray[] = new String[guids.size()]; - guids.toArray(guidsArray); - delegate.onGuidsSinceSucceeded(guidsArray); - } - } - - @Override - public void fetch(String[] guids, - RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException { - FetchRunnable command = new FetchRunnable(guids, now(), null, delegate); - executeDelegateCommand(command); - } - - abstract class FetchingRunnable implements Runnable { - protected final RepositorySessionFetchRecordsDelegate delegate; - - public FetchingRunnable(RepositorySessionFetchRecordsDelegate delegate) { - this.delegate = delegate; - } - - protected void fetchFromCursor(Cursor cursor, RecordFilter filter, long end) { - Logger.debug(LOG_TAG, "Fetch from cursor:"); - try { - try { - if (!cursor.moveToFirst()) { - delegate.onFetchCompleted(end); - return; - } - while (!cursor.isAfterLast()) { - Record r = retrieveDuringFetch(cursor); - if (r != null) { - if (filter == null || !filter.excludeRecord(r)) { - Logger.trace(LOG_TAG, "Processing record " + r.guid); - delegate.onFetchedRecord(transformRecord(r)); - } else { - Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid); - } - } - cursor.moveToNext(); - } - delegate.onFetchCompleted(end); - } catch (NoGuidForIdException e) { - Logger.warn(LOG_TAG, "No GUID for ID.", e); - delegate.onFetchFailed(e, null); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Exception in fetchFromCursor.", e); - delegate.onFetchFailed(e, null); - return; - } - } finally { - Logger.trace(LOG_TAG, "Closing cursor after fetch."); - cursor.close(); - } - } - } - - public class FetchRunnable extends FetchingRunnable { - private final String[] guids; - private final long end; - private final RecordFilter filter; - - public FetchRunnable(String[] guids, - long end, - RecordFilter filter, - RepositorySessionFetchRecordsDelegate delegate) { - super(delegate); - this.guids = guids; - this.end = end; - this.filter = filter; - } - - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - if (guids == null || guids.length < 1) { - Logger.error(LOG_TAG, "No guids sent to fetch"); - delegate.onFetchFailed(new InvalidRequestException(null), null); - return; - } - - try { - Cursor cursor = dbHelper.fetch(guids); - this.fetchFromCursor(cursor, filter, end); - } catch (NullCursorException e) { - delegate.onFetchFailed(e, null); - } - } - } - - @Override - public void fetchSince(long timestamp, - RepositorySessionFetchRecordsDelegate delegate) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - - Logger.debug(LOG_TAG, "Running fetchSince(" + timestamp + ")."); - FetchSinceRunnable command = new FetchSinceRunnable(timestamp, now(), this.storeTracker.getFilter(), delegate); - delegateQueue.execute(command); - } - - class FetchSinceRunnable extends FetchingRunnable { - private final long since; - private final long end; - private final RecordFilter filter; - - public FetchSinceRunnable(long since, - long end, - RecordFilter filter, - RepositorySessionFetchRecordsDelegate delegate) { - super(delegate); - this.since = since; - this.end = end; - this.filter = filter; - } - - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - try { - Cursor cursor = dbHelper.fetchSince(since); - this.fetchFromCursor(cursor, filter, end); - } catch (NullCursorException e) { - delegate.onFetchFailed(e, null); - return; - } - } - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - this.fetchSince(0, delegate); - } - - protected int storeCount = 0; - - @Override - public void store(final Record record) throws NoStoreDelegateException { - if (delegate == null) { - throw new NoStoreDelegateException(); - } - if (record == null) { - Logger.error(LOG_TAG, "Record sent to store was null"); - throw new IllegalArgumentException("Null record passed to AndroidBrowserRepositorySession.store()."); - } - - storeCount += 1; - Logger.debug(LOG_TAG, "Storing record with GUID " + record.guid + " (stored " + storeCount + " records this session)."); - - // Store Runnables *must* complete synchronously. It's OK, they - // run on a background thread. - Runnable command = new Runnable() { - - @Override - public void run() { - if (!isActive()) { - Logger.warn(LOG_TAG, "AndroidBrowserRepositorySession is inactive. Store failing."); - delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); - return; - } - - // Check that the record is a valid type. - // Fennec only supports bookmarks and folders. All other types of records, - // including livemarks and queries, are simply ignored. - // See Bug 708149. This might be resolved by Fennec changing its database - // schema, or by Sync storing non-applied records in its own private database. - if (shouldIgnore(record)) { - Logger.debug(LOG_TAG, "Ignoring record " + record.guid); - - // Don't throw: we don't want to abort the entire sync when we get a livemark! - // delegate.onRecordStoreFailed(new InvalidBookmarkTypeException(null)); - return; - } - - - // TODO: lift these into the session. - // Temporary: this matches prior syncing semantics, in which only - // the relationship between the local and remote record is considered. - // In the future we'll track these two timestamps and use them to - // determine which records have changed, and thus process incoming - // records more efficiently. - long lastLocalRetrieval = 0; // lastSyncTimestamp? - long lastRemoteRetrieval = 0; // TODO: adjust for clock skew. - boolean remotelyModified = record.lastModified > lastRemoteRetrieval; - - Record existingRecord; - try { - // GUID matching only: deleted records don't have a payload with which to search. - existingRecord = retrieveByGUIDDuringStore(record.guid); - if (record.deleted) { - if (existingRecord == null) { - // We're done. Don't bother with a callback. That can change later - // if we want it to. - trace("Incoming record " + record.guid + " is deleted, and no local version. Bye!"); - return; - } - - if (existingRecord.deleted) { - trace("Local record already deleted. Bye!"); - return; - } - - // Which one wins? - if (!remotelyModified) { - trace("Ignoring deleted record from the past."); - return; - } - - boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval; - if (!locallyModified) { - trace("Remote modified, local not. Deleting."); - storeRecordDeletion(record, existingRecord); - return; - } - - trace("Both local and remote records have been modified."); - if (record.lastModified > existingRecord.lastModified) { - trace("Remote is newer, and deleted. Deleting local."); - storeRecordDeletion(record, existingRecord); - return; - } - - trace("Remote is older, local is not deleted. Ignoring."); - return; - } - // End deletion logic. - - // Now we're processing a non-deleted incoming record. - // Apply any changes we need in order to correctly find existing records. - fixupRecord(record); - - if (existingRecord == null) { - trace("Looking up match for record " + record.guid); - existingRecord = findExistingRecord(record); - } - - if (existingRecord == null) { - // The record is new. - trace("No match. Inserting."); - insert(record); - return; - } - - // We found a local dupe. - trace("Incoming record " + record.guid + " dupes to local record " + existingRecord.guid); - - // Populate more expensive fields prior to reconciling. - existingRecord = transformRecord(existingRecord); - Record toStore = reconcileRecords(record, existingRecord, lastRemoteRetrieval, lastLocalRetrieval); - - if (toStore == null) { - Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record."); - return; - } - - // TODO: pass in timestamps? - - // This section of code will only run if the incoming record is not - // marked as deleted, so we never want to just drop ours from the database: - // we need to upload it later. - // Allowing deleted items to propagate through `replace` allows normal - // logging and side-effects to occur, and is no more expensive than simply - // bumping the modified time. - Logger.debug(LOG_TAG, "Replacing existing " + existingRecord.guid + - (toStore.deleted ? " with deleted record " : " with record ") + - toStore.guid); - Record replaced = replace(toStore, existingRecord); - - // Note that we don't track records here; deciding that is the job - // of reconcileRecords. - Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid + - "(" + replaced.androidID + ")"); - delegate.onRecordStoreSucceeded(replaced.guid); - return; - - } catch (MultipleRecordsForGuidException e) { - Logger.error(LOG_TAG, "Multiple records returned for given guid: " + record.guid); - delegate.onRecordStoreFailed(e, record.guid); - return; - } catch (NoGuidForIdException e) { - Logger.error(LOG_TAG, "Store failed for " + record.guid, e); - delegate.onRecordStoreFailed(e, record.guid); - return; - } catch (Exception e) { - Logger.error(LOG_TAG, "Store failed for " + record.guid, e); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - } - }; - storeWorkQueue.execute(command); - } - - /** - * Process a request for deletion of a record. - * Neither argument will ever be null. - * - * @param record the incoming record. This will be mostly blank, given that it's a deletion. - * @param existingRecord the existing record. Use this to decide how to process the deletion. - */ - protected void storeRecordDeletion(final Record record, final Record existingRecord) { - // TODO: we ought to mark the record as deleted rather than purging it, - // in order to support syncing to multiple destinations. Bug 722607. - dbHelper.purgeGuid(record.guid); - delegate.onRecordStoreSucceeded(record.guid); - } - - protected void insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - Record toStore = prepareRecord(record); - Uri recordURI = dbHelper.insert(toStore); - if (recordURI == null) { - throw new NullCursorException(new RuntimeException("Got null URI inserting record with guid " + record.guid)); - } - toStore.androidID = ContentUris.parseId(recordURI); - - updateBookkeeping(toStore); - trackRecord(toStore); - delegate.onRecordStoreSucceeded(toStore.guid); - - Logger.debug(LOG_TAG, "Inserted record with guid " + toStore.guid + " as androidID " + toStore.androidID); - } - - protected Record replace(Record newRecord, Record existingRecord) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - Record toStore = prepareRecord(newRecord); - - // newRecord should already have suitable androidID and guid. - dbHelper.update(existingRecord.guid, toStore); - updateBookkeeping(toStore); - Logger.debug(LOG_TAG, "replace() returning record " + toStore.guid); - return toStore; - } - - /** - * Retrieve a record from the store by GUID, without writing unnecessarily to the - * database. - * - * @throws NoGuidForIdException - * @throws NullCursorException - * @throws ParentNotFoundException - * @throws MultipleRecordsForGuidException - */ - protected Record retrieveByGUIDDuringStore(String guid) throws - NoGuidForIdException, - NullCursorException, - ParentNotFoundException, - MultipleRecordsForGuidException { - Cursor cursor = dbHelper.fetch(new String[] { guid }); - try { - if (!cursor.moveToFirst()) { - return null; - } - - Record r = retrieveDuringStore(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(); - } - } - - /** - * Attempt to find an equivalent record through some means other than GUID. - * - * @param record - * The record for which to search. - * @return - * An equivalent Record object, or null if none is found. - * - * @throws MultipleRecordsForGuidException - * @throws NoGuidForIdException - * @throws NullCursorException - * @throws ParentNotFoundException - */ - protected Record findExistingRecord(Record record) throws MultipleRecordsForGuidException, - NoGuidForIdException, NullCursorException, ParentNotFoundException { - - Logger.debug(LOG_TAG, "Finding existing record for incoming record with GUID " + record.guid); - String recordString = buildRecordString(record); - if (recordString == null) { - Logger.debug(LOG_TAG, "No record string for incoming record " + record.guid); - return null; - } - - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "Searching with record string " + recordString); - } else { - Logger.debug(LOG_TAG, "Searching with record string."); - } - String guid = getGuidForString(recordString); - if (guid == null) { - Logger.debug(LOG_TAG, "Failed to find existing record for " + record.guid); - return null; - } - - // Our map contained a match, but it could be a false positive. Since - // computed record string is supposed to be a unique key, we can easily - // verify our positive. - Logger.debug(LOG_TAG, "Found one. Checking stored record."); - Record stored = retrieveByGUIDDuringStore(guid); - String storedRecordString = buildRecordString(record); - if (recordString.equals(storedRecordString)) { - Logger.debug(LOG_TAG, "Existing record matches incoming record. Returning existing record."); - return stored; - } - - // Oh no, we got a false positive! (This should be *very* rare -- - // essentially, we got a hash collision.) Search the DB for this record - // explicitly by hand. - Logger.debug(LOG_TAG, "Existing record does not match incoming record. Trying to find record by record string."); - return findByRecordString(recordString); - } - - protected String getGuidForString(String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - if (recordToGuid == null) { - createRecordToGuidMap(); - } - return recordToGuid.get(recordString.hashCode()); - } - - protected void createRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - Logger.info(LOG_TAG, "BEGIN: creating record -> GUID map."); - recordToGuid = new SparseArray<String>(); - - // TODO: we should be able to do this entire thing with string concatenations within SQL. - // Also consider whether it's better to fetch and process every record in the DB into - // memory, or run a query per record to do the same thing. - Cursor cur = dbHelper.fetchAll(); - try { - if (!cur.moveToFirst()) { - return; - } - while (!cur.isAfterLast()) { - Record record = retrieveDuringStore(cur); - if (record != null) { - final String recordString = buildRecordString(record); - if (recordString != null) { - recordToGuid.put(recordString.hashCode(), record.guid); - } - } - cur.moveToNext(); - } - } finally { - cur.close(); - } - Logger.info(LOG_TAG, "END: creating record -> GUID map."); - } - - /** - * Search the local database for a record with the same "record string". - * <p> - * We expect to do this only in the unlikely event of a hash - * collision, so we iterate the database completely. Since we want - * to include information about the parents of bookmarks, it is - * difficult to do better purely using the - * <code>ContentProvider</code> interface. - * - * @param recordString - * the "record string" to search for; must be n - * @return a <code>Record</code> with the same "record string", or - * <code>null</code> if none is present. - * @throws ParentNotFoundException - * @throws NullCursorException - * @throws NoGuidForIdException - */ - protected Record findByRecordString(String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - Cursor cur = dbHelper.fetchAll(); - try { - if (!cur.moveToFirst()) { - return null; - } - while (!cur.isAfterLast()) { - Record record = retrieveDuringStore(cur); - if (record != null) { - final String storedRecordString = buildRecordString(record); - if (recordString.equals(storedRecordString)) { - return record; - } - } - cur.moveToNext(); - } - return null; - } finally { - cur.close(); - } - } - - public void putRecordToGuidMap(String recordString, String guid) throws NoGuidForIdException, NullCursorException, ParentNotFoundException { - if (recordString == null) { - return; - } - - if (recordToGuid == null) { - createRecordToGuidMap(); - } - recordToGuid.put(recordString.hashCode(), guid); - } - - protected abstract Record prepareRecord(Record record); - - protected void updateBookkeeping(Record record) throws NoGuidForIdException, - NullCursorException, - ParentNotFoundException { - putRecordToGuidMap(buildRecordString(record), record.guid); - } - - protected WipeRunnable getWipeRunnable(RepositorySessionWipeDelegate delegate) { - return new WipeRunnable(delegate); - } - - @Override - public void wipe(RepositorySessionWipeDelegate delegate) { - Runnable command = getWipeRunnable(delegate); - storeWorkQueue.execute(command); - } - - class WipeRunnable implements Runnable { - protected RepositorySessionWipeDelegate delegate; - - public WipeRunnable(RepositorySessionWipeDelegate delegate) { - this.delegate = delegate; - } - - @Override - public void run() { - if (!isActive()) { - delegate.onWipeFailed(new InactiveSessionException(null)); - return; - } - dbHelper.wipe(); - delegate.onWipeSucceeded(); - } - } - - // For testing purposes. - public AndroidBrowserRepositoryDataAccessor getDBHelper() { - return dbHelper; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java deleted file mode 100644 index d8d8756f7..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksDeletionManager.java +++ /dev/null @@ -1,239 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; - -/** - * Queue up deletions. Process them at the end. - * - * Algorithm: - * - * * Collect GUIDs as we go. For convenience we partition these into - * folders and non-folders. - * - * * Non-folders can be deleted in batches as we go. - * - * * At the end of the sync: - * * Delete all that aren't folders. - * * Move the remaining children of any that are folders to an "Orphans" folder. - * - We do this even for children that are _marked_ as deleted -- we still want - * to upload them, and their parent is irrelevant. - * * Delete all the folders. - * - * * Any outstanding records -- the ones we moved to "Orphans" -- are true orphans. - * These should be reuploaded (because their parent has changed), as should their - * new parent (because its children array has changed). - * We achieve the former by moving them without tracking (but we don't make any - * special effort here -- warning! Lurking bug!). - * We achieve the latter by bumping its mtime. The caller should take care of untracking it. - * - * Note that we make no particular effort to handle repositioning or reparenting: - * batching deletes at the end should be handled seamlessly by existing code, - * because the deleted records could have arrived in a batch at the end regardless. - * - * Note that this class is not thread safe. This should be fine: call it only - * from within a store runnable. - * - */ -public class BookmarksDeletionManager { - private static final String LOG_TAG = "BookmarkDelete"; - - private final AndroidBrowserBookmarksDataAccessor dataAccessor; - private RepositorySessionStoreDelegate delegate; - - private final int flushThreshold; - - private final HashSet<String> folders = new HashSet<String>(); - private final HashSet<String> nonFolders = new HashSet<String>(); - private int nonFolderCount = 0; - - // Records that we need to touch once we've deleted the non-folders. - private HashSet<String> nonFolderParents = new HashSet<String>(); - private HashSet<String> folderParents = new HashSet<String>(); - - /** - * Create an instance to be used for tracking deletions in a bookmarks - * repository session. - * - * @param dataAccessor - * Used to effect database changes. - * - * @param flushThreshold - * When this many non-folder records have been stored for deletion, - * an incremental flush occurs. - */ - public BookmarksDeletionManager(AndroidBrowserBookmarksDataAccessor dataAccessor, int flushThreshold) { - this.dataAccessor = dataAccessor; - this.flushThreshold = flushThreshold; - } - - /** - * Set the delegate to use for callbacks. - * If not invoked, no callbacks will be submitted. - * - * @param delegate a delegate, which should already be a delayed delegate. - */ - public void setDelegate(RepositorySessionStoreDelegate delegate) { - this.delegate = delegate; - } - - public void deleteRecord(String guid, boolean isFolder, String parentGUID) { - if (guid == null) { - Logger.warn(LOG_TAG, "Cannot queue deletion of record with no GUID."); - return; - } - Logger.debug(LOG_TAG, "Queuing deletion of " + guid); - - if (isFolder) { - folders.add(guid); - if (!folders.contains(parentGUID)) { - // We're not going to delete its parent; will need to bump it. - folderParents.add(parentGUID); - } - - nonFolderParents.remove(guid); - folderParents.remove(guid); - return; - } - - if (!folders.contains(parentGUID)) { - // We're not going to delete its parent; will need to bump it. - nonFolderParents.add(parentGUID); - } - - if (nonFolders.add(guid)) { - if (++nonFolderCount >= flushThreshold) { - deleteNonFolders(); - } - } - } - - /** - * Flush deletions that can be easily taken care of right now. - */ - public void incrementalFlush() { - // Yes, this means we only bump when we finish, not during an incremental flush. - deleteNonFolders(); - } - - /** - * Apply all pending deletions and reset state for the next batch of stores. - * - * @param orphanDestination the ID of the folder to which orphaned children - * should be moved. - * - * @throws NullCursorException - * @return a set of IDs to untrack. Will not be null. - */ - public Set<String> flushAll(long orphanDestination, long now) throws NullCursorException { - Logger.debug(LOG_TAG, "Doing complete flush of deleted items. Moving orphans to " + orphanDestination); - deleteNonFolders(); - - // Find out which parents *won't* be deleted, and thus need to have their - // modified times bumped. - nonFolderParents.removeAll(folders); - - Logger.debug(LOG_TAG, "Bumping modified times for " + nonFolderParents.size() + - " parents of deleted non-folders."); - dataAccessor.bumpModifiedByGUID(nonFolderParents, now); - - if (folders.size() > 0) { - final String[] folderGUIDs = folders.toArray(new String[folders.size()]); - final String[] folderIDs = getIDs(folderGUIDs); // Throws if any don't exist. - int moved = dataAccessor.moveChildren(folderIDs, orphanDestination); - if (moved > 0) { - dataAccessor.bumpModified(orphanDestination, now); - } - - // We've deleted or moved anything that might be under these folders. - // Just delete them. - final String folderWhere = RepoUtils.computeSQLInClause(folders.size(), BrowserContract.Bookmarks.GUID); - dataAccessor.delete(folderWhere, folderGUIDs); - invokeCallbacks(delegate, folderGUIDs); - - folderParents.removeAll(folders); - Logger.debug(LOG_TAG, "Bumping modified times for " + folderParents.size() + - " parents of deleted folders."); - dataAccessor.bumpModifiedByGUID(folderParents, now); - - // Clean up. - folders.clear(); - } - - HashSet<String> ret = nonFolderParents; - ret.addAll(folderParents); - - nonFolderParents = new HashSet<String>(); - folderParents = new HashSet<String>(); - return ret; - } - - private String[] getIDs(String[] guids) throws NullCursorException { - // Convert GUIDs to numeric IDs. - String[] ids = new String[guids.length]; - Map<String, Long> guidsToIDs = dataAccessor.idsForGUIDs(guids); - for (int i = 0; i < guids.length; ++i) { - String guid = guids[i]; - Long id = guidsToIDs.get(guid); - if (id == null) { - throw new IllegalArgumentException("Can't get ID for unknown record " + guid); - } - ids[i] = id.toString(); - } - return ids; - } - - /** - * Flush non-folder deletions. This can be called at any time. - */ - private void deleteNonFolders() { - if (nonFolderCount == 0) { - Logger.debug(LOG_TAG, "No non-folders to delete."); - return; - } - - Logger.debug(LOG_TAG, "Applying deletion of " + nonFolderCount + " non-folders."); - final String[] nonFolderGUIDs = nonFolders.toArray(new String[nonFolderCount]); - final String nonFolderWhere = RepoUtils.computeSQLInClause(nonFolderCount, BrowserContract.Bookmarks.GUID); - dataAccessor.delete(nonFolderWhere, nonFolderGUIDs); - - invokeCallbacks(delegate, nonFolderGUIDs); - - // Discard these. - // Note that we maintain folderParents and nonFolderParents; we need them later. - nonFolders.clear(); - nonFolderCount = 0; - } - - private void invokeCallbacks(RepositorySessionStoreDelegate delegate, - String[] nonFolderGUIDs) { - if (delegate == null) { - return; - } - Logger.trace(LOG_TAG, "Invoking store callback for " + nonFolderGUIDs.length + " GUIDs."); - for (String guid : nonFolderGUIDs) { - delegate.onRecordStoreSucceeded(guid); - } - } - - /** - * Clear state in case of redundancy (e.g., wipe). - */ - public void clear() { - nonFolders.clear(); - nonFolderCount = 0; - folders.clear(); - nonFolderParents.clear(); - folderParents.clear(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java deleted file mode 100644 index 98670d39b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BookmarksInsertionManager.java +++ /dev/null @@ -1,298 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; - -/** - * Queue up insertions: - * <ul> - * <li>Folder inserts where the parent is known. Do these immediately, because - * they allow other records to be inserted. Requires bookkeeping updates. On - * insert, flush the next set.</li> - * <li>Regular inserts where the parent is known. These can happen whenever. - * Batch for speed.</li> - * <li>Records where the parent is not known. These can be flushed out when the - * parent is known, or entered as orphans. This can be a queue earlier in the - * process, so they don't get assigned to Unsorted. Feed into the main batch - * when the parent arrives.</li> - * </ul> - * <p> - * Deletions are always done at the end so that orphaning is minimized, and - * that's why we are batching folders and non-folders separately. - * <p> - * Updates are always applied as they arrive. - * <p> - * Note that this class is not thread safe. This should be fine: call it only - * from within a store runnable. - */ -public class BookmarksInsertionManager { - public static final String LOG_TAG = "BookmarkInsert"; - public static boolean DEBUG = false; - - protected final int flushThreshold; - protected final BookmarkInserter inserter; - - /** - * Folders that have been successfully inserted. - */ - private final Set<String> insertedFolders = new HashSet<String>(); - - /** - * Non-folders waiting for bulk insertion. - * <p> - * We write in insertion order to keep things easy to debug. - */ - private final Set<BookmarkRecord> nonFoldersToWrite = new LinkedHashSet<BookmarkRecord>(); - - /** - * Map from parent folder GUID to child records (folders and non-folders) - * waiting to be enqueued after parent folder is inserted. - */ - private final Map<String, Set<BookmarkRecord>> recordsWaitingForParent = new HashMap<String, Set<BookmarkRecord>>(); - - /** - * Create an instance to be used for tracking insertions in a bookmarks - * repository session. - * - * @param flushThreshold - * When this many non-folder records have been stored for insertion, - * an incremental flush occurs. - * @param insertedFolders - * The GUIDs of all the folders already inserted into the database. - * @param inserter - * The <code>BookmarkInsert</code> to use. - */ - public BookmarksInsertionManager(int flushThreshold, Collection<String> insertedFolders, BookmarkInserter inserter) { - this.flushThreshold = flushThreshold; - this.insertedFolders.addAll(insertedFolders); - this.inserter = inserter; - } - - protected void addRecordWithUnwrittenParent(BookmarkRecord record) { - Set<BookmarkRecord> destination = recordsWaitingForParent.get(record.parentID); - if (destination == null) { - destination = new LinkedHashSet<BookmarkRecord>(); - recordsWaitingForParent.put(record.parentID, destination); - } - destination.add(record); - } - - /** - * If <code>record</code> is a folder, insert it immediately; if it is a - * non-folder, enqueue it. Then do the same for any records waiting for this record. - * - * @param record - * the <code>BookmarkRecord</code> to enqueue. - */ - protected void recursivelyEnqueueRecordAndChildren(BookmarkRecord record) { - if (record.isFolder()) { - if (!inserter.insertFolder(record)) { - Logger.warn(LOG_TAG, "Folder with known parent with guid " + record.parentID + " failed to insert!"); - return; - } - Logger.debug(LOG_TAG, "Folder with known parent with guid " + record.parentID + " inserted; adding to inserted folders."); - insertedFolders.add(record.guid); - } else { - Logger.debug(LOG_TAG, "Non-folder has known parent with guid " + record.parentID + "; adding to insertion queue."); - nonFoldersToWrite.add(record); - } - - // Now process record's children. - Set<BookmarkRecord> waiting = recordsWaitingForParent.remove(record.guid); - if (waiting == null) { - return; - } - for (BookmarkRecord waiter : waiting) { - recursivelyEnqueueRecordAndChildren(waiter); - } - } - - /** - * Enqueue a folder. - * - * @param record - * the folder to enqueue. - */ - protected void enqueueFolder(BookmarkRecord record) { - Logger.debug(LOG_TAG, "Inserting folder with guid " + record.guid); - - if (!insertedFolders.contains(record.parentID)) { - Logger.debug(LOG_TAG, "Folder has unknown parent with guid " + record.parentID + "; keeping until we see the parent."); - addRecordWithUnwrittenParent(record); - return; - } - - // Parent is known; add as much of the tree as this roots. - recursivelyEnqueueRecordAndChildren(record); - flushNonFoldersIfNecessary(); - } - - /** - * Enqueue a non-folder. - * - * @param record - * the non-folder to enqueue. - */ - protected void enqueueNonFolder(BookmarkRecord record) { - Logger.debug(LOG_TAG, "Inserting non-folder with guid " + record.guid); - - if (!insertedFolders.contains(record.parentID)) { - Logger.debug(LOG_TAG, "Non-folder has unknown parent with guid " + record.parentID + "; keeping until we see the parent."); - addRecordWithUnwrittenParent(record); - return; - } - - // Parent is known; add to insertion queue and maybe write. - Logger.debug(LOG_TAG, "Non-folder has known parent with guid " + record.parentID + "; adding to insertion queue."); - nonFoldersToWrite.add(record); - flushNonFoldersIfNecessary(); - } - - /** - * Enqueue a bookmark record for eventual insertion. - * - * @param record - * the <code>BookmarkRecord</code> to enqueue. - */ - public void enqueueRecord(BookmarkRecord record) { - if (record.isFolder()) { - enqueueFolder(record); - } else { - enqueueNonFolder(record); - } - if (DEBUG) { - dumpState(); - } - } - - /** - * Flush non-folders; empties the insertion queue entirely. - */ - protected void flushNonFolders() { - inserter.bulkInsertNonFolders(nonFoldersToWrite); // All errors are handled in bulkInsertNonFolders. - nonFoldersToWrite.clear(); - } - - /** - * Flush non-folder insertions if there are many of them; empties the - * insertion queue entirely. - */ - protected void flushNonFoldersIfNecessary() { - int num = nonFoldersToWrite.size(); - if (num < flushThreshold) { - Logger.debug(LOG_TAG, "Incremental flush called with " + num + " < " + flushThreshold + " non-folders; not flushing."); - return; - } - Logger.debug(LOG_TAG, "Incremental flush called with " + num + " non-folders; flushing."); - flushNonFolders(); - } - - /** - * Insert all remaining folders followed by all remaining non-folders, - * regardless of whether parent records have been successfully inserted. - */ - public void finishUp() { - // Iterate through all waiting records, writing the folders and collecting - // the non-folders for bulk insertion. - int numFolders = 0; - int numNonFolders = 0; - for (Set<BookmarkRecord> records : recordsWaitingForParent.values()) { - for (BookmarkRecord record : records) { - if (!record.isFolder()) { - numNonFolders += 1; - nonFoldersToWrite.add(record); - continue; - } - - numFolders += 1; - if (!inserter.insertFolder(record)) { - Logger.warn(LOG_TAG, "Folder with known parent with guid " + record.parentID + " failed to insert!"); - continue; - } - - Logger.debug(LOG_TAG, "Folder with known parent with guid " + record.parentID + " inserted; adding to inserted folders."); - insertedFolders.add(record.guid); - } - } - recordsWaitingForParent.clear(); - flushNonFolders(); - - Logger.debug(LOG_TAG, "finishUp inserted " + - numFolders + " folders without known parents and " + - numNonFolders + " non-folders without known parents."); - if (DEBUG) { - dumpState(); - } - } - - public void clear() { - this.insertedFolders.clear(); - this.nonFoldersToWrite.clear(); - this.recordsWaitingForParent.clear(); - } - - // For debugging. - public boolean isClear() { - return nonFoldersToWrite.isEmpty() && recordsWaitingForParent.isEmpty(); - } - - // For debugging. - public void dumpState() { - ArrayList<String> readies = new ArrayList<String>(); - for (BookmarkRecord record : nonFoldersToWrite) { - readies.add(record.guid); - } - String ready = Utils.toCommaSeparatedString(new ArrayList<String>(readies)); - - ArrayList<String> waits = new ArrayList<String>(); - for (Set<BookmarkRecord> recs : recordsWaitingForParent.values()) { - for (BookmarkRecord rec : recs) { - waits.add(rec.guid); - } - } - String waiting = Utils.toCommaSeparatedString(waits); - String known = Utils.toCommaSeparatedString(insertedFolders); - - Logger.debug(LOG_TAG, "Q=(" + ready + "), W = (" + waiting + "), P=(" + known + ")"); - } - - public interface BookmarkInserter { - /** - * Insert a single folder. - * <p> - * All exceptions should be caught and all delegate callbacks invoked here. - * - * @param record - * the record to insert. - * @return - * <code>true</code> if the folder was inserted; <code>false</code> otherwise. - */ - public boolean insertFolder(BookmarkRecord record); - - /** - * Insert many non-folders. Each non-folder's parent was already present in - * the database before this <code>BookmarkInsertionsManager</code> was - * created, or had <code>insertFolder</code> called with it as argument (and - * possibly was not inserted). - * <p> - * All exceptions should be caught and all delegate callbacks invoked here. - * - * @param records - * the records to insert. - */ - public void bulkInsertNonFolders(Collection<BookmarkRecord> records); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java deleted file mode 100644 index e83aea087..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpers.java +++ /dev/null @@ -1,154 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.setup.Constants; - -import android.net.Uri; - -public class BrowserContractHelpers extends BrowserContract { - - protected static Uri withSyncAndDeletedAndProfile(Uri u) { - return u.buildUpon() - .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE) - .appendQueryParameter(PARAM_IS_SYNC, "true") - .appendQueryParameter(PARAM_SHOW_DELETED, "true") - .build(); - } - protected static Uri withSyncAndProfile(Uri u) { - return u.buildUpon() - .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE) - .appendQueryParameter(PARAM_IS_SYNC, "true") - .build(); - } - - public static final Uri BOOKMARKS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.CONTENT_URI); - public static final Uri BOOKMARKS_PARENTS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.PARENTS_CONTENT_URI); - public static final Uri BOOKMARKS_POSITIONS_CONTENT_URI = withSyncAndDeletedAndProfile(Bookmarks.POSITIONS_CONTENT_URI); - public static final Uri HISTORY_CONTENT_URI = withSyncAndDeletedAndProfile(History.CONTENT_URI); - public static final Uri VISITS_CONTENT_URI = withSyncAndDeletedAndProfile(Visits.CONTENT_URI); - public static final Uri SCHEMA_CONTENT_URI = withSyncAndDeletedAndProfile(Schema.CONTENT_URI); - public static final Uri PASSWORDS_CONTENT_URI = withSyncAndDeletedAndProfile(Passwords.CONTENT_URI); - public static final Uri DELETED_PASSWORDS_CONTENT_URI = withSyncAndDeletedAndProfile(DeletedPasswords.CONTENT_URI); - public static final Uri FORM_HISTORY_CONTENT_URI = withSyncAndProfile(FormHistory.CONTENT_URI); - public static final Uri DELETED_FORM_HISTORY_CONTENT_URI = withSyncAndProfile(DeletedFormHistory.CONTENT_URI); - public static final Uri TABS_CONTENT_URI = withSyncAndProfile(Tabs.CONTENT_URI); - public static final Uri CLIENTS_CONTENT_URI = withSyncAndProfile(Clients.CONTENT_URI); - public static final Uri LOGINS_CONTENT_URI = withSyncAndProfile(Logins.CONTENT_URI); - - public static final String[] PasswordColumns = new String[] { - Passwords.ID, - Passwords.HOSTNAME, - Passwords.HTTP_REALM, - Passwords.FORM_SUBMIT_URL, - Passwords.USERNAME_FIELD, - Passwords.PASSWORD_FIELD, - Passwords.ENCRYPTED_USERNAME, - Passwords.ENCRYPTED_PASSWORD, - Passwords.ENC_TYPE, - Passwords.TIME_CREATED, - Passwords.TIME_LAST_USED, - Passwords.TIME_PASSWORD_CHANGED, - Passwords.TIMES_USED, - Passwords.GUID - }; - - public static final String[] HistoryColumns = new String[] { - CommonColumns._ID, - SyncColumns.GUID, - SyncColumns.DATE_CREATED, - SyncColumns.DATE_MODIFIED, - SyncColumns.IS_DELETED, - History.TITLE, - History.URL, - History.DATE_LAST_VISITED, - History.VISITS - }; - - public static final String[] BookmarkColumns = new String[] { - CommonColumns._ID, - SyncColumns.GUID, - SyncColumns.DATE_CREATED, - SyncColumns.DATE_MODIFIED, - SyncColumns.IS_DELETED, - Bookmarks.TITLE, - Bookmarks.URL, - Bookmarks.TYPE, - Bookmarks.PARENT, - Bookmarks.POSITION, - Bookmarks.TAGS, - Bookmarks.DESCRIPTION, - Bookmarks.KEYWORD - }; - - public static final String[] FormHistoryColumns = new String[] { - FormHistory.ID, - FormHistory.GUID, - FormHistory.FIELD_NAME, - FormHistory.VALUE, - FormHistory.TIMES_USED, - FormHistory.FIRST_USED, - FormHistory.LAST_USED - }; - - public static final String[] DeletedColumns = new String[] { - BrowserContract.DeletedColumns.ID, - BrowserContract.DeletedColumns.GUID, - BrowserContract.DeletedColumns.TIME_DELETED - }; - - // Mapping from Sync types to Fennec types. - public static final String[] BOOKMARK_TYPE_CODE_TO_STRING = { - // Observe omissions: "microsummary", "item". - "folder", "bookmark", "separator", "livemark", "query" - }; - private static final int MAX_BOOKMARK_TYPE_CODE = BOOKMARK_TYPE_CODE_TO_STRING.length - 1; - public static final Map<String, Integer> BOOKMARK_TYPE_STRING_TO_CODE; - static { - HashMap<String, Integer> t = new HashMap<String, Integer>(); - t.put("folder", Bookmarks.TYPE_FOLDER); - t.put("bookmark", Bookmarks.TYPE_BOOKMARK); - t.put("separator", Bookmarks.TYPE_SEPARATOR); - t.put("livemark", Bookmarks.TYPE_LIVEMARK); - t.put("query", Bookmarks.TYPE_QUERY); - BOOKMARK_TYPE_STRING_TO_CODE = Collections.unmodifiableMap(t); - } - - /** - * Convert a database bookmark type code into the Sync string equivalent. - * - * @param code one of the <code>Bookmarks.TYPE_*</code> enumerations. - * @return the string equivalent, or null if not found. - */ - public static String typeStringForCode(int code) { - if (0 <= code && code <= MAX_BOOKMARK_TYPE_CODE) { - return BOOKMARK_TYPE_CODE_TO_STRING[code]; - } - return null; - } - - /** - * Convert a Sync type string into a Fennec type code. - * - * @param type a type string, such as "livemark". - * @return the type code, or -1 if not found. - */ - public static int typeCodeForString(String type) { - Integer found = BOOKMARK_TYPE_STRING_TO_CODE.get(type); - if (found == null) { - return -1; - } - return found; - } - - public static boolean isSupportedType(String type) { - return BOOKMARK_TYPE_STRING_TO_CODE.containsKey(type); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java deleted file mode 100644 index 5c17f9b85..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/CachedSQLiteOpenHelper.java +++ /dev/null @@ -1,62 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDatabase.CursorFactory; -import android.database.sqlite.SQLiteOpenHelper; - -public abstract class CachedSQLiteOpenHelper extends SQLiteOpenHelper { - - public CachedSQLiteOpenHelper(Context context, String name, CursorFactory factory, - int version) { - super(context, name, factory, version); - } - - // Cache these so we don't have to track them across cursors. Call `close` - // when you're done. - private SQLiteDatabase readableDatabase; - private SQLiteDatabase writableDatabase; - - synchronized protected SQLiteDatabase getCachedReadableDatabase() { - if (readableDatabase == null) { - if (writableDatabase == null) { - readableDatabase = this.getReadableDatabase(); - return readableDatabase; - } else { - return writableDatabase; - } - } else { - return readableDatabase; - } - } - - synchronized protected SQLiteDatabase getCachedWritableDatabase() { - if (writableDatabase == null) { - writableDatabase = this.getWritableDatabase(); - } - return writableDatabase; - } - - @Override - synchronized public void close() { - if (readableDatabase != null) { - readableDatabase.close(); - readableDatabase = null; - } - if (writableDatabase != null) { - writableDatabase.close(); - writableDatabase = null; - } - super.close(); - } - - // Used for testing. - public boolean isClosed() { - return readableDatabase == null && - writableDatabase == null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java deleted file mode 100644 index 4962a20c6..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabase.java +++ /dev/null @@ -1,252 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - -public class ClientsDatabase extends CachedSQLiteOpenHelper { - - public static final String LOG_TAG = "ClientsDatabase"; - - // Database Specifications. - protected static final String DB_NAME = "clients_database"; - protected static final int SCHEMA_VERSION = 3; - - // Clients Table. - public static final String TBL_CLIENTS = "clients"; - public static final String COL_ACCOUNT_GUID = "guid"; - public static final String COL_PROFILE = "profile"; - public static final String COL_NAME = "name"; - public static final String COL_TYPE = "device_type"; - - // Optional fields. - public static final String COL_FORMFACTOR = "formfactor"; - public static final String COL_OS = "os"; - public static final String COL_APPLICATION = "application"; - public static final String COL_APP_PACKAGE = "appPackage"; - public static final String COL_DEVICE = "device"; - - public static final String[] TBL_CLIENTS_COLUMNS = new String[] { COL_ACCOUNT_GUID, COL_PROFILE, COL_NAME, COL_TYPE, - COL_FORMFACTOR, COL_OS, COL_APPLICATION, COL_APP_PACKAGE, COL_DEVICE }; - public static final String TBL_CLIENTS_KEY = COL_ACCOUNT_GUID + " = ? AND " + - COL_PROFILE + " = ?"; - - // Commands Table. - public static final String TBL_COMMANDS = "commands"; - public static final String COL_COMMAND = "command"; - public static final String COL_ARGS = "args"; - - public static final String[] TBL_COMMANDS_COLUMNS = new String[] { COL_ACCOUNT_GUID, COL_COMMAND, COL_ARGS }; - public static final String TBL_COMMANDS_KEY = COL_ACCOUNT_GUID + " = ? AND " + - COL_COMMAND + " = ? AND " + - COL_ARGS + " = ?"; - public static final String TBL_COMMANDS_GUID_QUERY = COL_ACCOUNT_GUID + " = ? "; - - private final RepoUtils.QueryHelper queryHelper; - - public ClientsDatabase(Context context) { - super(context, DB_NAME, null, SCHEMA_VERSION); - this.queryHelper = new RepoUtils.QueryHelper(context, null, LOG_TAG); - Logger.debug(LOG_TAG, "ClientsDatabase instantiated."); - } - - @Override - public void onCreate(SQLiteDatabase db) { - Logger.debug(LOG_TAG, "ClientsDatabase.onCreate()."); - createClientsTable(db); - createCommandsTable(db); - } - - public static void createClientsTable(SQLiteDatabase db) { - Logger.debug(LOG_TAG, "ClientsDatabase.createClientsTable()."); - String createClientsTableSql = "CREATE TABLE " + TBL_CLIENTS + " (" - + COL_ACCOUNT_GUID + " TEXT, " - + COL_PROFILE + " TEXT, " - + COL_NAME + " TEXT, " - + COL_TYPE + " TEXT, " - + COL_FORMFACTOR + " TEXT, " - + COL_OS + " TEXT, " - + COL_APPLICATION + " TEXT, " - + COL_APP_PACKAGE + " TEXT, " - + COL_DEVICE + " TEXT, " - + "PRIMARY KEY (" + COL_ACCOUNT_GUID + ", " + COL_PROFILE + "))"; - db.execSQL(createClientsTableSql); - } - - public static void createCommandsTable(SQLiteDatabase db) { - Logger.debug(LOG_TAG, "ClientsDatabase.createCommandsTable()."); - String createCommandsTableSql = "CREATE TABLE " + TBL_COMMANDS + " (" - + COL_ACCOUNT_GUID + " TEXT, " - + COL_COMMAND + " TEXT, " - + COL_ARGS + " TEXT, " - + "PRIMARY KEY (" + COL_ACCOUNT_GUID + ", " + COL_COMMAND + ", " + COL_ARGS + "), " - + "FOREIGN KEY (" + COL_ACCOUNT_GUID + ") REFERENCES " + TBL_CLIENTS + " (" + COL_ACCOUNT_GUID + "))"; - db.execSQL(createCommandsTableSql); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Logger.debug(LOG_TAG, "ClientsDatabase.onUpgrade(" + oldVersion + ", " + newVersion + ")."); - if (oldVersion < 2) { - // For now we'll just drop and recreate the tables. - db.execSQL("DROP TABLE IF EXISTS " + TBL_CLIENTS); - db.execSQL("DROP TABLE IF EXISTS " + TBL_COMMANDS); - onCreate(db); - return; - } - - if (newVersion >= 3) { - // Add the optional columns to clients. - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_FORMFACTOR + " TEXT"); - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_OS + " TEXT"); - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_APPLICATION + " TEXT"); - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_APP_PACKAGE + " TEXT"); - db.execSQL("ALTER TABLE " + TBL_CLIENTS + " ADD COLUMN " + COL_DEVICE + " TEXT"); - } - } - - public void wipeDB() { - SQLiteDatabase db = this.getCachedWritableDatabase(); - onUpgrade(db, 0, SCHEMA_VERSION); - } - - public void wipeClientsTable() { - SQLiteDatabase db = this.getCachedWritableDatabase(); - db.execSQL("DELETE FROM " + TBL_CLIENTS); - } - - public void wipeCommandsTable() { - SQLiteDatabase db = this.getCachedWritableDatabase(); - db.execSQL("DELETE FROM " + TBL_COMMANDS); - } - - // If a record with given GUID exists, we'll update it, - // otherwise we'll insert it. - public void store(String profileId, ClientRecord record) { - SQLiteDatabase db = this.getCachedWritableDatabase(); - - ContentValues cv = new ContentValues(); - cv.put(COL_ACCOUNT_GUID, record.guid); - cv.put(COL_PROFILE, profileId); - cv.put(COL_NAME, record.name); - cv.put(COL_TYPE, record.type); - - if (record.formfactor != null) { - cv.put(COL_FORMFACTOR, record.formfactor); - } - - if (record.os != null) { - cv.put(COL_OS, record.os); - } - - if (record.application != null) { - cv.put(COL_APPLICATION, record.application); - } - - if (record.appPackage != null) { - cv.put(COL_APP_PACKAGE, record.appPackage); - } - - if (record.device != null) { - cv.put(COL_DEVICE, record.device); - } - - String[] args = new String[] { record.guid, profileId }; - int rowsUpdated = db.update(TBL_CLIENTS, cv, TBL_CLIENTS_KEY, args); - - if (rowsUpdated >= 1) { - Logger.debug(LOG_TAG, "Replaced client record for row with accountGUID " + record.guid); - } else { - long rowId = db.insert(TBL_CLIENTS, null, cv); - Logger.debug(LOG_TAG, "Inserted client record into row: " + rowId); - } - } - - /** - * Store a command in the commands database if it doesn't already exist. - * - * @param accountGUID - * @param command - The command type - * @param args - A JSON string of args - * @throws NullCursorException - */ - public void store(String accountGUID, String command, String args) throws NullCursorException { - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "Storing command " + command + " with args " + args); - } else { - Logger.trace(LOG_TAG, "Storing command " + command + "."); - } - SQLiteDatabase db = this.getCachedWritableDatabase(); - - ContentValues cv = new ContentValues(); - cv.put(COL_ACCOUNT_GUID, accountGUID); - cv.put(COL_COMMAND, command); - if (args == null) { - cv.put(COL_ARGS, "[]"); - } else { - cv.put(COL_ARGS, args); - } - - Cursor cur = this.fetchSpecificCommand(accountGUID, command, args); - try { - if (cur.moveToFirst()) { - Logger.debug(LOG_TAG, "Command already exists in database."); - return; - } - } finally { - cur.close(); - } - - long rowId = db.insert(TBL_COMMANDS, null, cv); - Logger.debug(LOG_TAG, "Inserted command into row: " + rowId); - } - - public Cursor fetchClientsCursor(String accountGUID, String profileId) throws NullCursorException { - String[] args = new String[] { accountGUID, profileId }; - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchClientsCursor", TBL_CLIENTS, TBL_CLIENTS_COLUMNS, TBL_CLIENTS_KEY, args); - } - - public Cursor fetchSpecificCommand(String accountGUID, String command, String commandArgs) throws NullCursorException { - String[] args = new String[] { accountGUID, command, commandArgs }; - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchSpecificCommand", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, TBL_COMMANDS_KEY, args); - } - - public Cursor fetchCommandsForClient(String accountGUID) throws NullCursorException { - String[] args = new String[] { accountGUID }; - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchCommandsForClient", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, TBL_COMMANDS_GUID_QUERY, args); - } - - public Cursor fetchAllClients() throws NullCursorException { - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchAllClients", TBL_CLIENTS, TBL_CLIENTS_COLUMNS, null, null); - } - - public Cursor fetchAllCommands() throws NullCursorException { - SQLiteDatabase db = this.getCachedReadableDatabase(); - - return queryHelper.safeQuery(db, ".fetchAllCommands", TBL_COMMANDS, TBL_COMMANDS_COLUMNS, null, null); - } - - public void deleteClient(String accountGUID, String profileId) { - String[] args = new String[] { accountGUID, profileId }; - - SQLiteDatabase db = this.getCachedWritableDatabase(); - db.delete(TBL_CLIENTS, TBL_CLIENTS_KEY, args); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java deleted file mode 100644 index 4af84ceaf..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/ClientsDatabaseAccessor.java +++ /dev/null @@ -1,178 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.json.simple.JSONArray; - -import org.mozilla.gecko.sync.CommandProcessor.Command; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; -import org.mozilla.gecko.sync.setup.Constants; - -import android.content.Context; -import android.database.Cursor; - -public class ClientsDatabaseAccessor { - - public static final String LOG_TAG = "ClientsDatabaseAccessor"; - - private ClientsDatabase db; - - // Need this so we can properly stub out the class for testing. - public ClientsDatabaseAccessor() {} - - public ClientsDatabaseAccessor(Context context) { - db = new ClientsDatabase(context); - } - - public void store(ClientRecord record) { - db.store(getProfileId(), record); - } - - public void store(Collection<ClientRecord> records) { - for (ClientRecord record : records) { - this.store(record); - } - } - - public void store(String accountGUID, Command command) throws NullCursorException { - db.store(accountGUID, command.commandType, command.args.toJSONString()); - } - - public ClientRecord fetchClient(String accountGUID) throws NullCursorException { - final Cursor cur = db.fetchClientsCursor(accountGUID, getProfileId()); - try { - if (!cur.moveToFirst()) { - return null; - } - return recordFromCursor(cur); - } finally { - cur.close(); - } - } - - public Map<String, ClientRecord> fetchAllClients() throws NullCursorException { - final HashMap<String, ClientRecord> map = new HashMap<String, ClientRecord>(); - final Cursor cur = db.fetchAllClients(); - try { - if (!cur.moveToFirst()) { - return Collections.unmodifiableMap(map); - } - - while (!cur.isAfterLast()) { - ClientRecord clientRecord = recordFromCursor(cur); - map.put(clientRecord.guid, clientRecord); - cur.moveToNext(); - } - return Collections.unmodifiableMap(map); - } finally { - cur.close(); - } - } - - public List<Command> fetchAllCommands() throws NullCursorException { - final List<Command> commands = new ArrayList<Command>(); - final Cursor cur = db.fetchAllCommands(); - try { - if (!cur.moveToFirst()) { - return Collections.unmodifiableList(commands); - } - - while (!cur.isAfterLast()) { - Command command = commandFromCursor(cur); - commands.add(command); - cur.moveToNext(); - } - return Collections.unmodifiableList(commands); - } finally { - cur.close(); - } - } - - public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException { - final List<Command> commands = new ArrayList<Command>(); - final Cursor cur = db.fetchCommandsForClient(accountGUID); - try { - if (!cur.moveToFirst()) { - return Collections.unmodifiableList(commands); - } - - while(!cur.isAfterLast()) { - Command command = commandFromCursor(cur); - commands.add(command); - cur.moveToNext(); - } - return Collections.unmodifiableList(commands); - } finally { - cur.close(); - } - } - - protected static ClientRecord recordFromCursor(Cursor cur) { - final String accountGUID = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID); - final String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME); - final String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE); - - final ClientRecord record = new ClientRecord(accountGUID); - record.name = clientName; - record.type = clientType; - - // Optional fields. These will either be null or strings. - record.formfactor = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_FORMFACTOR); - record.os = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_OS); - record.device = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_DEVICE); - record.appPackage = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_APP_PACKAGE); - record.application = RepoUtils.optStringFromCursor(cur, ClientsDatabase.COL_APPLICATION); - - return record; - } - - protected static Command commandFromCursor(Cursor cur) { - String commandType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_COMMAND); - JSONArray commandArgs = RepoUtils.getJSONArrayFromCursor(cur, ClientsDatabase.COL_ARGS); - return new Command(commandType, commandArgs); - } - - public int clientsCount() { - try { - final Cursor cur = db.fetchAllClients(); - try { - return cur.getCount(); - } finally { - cur.close(); - } - } catch (NullCursorException e) { - return 0; - } - - } - - private String getProfileId() { - return Constants.DEFAULT_PROFILE; - } - - public void wipeDB() { - db.wipeDB(); - } - - public void wipeClientsTable() { - db.wipeClientsTable(); - } - - public void wipeCommandsTable() { - db.wipeCommandsTable(); - } - - public void close() { - db.close(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java deleted file mode 100644 index 720d856eb..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FennecTabsRepository.java +++ /dev/null @@ -1,383 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.db.Tab; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.Clients; -import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.NoContentProviderException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; -import org.mozilla.gecko.sync.repositories.domain.TabsRecord; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; - -public class FennecTabsRepository extends Repository { - private static final String LOG_TAG = "FennecTabsRepository"; - - protected final ClientsDataDelegate clientsDataDelegate; - - public FennecTabsRepository(ClientsDataDelegate clientsDataDelegate) { - this.clientsDataDelegate = clientsDataDelegate; - } - - /** - * Note that -- unlike most repositories -- this will only fetch Fennec's tabs, - * and only store tabs from other clients. - * - * It will never retrieve tabs from other clients, or store tabs for Fennec, - * unless you use {@link #fetch(String[], RepositorySessionFetchRecordsDelegate)} - * and specify an explicit GUID. - */ - public class FennecTabsRepositorySession extends RepositorySession { - protected static final String LOG_TAG = "FennecTabsSession"; - - private final ContentProviderClient tabsProvider; - private final ContentProviderClient clientsProvider; - - protected final RepoUtils.QueryHelper tabsHelper; - - protected final ClientsDatabaseAccessor clientsDatabase; - - protected ContentProviderClient getContentProvider(final Context context, final Uri uri) throws NoContentProviderException { - ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(uri); - if (client == null) { - throw new NoContentProviderException(uri); - } - return client; - } - - protected void releaseProviders() { - try { - clientsProvider.release(); - } catch (Exception e) {} - try { - tabsProvider.release(); - } catch (Exception e) {} - clientsDatabase.close(); - } - - public FennecTabsRepositorySession(Repository repository, Context context) throws NoContentProviderException { - super(repository); - clientsProvider = getContentProvider(context, BrowserContractHelpers.CLIENTS_CONTENT_URI); - try { - tabsProvider = getContentProvider(context, BrowserContractHelpers.TABS_CONTENT_URI); - } catch (NoContentProviderException e) { - clientsProvider.release(); - throw e; - } catch (Exception e) { - clientsProvider.release(); - // Oh, Java. - throw new RuntimeException(e); - } - - tabsHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.TABS_CONTENT_URI, LOG_TAG); - clientsDatabase = new ClientsDatabaseAccessor(context); - } - - @Override - public void abort() { - releaseProviders(); - super.abort(); - } - - @Override - public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - releaseProviders(); - super.finish(delegate); - } - - // Default parameters for local data: local client has null GUID. Override - // these to test against non-live data. - protected String localClientSelection() { - return BrowserContract.Tabs.CLIENT_GUID + " IS NULL"; - } - - protected String[] localClientSelectionArgs() { - return null; - } - - @Override - public void guidsSince(final long timestamp, - final RepositorySessionGuidsSinceDelegate delegate) { - // Bug 783692: Now that Bug 730039 has landed, we could implement this, - // but it's not a priority since it's not used (yet). - Logger.warn(LOG_TAG, "Not returning anything from guidsSince."); - delegateQueue.execute(new Runnable() { - @Override - public void run() { - delegate.onGuidsSinceSucceeded(new String[] {}); - } - }); - } - - @Override - public void fetchSince(final long timestamp, - final RepositorySessionFetchRecordsDelegate delegate) { - if (tabsProvider == null) { - throw new IllegalArgumentException("tabsProvider was null."); - } - if (tabsHelper == null) { - throw new IllegalArgumentException("tabsHelper was null."); - } - - final String positionAscending = BrowserContract.Tabs.POSITION + " ASC"; - - final String localClientSelection = localClientSelection(); - final String[] localClientSelectionArgs = localClientSelectionArgs(); - - final Runnable command = new Runnable() { - @Override - public void run() { - // We fetch all local tabs (since the record must contain them all) - // but only process the record if the timestamp is sufficiently - // recent, or if the client data has been modified. - try { - final Cursor cursor = tabsHelper.safeQuery(tabsProvider, ".fetchSince()", null, - localClientSelection, localClientSelectionArgs, positionAscending); - try { - final String localClientGuid = clientsDataDelegate.getAccountGUID(); - final String localClientName = clientsDataDelegate.getClientName(); - final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, localClientGuid, localClientName); - - if (tabsRecord.lastModified >= timestamp || - clientsDataDelegate.getLastModifiedTimestamp() >= timestamp) { - delegate.onFetchedRecord(tabsRecord); - } - } finally { - cursor.close(); - } - } catch (Exception e) { - delegate.onFetchFailed(e, null); - return; - } - delegate.onFetchCompleted(now()); - } - }; - - delegateQueue.execute(command); - } - - @Override - public void fetch(final String[] guids, - final RepositorySessionFetchRecordsDelegate delegate) { - // Bug 783692: Now that Bug 730039 has landed, we could implement this, - // but it's not a priority since it's not used (yet). - Logger.warn(LOG_TAG, "Not returning anything from fetch"); - delegateQueue.execute(new Runnable() { - @Override - public void run() { - delegate.onFetchCompleted(now()); - } - }); - } - - @Override - public void fetchAll(final RepositorySessionFetchRecordsDelegate delegate) { - fetchSince(0, delegate); - } - - private static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?"; - private static final String CLIENT_GUID_IS = BrowserContract.Clients.GUID + " = ?"; - - @Override - public void store(final Record record) throws NoStoreDelegateException { - if (delegate == null) { - Logger.warn(LOG_TAG, "No store delegate."); - throw new NoStoreDelegateException(); - } - if (record == null) { - Logger.error(LOG_TAG, "Record sent to store was null"); - throw new IllegalArgumentException("Null record passed to FennecTabsRepositorySession.store()."); - } - if (!(record instanceof TabsRecord)) { - Logger.error(LOG_TAG, "Can't store anything but a TabsRecord"); - throw new IllegalArgumentException("Non-TabsRecord passed to FennecTabsRepositorySession.store()."); - } - final TabsRecord tabsRecord = (TabsRecord) record; - - Runnable command = new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Storing tabs for client " + tabsRecord.guid); - if (!isActive()) { - delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); - return; - } - if (tabsRecord.guid == null) { - delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid); - return; - } - - try { - // This is nice and easy: we *always* store. - final String[] selectionArgs = new String[] { tabsRecord.guid }; - if (tabsRecord.deleted) { - try { - Logger.debug(LOG_TAG, "Clearing entry for client " + tabsRecord.guid); - clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, - CLIENT_GUID_IS, - selectionArgs); - delegate.onRecordStoreSucceeded(record.guid); - } catch (Exception e) { - delegate.onRecordStoreFailed(e, record.guid); - } - return; - } - - // If it exists, update the client record; otherwise insert. - final ContentValues clientsCV = tabsRecord.getClientsContentValues(); - - final ClientRecord clientRecord = clientsDatabase.fetchClient(tabsRecord.guid); - if (null != clientRecord) { - // Null is an acceptable device type. - clientsCV.put(Clients.DEVICE_TYPE, clientRecord.type); - } - - Logger.debug(LOG_TAG, "Updating clients provider."); - final int updated = clientsProvider.update(BrowserContractHelpers.CLIENTS_CONTENT_URI, - clientsCV, - CLIENT_GUID_IS, - selectionArgs); - if (0 == updated) { - clientsProvider.insert(BrowserContractHelpers.CLIENTS_CONTENT_URI, clientsCV); - } - - // Now insert tabs. - final ContentValues[] tabsArray = tabsRecord.getTabsContentValues(); - Logger.debug(LOG_TAG, "Inserting " + tabsArray.length + " tabs for client " + tabsRecord.guid); - - tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, selectionArgs); - final int inserted = tabsProvider.bulkInsert(BrowserContractHelpers.TABS_CONTENT_URI, tabsArray); - Logger.trace(LOG_TAG, "Inserted: " + inserted); - - delegate.onRecordStoreSucceeded(record.guid); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Error storing tabs.", e); - delegate.onRecordStoreFailed(e, record.guid); - } - } - }; - - storeWorkQueue.execute(command); - } - - @Override - public void wipe(RepositorySessionWipeDelegate delegate) { - try { - tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, null, null); - clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, null, null); - } catch (RemoteException e) { - Logger.warn(LOG_TAG, "Got RemoteException in wipe.", e); - delegate.onWipeFailed(e); - return; - } - delegate.onWipeSucceeded(); - } - } - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - try { - final FennecTabsRepositorySession session = new FennecTabsRepositorySession(this, context); - delegate.onSessionCreated(session); - } catch (Exception e) { - delegate.onSessionCreateFailed(e); - } - } - - /** - * Extract a <code>TabsRecord</code> from a cursor. - * <p> - * Caller is responsible for creating and closing cursor. Each row of the - * cursor should be an individual tab record. - * <p> - * The extracted tabs record has the given client GUID and client name. - * - * @param cursor - * to inspect. - * @param clientGuid - * returned tabs record will have this client GUID. - * @param clientName - * returned tabs record will have this client name. - * @return <code>TabsRecord</code> instance. - */ - public static TabsRecord tabsRecordFromCursor(final Cursor cursor, final String clientGuid, final String clientName) { - final String collection = "tabs"; - final TabsRecord record = new TabsRecord(clientGuid, collection, 0, false); - record.tabs = new ArrayList<Tab>(); - record.clientName = clientName; - - record.androidID = -1; - record.deleted = false; - - record.lastModified = 0; - - int position = cursor.getPosition(); - try { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - final Tab tab = Tab.fromCursor(cursor); - record.tabs.add(tab); - - if (tab.lastUsed > record.lastModified) { - record.lastModified = tab.lastUsed; - } - - cursor.moveToNext(); - } - } finally { - cursor.moveToPosition(position); - } - - return record; - } - - /** - * Deletes all non-local clients and their associated remote tabs. - */ - public static void deleteNonLocalClientsAndTabs(Context context) { - final String nonLocalClientSelection = BrowserContract.Clients.GUID + " IS NOT NULL"; - - ContentProviderClient clientsProvider = context.getContentResolver() - .acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI); - if (clientsProvider == null) { - Logger.warn(LOG_TAG, "Unable to create clientsProvider!"); - return; - } - - try { - Logger.info(LOG_TAG, "Clearing all non-local clients and their associated remote tabs for default profile."); - clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, nonLocalClientSelection, null); - } catch (RemoteException e) { - Logger.warn(LOG_TAG, "Error while deleting", e); - } finally { - try { - clientsProvider.release(); - } catch (Exception e) { - Logger.warn(LOG_TAG, "Got exception releasing clientsProvider!", e); - } - } - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java deleted file mode 100644 index 9beafa712..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/FormHistoryRepositorySession.java +++ /dev/null @@ -1,723 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Callable; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.DeletedFormHistory; -import org.mozilla.gecko.db.BrowserContract.FormHistory; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.NoContentProviderException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.RecordFilter; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; - -public class FormHistoryRepositorySession extends - StoreTrackingRepositorySession { - public static final String LOG_TAG = "FormHistoryRepoSess"; - - /** - * Number of records to insert in one batch. - */ - public static final int INSERT_ITEM_THRESHOLD = 200; - - private static final Uri FORM_HISTORY_CONTENT_URI = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI; - private static final Uri DELETED_FORM_HISTORY_CONTENT_URI = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI; - - public static class FormHistoryRepository extends Repository { - - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - try { - final FormHistoryRepositorySession session = new FormHistoryRepositorySession(this, context); - delegate.onSessionCreated(session); - } catch (Exception e) { - delegate.onSessionCreateFailed(e); - } - } - } - - protected final ContentProviderClient formsProvider; - protected final RepoUtils.QueryHelper regularHelper; - protected final RepoUtils.QueryHelper deletedHelper; - - /** - * Acquire the content provider client. - * <p> - * The caller is responsible for releasing the client. - * - * @param context The application context. - * @return The <code>ContentProviderClient</code>. - * @throws NoContentProviderException - */ - public static ContentProviderClient acquireContentProvider(final Context context) - throws NoContentProviderException { - Uri uri = BrowserContract.FORM_HISTORY_AUTHORITY_URI; - ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(uri); - if (client == null) { - throw new NoContentProviderException(uri); - } - return client; - } - - protected void releaseProviders() { - try { - if (formsProvider != null) { - formsProvider.release(); - } - } catch (Exception e) { - } - } - - // Only used for testing. - public ContentProviderClient getFormsProvider() { - return formsProvider; - } - - public FormHistoryRepositorySession(Repository repository, Context context) - throws NoContentProviderException { - super(repository); - formsProvider = acquireContentProvider(context); - regularHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI, LOG_TAG); - deletedHelper = new RepoUtils.QueryHelper(context, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI, LOG_TAG); - } - - @Override - public void abort() { - releaseProviders(); - super.abort(); - } - - @Override - public void finish(final RepositorySessionFinishDelegate delegate) - throws InactiveSessionException { - releaseProviders(); - super.finish(delegate); - } - - protected static final String[] GUID_COLUMNS = new String[] { FormHistory.GUID }; - - @Override - public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) { - Runnable command = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onGuidsSinceFailed(new InactiveSessionException(null)); - return; - } - - ArrayList<String> guids = new ArrayList<String>(); - - final long sharedEnd = now(); - Cursor cur = null; - try { - cur = regularHelper.safeQuery(formsProvider, "", GUID_COLUMNS, regularBetween(timestamp, sharedEnd), null, null); - cur.moveToFirst(); - while (!cur.isAfterLast()) { - guids.add(cur.getString(0)); - cur.moveToNext(); - } - } catch (RemoteException | NullCursorException e) { - delegate.onGuidsSinceFailed(e); - return; - } finally { - if (cur != null) { - cur.close(); - } - } - - try { - cur = deletedHelper.safeQuery(formsProvider, "", GUID_COLUMNS, deletedBetween(timestamp, sharedEnd), null, null); - cur.moveToFirst(); - while (!cur.isAfterLast()) { - guids.add(cur.getString(0)); - cur.moveToNext(); - } - } catch (RemoteException | NullCursorException e) { - delegate.onGuidsSinceFailed(e); - return; - } finally { - if (cur != null) { - cur.close(); - } - } - - String guidsArray[] = guids.toArray(new String[guids.size()]); - delegate.onGuidsSinceSucceeded(guidsArray); - } - }; - delegateQueue.execute(command); - } - - protected static FormHistoryRecord retrieveDuringFetch(final Cursor cursor) { - // A simple and efficient way to distinguish two tables. - if (cursor.getColumnCount() == BrowserContractHelpers.FormHistoryColumns.length) { - return formHistoryRecordFromCursor(cursor); - } else { - return deletedFormHistoryRecordFromCursor(cursor); - } - } - - protected static FormHistoryRecord formHistoryRecordFromCursor(final Cursor cursor) { - String guid = RepoUtils.getStringFromCursor(cursor, FormHistory.GUID); - String collection = "forms"; - FormHistoryRecord record = new FormHistoryRecord(guid, collection, 0, false); - - record.fieldName = RepoUtils.getStringFromCursor(cursor, FormHistory.FIELD_NAME); - record.fieldValue = RepoUtils.getStringFromCursor(cursor, FormHistory.VALUE); - record.androidID = RepoUtils.getLongFromCursor(cursor, FormHistory.ID); - record.lastModified = RepoUtils.getLongFromCursor(cursor, FormHistory.FIRST_USED) / 1000; // Convert microseconds to milliseconds. - record.deleted = false; - - record.log(LOG_TAG); - return record; - } - - protected static FormHistoryRecord deletedFormHistoryRecordFromCursor(final Cursor cursor) { - String guid = RepoUtils.getStringFromCursor(cursor, DeletedFormHistory.GUID); - String collection = "forms"; - FormHistoryRecord record = new FormHistoryRecord(guid, collection, 0, false); - - record.guid = RepoUtils.getStringFromCursor(cursor, DeletedFormHistory.GUID); - record.androidID = RepoUtils.getLongFromCursor(cursor, DeletedFormHistory.ID); - record.lastModified = RepoUtils.getLongFromCursor(cursor, DeletedFormHistory.TIME_DELETED); - record.deleted = true; - - record.log(LOG_TAG); - return record; - } - - protected static void fetchFromCursor(final Cursor cursor, final RecordFilter filter, final RepositorySessionFetchRecordsDelegate delegate) - throws NullCursorException { - Logger.debug(LOG_TAG, "Fetch from cursor"); - if (cursor == null) { - throw new NullCursorException(null); - } - try { - if (!cursor.moveToFirst()) { - return; - } - while (!cursor.isAfterLast()) { - Record r = retrieveDuringFetch(cursor); - if (r != null) { - if (filter == null || !filter.excludeRecord(r)) { - Logger.trace(LOG_TAG, "Processing record " + r.guid); - delegate.onFetchedRecord(r); - } else { - Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid); - } - } - cursor.moveToNext(); - } - } finally { - Logger.trace(LOG_TAG, "Closing cursor after fetch."); - cursor.close(); - } - } - - protected void fetchHelper(final RepositorySessionFetchRecordsDelegate delegate, final long end, final List<Callable<Cursor>> cursorCallables) { - if (this.storeTracker == null) { - throw new IllegalStateException("Store tracker not yet initialized!"); - } - - final RecordFilter filter = this.storeTracker.getFilter(); - - Runnable command = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - for (Callable<Cursor> cursorCallable : cursorCallables) { - Cursor cursor = null; - try { - cursor = cursorCallable.call(); - fetchFromCursor(cursor, filter, delegate); // Closes cursor. - } catch (Exception e) { - Logger.warn(LOG_TAG, "Exception during fetchHelper", e); - delegate.onFetchFailed(e, null); - return; - } - } - - delegate.onFetchCompleted(end); - } - }; - - delegateQueue.execute(command); - } - - protected static String regularBetween(long start, long end) { - return FormHistory.FIRST_USED + " >= " + Long.toString(1000 * start) + " AND " + - FormHistory.FIRST_USED + " <= " + Long.toString(1000 * end); // Microseconds. - } - - protected static String deletedBetween(long start, long end) { - return DeletedFormHistory.TIME_DELETED + " >= " + Long.toString(start) + " AND " + - DeletedFormHistory.TIME_DELETED + " <= " + Long.toString(end); // Milliseconds. - } - - @Override - public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) { - Logger.trace(LOG_TAG, "Running fetchSince(" + timestamp + ")."); - - /* - * We need to be careful about the timestamp we complete the fetch with. If - * the first cursor Callable takes a year, then the second could return - * records long after the first was kicked off. To protect against this, we - * set an end point and bound our search. - */ - final long sharedEnd = now(); - - Callable<Cursor> regularCallable = new Callable<Cursor>() { - @Override - public Cursor call() throws Exception { - return regularHelper.safeQuery(formsProvider, ".fetchSince(regular)", null, regularBetween(timestamp, sharedEnd), null, null); - } - }; - - Callable<Cursor> deletedCallable = new Callable<Cursor>() { - @Override - public Cursor call() throws Exception { - return deletedHelper.safeQuery(formsProvider, ".fetchSince(deleted)", null, deletedBetween(timestamp, sharedEnd), null, null); - } - }; - - @SuppressWarnings("unchecked") - List<Callable<Cursor>> callableCursors = Arrays.asList(regularCallable, deletedCallable); - - fetchHelper(delegate, sharedEnd, callableCursors); - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - Logger.trace(LOG_TAG, "Running fetchAll."); - fetchSince(0, delegate); - } - - @Override - public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) { - Logger.trace(LOG_TAG, "Running fetch."); - - final long sharedEnd = now(); - final String where = RepoUtils.computeSQLInClause(guids.length, FormHistory.GUID); - - Callable<Cursor> regularCallable = new Callable<Cursor>() { - @Override - public Cursor call() throws Exception { - String regularWhere = where + " AND " + FormHistory.FIRST_USED + " <= " + Long.toString(1000 * sharedEnd); // Microseconds. - return regularHelper.safeQuery(formsProvider, ".fetch(regular)", null, regularWhere, guids, null); - } - }; - - Callable<Cursor> deletedCallable = new Callable<Cursor>() { - @Override - public Cursor call() throws Exception { - String deletedWhere = where + " AND " + DeletedFormHistory.TIME_DELETED + " <= " + Long.toString(sharedEnd); // Milliseconds. - return deletedHelper.safeQuery(formsProvider, ".fetch(deleted)", null, deletedWhere, guids, null); - } - }; - - @SuppressWarnings("unchecked") - List<Callable<Cursor>> callableCursors = Arrays.asList(regularCallable, deletedCallable); - - fetchHelper(delegate, sharedEnd, callableCursors); - } - - protected static final String GUID_IS = FormHistory.GUID + " = ?"; - - protected Record findExistingRecordByGuid(String guid) - throws RemoteException, NullCursorException { - Cursor cursor = null; - try { - cursor = regularHelper.safeQuery(formsProvider, ".findExistingRecordByGuid(regular)", - null, GUID_IS, new String[] { guid }, null); - if (cursor.moveToFirst()) { - return formHistoryRecordFromCursor(cursor); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - try { - cursor = deletedHelper.safeQuery(formsProvider, ".findExistingRecordByGuid(deleted)", - null, GUID_IS, new String[] { guid }, null); - if (cursor.moveToFirst()) { - return deletedFormHistoryRecordFromCursor(cursor); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - return null; - } - - protected Record findExistingRecordByPayload(Record rawRecord) - throws RemoteException, NullCursorException { - if (!rawRecord.deleted) { - FormHistoryRecord record = (FormHistoryRecord) rawRecord; - Cursor cursor = null; - try { - String where = FormHistory.FIELD_NAME + " = ? AND " + FormHistory.VALUE + " = ?"; - cursor = regularHelper.safeQuery(formsProvider, ".findExistingRecordByPayload", - null, where, new String[] { record.fieldName, record.fieldValue }, null); - if (cursor.moveToFirst()) { - return formHistoryRecordFromCursor(cursor); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - return null; - } - - /** - * Called when a record with locally known GUID has been reported deleted by - * the server. - * <p> - * We purge the record's GUID from the regular and deleted tables. - * - * @param existingRecord - * The local <code>Record</code> to replace. - * @throws RemoteException - */ - protected void deleteExistingRecord(Record existingRecord) throws RemoteException { - if (existingRecord.deleted) { - formsProvider.delete(DELETED_FORM_HISTORY_CONTENT_URI, GUID_IS, new String[] { existingRecord.guid }); - return; - } - formsProvider.delete(FORM_HISTORY_CONTENT_URI, GUID_IS, new String[] { existingRecord.guid }); - } - - protected static ContentValues contentValuesForRegularRecord(Record rawRecord) { - if (rawRecord.deleted) { - throw new IllegalArgumentException("Deleted record passed to insertNewRegularRecord."); - } - - FormHistoryRecord record = (FormHistoryRecord) rawRecord; - ContentValues cv = new ContentValues(); - cv.put(FormHistory.GUID, record.guid); - cv.put(FormHistory.FIELD_NAME, record.fieldName); - cv.put(FormHistory.VALUE, record.fieldValue); - cv.put(FormHistory.FIRST_USED, 1000 * record.lastModified); // Microseconds. - return cv; - } - - protected final Object recordsBufferMonitor = new Object(); - protected ArrayList<ContentValues> recordsBuffer = new ArrayList<ContentValues>(); - - protected void enqueueRegularRecord(Record record) { - synchronized (recordsBufferMonitor) { - if (recordsBuffer.size() >= INSERT_ITEM_THRESHOLD) { - // Insert the existing contents, then enqueue. - try { - flushInsertQueue(); - } catch (Exception e) { - delegate.onRecordStoreFailed(e, record.guid); - return; - } - } - // Store the ContentValues, rather than the record. - recordsBuffer.add(contentValuesForRegularRecord(record)); - } - } - - // Should always be called from storeWorkQueue. - protected void flushInsertQueue() throws RemoteException { - synchronized (recordsBufferMonitor) { - if (recordsBuffer.size() > 0) { - final ContentValues[] outgoing = recordsBuffer.toArray(new ContentValues[recordsBuffer.size()]); - recordsBuffer = new ArrayList<ContentValues>(); - - if (outgoing == null || outgoing.length == 0) { - Logger.debug(LOG_TAG, "No form history items to insert; returning immediately."); - return; - } - - long before = System.currentTimeMillis(); - formsProvider.bulkInsert(FORM_HISTORY_CONTENT_URI, outgoing); - long after = System.currentTimeMillis(); - Logger.debug(LOG_TAG, "Inserted " + outgoing.length + " form history items in (" + (after - before) + " milliseconds)."); - } - } - } - - @Override - public void storeDone() { - Runnable command = new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Checking for residual form history items to insert."); - try { - synchronized (recordsBufferMonitor) { - flushInsertQueue(); - } - storeDone(now()); - } catch (Exception e) { - // XXX TODO - delegate.onRecordStoreFailed(e, null); - } - } - }; - storeWorkQueue.execute(command); - } - - /** - * Called when a regular record with locally unknown GUID has been fetched - * from the server. - * <p> - * Since the record is regular, we insert it into the regular table. - * - * @param record The regular <code>Record</code> from the server. - * @throws RemoteException - */ - protected void insertNewRegularRecord(Record record) - throws RemoteException { - enqueueRegularRecord(record); - } - - /** - * Called when a regular record with has been fetched from the server and - * should replace an existing record. - * <p> - * We delete the existing record entirely, and then insert the new record into - * the regular table. - * - * @param toStore - * The regular <code>Record</code> from the server. - * @param existingRecord - * The local <code>Record</code> to replace. - * @throws RemoteException - */ - protected void replaceExistingRecordWithRegularRecord(Record toStore, Record existingRecord) - throws RemoteException { - if (existingRecord.deleted) { - // Need two database operations -- purge from deleted table, insert into regular table. - deleteExistingRecord(existingRecord); - insertNewRegularRecord(toStore); - return; - } - - final ContentValues cv = contentValuesForRegularRecord(toStore); - int updated = formsProvider.update(FORM_HISTORY_CONTENT_URI, cv, GUID_IS, new String[] { existingRecord.guid }); - if (updated != 1) { - Logger.warn(LOG_TAG, "Expected to update 1 record with guid " + existingRecord.guid + " but updated " + updated + " records."); - } - } - - @Override - public void store(Record rawRecord) throws NoStoreDelegateException { - if (delegate == null) { - Logger.warn(LOG_TAG, "No store delegate."); - throw new NoStoreDelegateException(); - } - if (rawRecord == null) { - Logger.error(LOG_TAG, "Record sent to store was null"); - throw new IllegalArgumentException("Null record passed to FormHistoryRepositorySession.store()."); - } - if (!(rawRecord instanceof FormHistoryRecord)) { - Logger.error(LOG_TAG, "Can't store anything but a FormHistoryRecord"); - throw new IllegalArgumentException("Non-FormHistoryRecord passed to FormHistoryRepositorySession.store()."); - } - final FormHistoryRecord record = (FormHistoryRecord) rawRecord; - - Runnable command = new Runnable() { - @Override - public void run() { - if (!isActive()) { - Logger.warn(LOG_TAG, "FormHistoryRepositorySession is inactive. Store failing."); - delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); - return; - } - - // TODO: lift these into the session. - // Temporary: this matches prior syncing semantics, in which only - // the relationship between the local and remote record is considered. - // In the future we'll track these two timestamps and use them to - // determine which records have changed, and thus process incoming - // records more efficiently. - long lastLocalRetrieval = 0; // lastSyncTimestamp? - long lastRemoteRetrieval = 0; // TODO: adjust for clock skew. - boolean remotelyModified = record.lastModified > lastRemoteRetrieval; - - Record existingRecord; - try { - // GUID matching only: deleted records don't have a payload with which to search. - existingRecord = findExistingRecordByGuid(record.guid); - if (record.deleted) { - if (existingRecord == null) { - // We're done. Don't bother with a callback. That can change later - // if we want it to. - Logger.trace(LOG_TAG, "Incoming record " + record.guid + " is deleted, and no local version. Bye!"); - return; - } - - if (existingRecord.deleted) { - Logger.trace(LOG_TAG, "Local record already deleted. Purging local."); - deleteExistingRecord(existingRecord); - return; - } - - // Which one wins? - if (!remotelyModified) { - Logger.trace(LOG_TAG, "Ignoring deleted record from the past."); - return; - } - - boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval; - if (!locallyModified) { - Logger.trace(LOG_TAG, "Remote modified, local not. Deleting."); - deleteExistingRecord(existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - Logger.trace(LOG_TAG, "Both local and remote records have been modified."); - if (record.lastModified > existingRecord.lastModified) { - Logger.trace(LOG_TAG, "Remote is newer, and deleted. Purging local."); - deleteExistingRecord(existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring."); - if (!locallyModified) { - Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!"); - // Ensure that this is tracked for upload. - } - return; - } - // End deletion logic. - - // Now we're processing a non-deleted incoming record. - if (existingRecord == null) { - Logger.trace(LOG_TAG, "Looking up match for record " + record.guid); - existingRecord = findExistingRecordByPayload(record); - } - - if (existingRecord == null) { - // The record is new. - Logger.trace(LOG_TAG, "No match. Inserting."); - insertNewRegularRecord(record); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - // We found a local duplicate. - Logger.trace(LOG_TAG, "Incoming record " + record.guid + " dupes to local record " + existingRecord.guid); - - if (!RepoUtils.stringsEqual(record.guid, existingRecord.guid)) { - // We found a local record that does NOT have the same GUID -- keep the server's version. - Logger.trace(LOG_TAG, "Remote guid different from local guid. Storing to keep remote guid."); - replaceExistingRecordWithRegularRecord(record, existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - // We found a local record that does have the same GUID -- check modification times. - boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval; - if (!locallyModified) { - Logger.trace(LOG_TAG, "Remote modified, local not. Storing."); - replaceExistingRecordWithRegularRecord(record, existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - Logger.trace(LOG_TAG, "Both local and remote records have been modified."); - if (record.lastModified > existingRecord.lastModified) { - Logger.trace(LOG_TAG, "Remote is newer, and not deleted. Storing."); - replaceExistingRecordWithRegularRecord(record, existingRecord); - trackRecord(record); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - - Logger.trace(LOG_TAG, "Remote is older, local is not deleted. Ignoring."); - if (!locallyModified) { - Logger.warn(LOG_TAG, "Inconsistency: old remote record is not deleted, but local record not modified!"); - } - return; - } catch (Exception e) { - Logger.error(LOG_TAG, "Store failed for " + record.guid, e); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - } - }; - - storeWorkQueue.execute(command); - } - - /** - * Purge all data from the underlying databases. - */ - public static void purgeDatabases(ContentProviderClient formsProvider) - throws RemoteException { - formsProvider.delete(FORM_HISTORY_CONTENT_URI, null, null); - formsProvider.delete(DELETED_FORM_HISTORY_CONTENT_URI, null, null); - } - - @Override - public void wipe(final RepositorySessionWipeDelegate delegate) { - Runnable command = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onWipeFailed(new InactiveSessionException(null)); - return; - } - - try { - Logger.debug(LOG_TAG, "Wiping form history and deleted form history..."); - purgeDatabases(formsProvider); - Logger.debug(LOG_TAG, "Wiping form history and deleted form history... DONE"); - } catch (Exception e) { - delegate.onWipeFailed(e); - return; - } - - delegate.onWipeSucceeded(); - } - }; - storeWorkQueue.execute(command); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java deleted file mode 100644 index f7b7416df..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/PasswordsRepositorySession.java +++ /dev/null @@ -1,725 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.DeletedColumns; -import org.mozilla.gecko.db.BrowserContract.DeletedPasswords; -import org.mozilla.gecko.db.BrowserContract.Passwords; -import org.mozilla.gecko.sync.repositories.InactiveSessionException; -import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.RecordFilter; -import org.mozilla.gecko.sync.repositories.Repository; -import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession; -import org.mozilla.gecko.sync.repositories.android.RepoUtils.QueryHelper; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; -import org.mozilla.gecko.sync.repositories.domain.PasswordRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import android.content.ContentProviderClient; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; - -public class PasswordsRepositorySession extends - StoreTrackingRepositorySession { - - public static class PasswordsRepository extends Repository { - @Override - public void createSession(RepositorySessionCreationDelegate delegate, - Context context) { - PasswordsRepositorySession session = new PasswordsRepositorySession(PasswordsRepository.this, context); - final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate(); - deferredCreationDelegate.onSessionCreated(session); - } - } - - private static final String LOG_TAG = "PasswordsRepoSession"; - private static final String COLLECTION = "passwords"; - - private final RepoUtils.QueryHelper passwordsHelper; - private final RepoUtils.QueryHelper deletedPasswordsHelper; - private final ContentProviderClient passwordsProvider; - - private final Context context; - - public PasswordsRepositorySession(Repository repository, Context context) { - super(repository); - this.context = context; - this.passwordsHelper = new QueryHelper(context, BrowserContractHelpers.PASSWORDS_CONTENT_URI, LOG_TAG); - this.deletedPasswordsHelper = new QueryHelper(context, BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, LOG_TAG); - this.passwordsProvider = context.getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI); - } - - private static final String[] GUID_COLS = new String[] { Passwords.GUID }; - private static final String[] DELETED_GUID_COLS = new String[] { DeletedColumns.GUID }; - - private static final String WHERE_GUID_IS = Passwords.GUID + " = ?"; - private static final String WHERE_DELETED_GUID_IS = DeletedPasswords.GUID + " = ?"; - - @Override - public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) { - final Runnable guidsSinceRunnable = new Runnable() { - @Override - public void run() { - - if (!isActive()) { - delegate.onGuidsSinceFailed(new InactiveSessionException(null)); - return; - } - - // Checks succeeded, now get GUIDs. - final List<String> guids = new ArrayList<String>(); - try { - Logger.debug(LOG_TAG, "Fetching guidsSince from data table."); - final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", GUID_COLS, dateModifiedWhere(timestamp), null, null); - try { - if (data.moveToFirst()) { - while (!data.isAfterLast()) { - guids.add(RepoUtils.getStringFromCursor(data, Passwords.GUID)); - data.moveToNext(); - } - } - } finally { - data.close(); - } - - // Fetch guids from deleted table. - Logger.debug(LOG_TAG, "Fetching guidsSince from deleted table."); - final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", DELETED_GUID_COLS, dateModifiedWhereDeleted(timestamp), null, null); - try { - if (deleted.moveToFirst()) { - while (!deleted.isAfterLast()) { - guids.add(RepoUtils.getStringFromCursor(deleted, DeletedColumns.GUID)); - deleted.moveToNext(); - } - } - } finally { - deleted.close(); - } - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception in fetch."); - delegate.onGuidsSinceFailed(e); - return; - } - String[] guidStrings = new String[guids.size()]; - delegate.onGuidsSinceSucceeded(guids.toArray(guidStrings)); - } - }; - - delegateQueue.execute(guidsSinceRunnable); - } - - @Override - public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) { - final RecordFilter filter = this.storeTracker.getFilter(); - final Runnable fetchSinceRunnable = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - final long end = now(); - try { - // Fetch from data table. - Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetchSince", - getAllColumns(), - dateModifiedWhere(timestamp), - null, null); - if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) { - return; - } - - // Fetch from deleted table. - Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetchSince", - getAllDeletedColumns(), - dateModifiedWhereDeleted(timestamp), - null, null); - if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) { - return; - } - - // Success! - try { - delegate.onFetchCompleted(end); - } catch (Exception e) { - Logger.error(LOG_TAG, "Delegate fetch completed callback failed.", e); - // Don't call failure callback. - return; - } - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception in fetch."); - delegate.onFetchFailed(e, null); - } - } - }; - - delegateQueue.execute(fetchSinceRunnable); - } - - @Override - public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) { - if (guids == null || guids.length < 1) { - Logger.error(LOG_TAG, "No guids to be fetched."); - final long end = now(); - delegateQueue.execute(new Runnable() { - @Override - public void run() { - delegate.onFetchCompleted(end); - } - }); - return; - } - - // Checks succeeded, now fetch. - final RecordFilter filter = this.storeTracker.getFilter(); - final Runnable fetchRunnable = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onFetchFailed(new InactiveSessionException(null), null); - return; - } - - final long end = now(); - final String where = RepoUtils.computeSQLInClause(guids.length, "guid"); - Logger.trace(LOG_TAG, "Fetch guids where: " + where); - - try { - // Fetch records from data table. - Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetch", - getAllColumns(), - where, guids, null); - if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) { - return; - } - - // Fetch records from deleted table. - Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetch", - getAllDeletedColumns(), - where, guids, null); - if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) { - return; - } - - delegate.onFetchCompleted(end); - - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception in fetch."); - delegate.onFetchFailed(e, null); - } - } - }; - - delegateQueue.execute(fetchRunnable); - } - - @Override - public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) { - fetchSince(0, delegate); - } - - @Override - public void store(final Record record) throws NoStoreDelegateException { - if (delegate == null) { - Logger.error(LOG_TAG, "No store delegate."); - throw new NoStoreDelegateException(); - } - if (record == null) { - Logger.error(LOG_TAG, "Record sent to store was null."); - throw new IllegalArgumentException("Null record passed to PasswordsRepositorySession.store()."); - } - if (!(record instanceof PasswordRecord)) { - Logger.error(LOG_TAG, "Can't store anything but a PasswordRecord."); - throw new IllegalArgumentException("Non-PasswordRecord passed to PasswordsRepositorySession.store()."); - } - - final PasswordRecord remoteRecord = (PasswordRecord) record; - - final Runnable storeRunnable = new Runnable() { - @Override - public void run() { - if (!isActive()) { - Logger.warn(LOG_TAG, "RepositorySession is inactive. Store failing."); - delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); - return; - } - - final String guid = remoteRecord.guid; - if (guid == null) { - delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid); - return; - } - - PasswordRecord existingRecord; - try { - existingRecord = retrieveByGUID(guid); - } catch (NullCursorException | RemoteException e) { - // Indicates a serious problem. - delegate.onRecordStoreFailed(e, record.guid); - return; - } - - long lastLocalRetrieval = 0; // lastSyncTimestamp? - long lastRemoteRetrieval = 0; // TODO: adjust for clock skew. - boolean remotelyModified = remoteRecord.lastModified > lastRemoteRetrieval; - - // Check deleted state first. - if (remoteRecord.deleted) { - if (existingRecord == null) { - // Do nothing, record does not exist anyways. - Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " is deleted, and no local version."); - return; - } - - if (existingRecord.deleted) { - // Record is already tracked as deleted. Delete from local. - storeRecordDeletion(existingRecord); // different from ABRepoSess. - Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " and local are both deleted."); - return; - } - - // Which one wins? - if (!remotelyModified) { - trace("Ignoring deleted record from the past."); - return; - } - - boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval; - if (!locallyModified) { - trace("Remote modified, local not. Deleting."); - storeRecordDeletion(remoteRecord); - return; - } - - trace("Both local and remote records have been modified."); - if (remoteRecord.lastModified > existingRecord.lastModified) { - trace("Remote is newer, and deleted. Deleting local."); - storeRecordDeletion(remoteRecord); - return; - } - - trace("Remote is older, local is not deleted. Ignoring."); - if (!locallyModified) { - Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!"); - // Ensure that this is tracked for upload. - } - return; - } - // End deletion logic. - - // Validate the incoming record. - if (!remoteRecord.isValid()) { - Logger.warn(LOG_TAG, "Incoming record is invalid. Reporting store failed."); - delegate.onRecordStoreFailed(new RuntimeException("Can't store invalid password record."), record.guid); - return; - } - - // Now we're processing a non-deleted incoming record. - if (existingRecord == null) { - trace("Looking up match for record " + remoteRecord.guid); - try { - existingRecord = findExistingRecord(remoteRecord); - } catch (RemoteException e) { - Logger.error(LOG_TAG, "Remote exception in findExistingRecord."); - delegate.onRecordStoreFailed(e, record.guid); - } catch (NullCursorException e) { - Logger.error(LOG_TAG, "Null cursor in findExistingRecord."); - delegate.onRecordStoreFailed(e, record.guid); - } - } - - if (existingRecord == null) { - // The record is new. - trace("No match. Inserting."); - Logger.debug(LOG_TAG, "Didn't find matching record. Inserting."); - Record inserted = null; - try { - inserted = insert(remoteRecord); - } catch (RemoteException e) { - Logger.debug(LOG_TAG, "Record insert caused a RemoteException."); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - trackRecord(inserted); - delegate.onRecordStoreSucceeded(inserted.guid); - return; - } - - // We found a local dupe. - trace("Incoming record " + remoteRecord.guid + " dupes to local record " + existingRecord.guid); - Logger.debug(LOG_TAG, "remote " + remoteRecord.guid + " dupes to " + existingRecord.guid); - - if (existingRecord.deleted && existingRecord.lastModified > remoteRecord.lastModified) { - Logger.debug(LOG_TAG, "Local deletion is newer, not storing remote record."); - return; - } - - Record toStore = reconcileRecords(remoteRecord, existingRecord, lastRemoteRetrieval, lastLocalRetrieval); - if (toStore == null) { - Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record."); - return; - } - - // TODO: pass in timestamps? - Logger.debug(LOG_TAG, "Replacing " + existingRecord.guid + " with record " + toStore.guid); - Record replaced = null; - try { - replaced = replace(existingRecord, toStore); - } catch (RemoteException e) { - Logger.debug(LOG_TAG, "Record replace caused a RemoteException."); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - - // Note that we don't track records here; deciding that is the job - // of reconcileRecords. - Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid + - "(" + replaced.androidID + ")"); - delegate.onRecordStoreSucceeded(record.guid); - return; - } - }; - storeWorkQueue.execute(storeRunnable); - } - - @Override - public void wipe(final RepositorySessionWipeDelegate delegate) { - Logger.info(LOG_TAG, "Wiping " + BrowserContractHelpers.PASSWORDS_CONTENT_URI + ", " + BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI); - - Runnable wipeRunnable = new Runnable() { - @Override - public void run() { - if (!isActive()) { - delegate.onWipeFailed(new InactiveSessionException(null)); - return; - } - - // Wipe both data and deleted. - try { - context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null); - context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null); - } catch (Exception e) { - delegate.onWipeFailed(e); - return; - } - delegate.onWipeSucceeded(); - } - }; - storeWorkQueue.execute(wipeRunnable); - } - - @Override - public void abort() { - passwordsProvider.release(); - super.abort(); - } - - @Override - public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException { - passwordsProvider.release(); - super.finish(delegate); - } - - public void deleteGUID(String guid) throws RemoteException { - final String[] args = new String[] { guid }; - - int deleted = passwordsProvider.delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, WHERE_GUID_IS, args) + - passwordsProvider.delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, WHERE_DELETED_GUID_IS, args); - if (deleted == 1) { - return; - } - Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid); - } - - /** - * Insert record and return the record with its updated androidId set. - * - * @param record the record to insert. - * @return updated record. - * @throws RemoteException - */ - public PasswordRecord insert(PasswordRecord record) throws RemoteException { - record.timePasswordChanged = now(); - // TODO: are these necessary for Fennec autocomplete? - // record.timesUsed = 1; - // record.timeLastUsed = now(); - ContentValues cv = getContentValues(record); - Uri insertedUri = passwordsProvider.insert(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv); - if (insertedUri == null) { - throw new RemoteException(); // Not much to be done here, save throw. - } - record.androidID = ContentUris.parseId(insertedUri); - return record; - } - - public Record replace(Record origRecord, Record newRecord) throws RemoteException { - PasswordRecord newPasswordRecord = (PasswordRecord) newRecord; - PasswordRecord origPasswordRecord = (PasswordRecord) origRecord; - propagateTimes(newPasswordRecord, origPasswordRecord); - ContentValues cv = getContentValues(newPasswordRecord); - - final String[] args = new String[] { origRecord.guid }; - - if (origRecord.deleted) { - // Purge from deleted table. - deleteGUID(origRecord.guid); - insert(newPasswordRecord); - } else { - int updated = context.getContentResolver().update(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv, WHERE_GUID_IS, args); - if (updated != 1) { - Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + origPasswordRecord.guid); - } - } - - return newRecord; - } - - // When replacing a record, propagate the times. - private static void propagateTimes(PasswordRecord toRecord, PasswordRecord fromRecord) { - toRecord.timePasswordChanged = now(); - toRecord.timeCreated = fromRecord.timeCreated; - toRecord.timeLastUsed = fromRecord.timeLastUsed; - toRecord.timesUsed = fromRecord.timesUsed; - } - - private static String[] getAllColumns() { - return BrowserContractHelpers.PasswordColumns; - } - - private static String[] getAllDeletedColumns() { - return BrowserContractHelpers.DeletedColumns; - } - - /** - * Constructs the DB query string for entry age for deleted records. - * - * @param timestamp - * @return String DB query string for dates to fetch. - */ - private static String dateModifiedWhereDeleted(long timestamp) { - return DeletedColumns.TIME_DELETED + " >= " + Long.toString(timestamp); - } - - /** - * Constructs the DB query string for entry age for (undeleted) records. - * - * @param timestamp - * @return String DB query string for dates to fetch. - */ - private static String dateModifiedWhere(long timestamp) { - return Passwords.TIME_PASSWORD_CHANGED + " >= " + Long.toString(timestamp); - } - - - /** - * Fetch from the cursor with the given parameters, invoking - * delegate callbacks and closing the cursor. - * Returns true on success, false if failure was signaled. - * - * @param cursor - fetch* cursor. - * @param deleted - * true if using deleted table, false when using data table. - * @param delegate - * FetchRecordsDelegate to process records. - */ - private static boolean fetchAndCloseCursorDeleted(final Cursor cursor, - final boolean deleted, - final RecordFilter filter, - final RepositorySessionFetchRecordsDelegate delegate) { - if (cursor == null) { - return true; - } - - try { - while (cursor.moveToNext()) { - Record r = deleted ? deletedPasswordRecordFromCursor(cursor) : passwordRecordFromCursor(cursor); - if (r != null) { - if (filter == null || !filter.excludeRecord(r)) { - Logger.debug(LOG_TAG, "Processing record " + r.guid); - delegate.onFetchedRecord(r); - } else { - Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid); - } - } - } - } catch (Exception e) { - Logger.error(LOG_TAG, "Exception in fetch."); - delegate.onFetchFailed(e, null); - return false; - } finally { - cursor.close(); - } - - return true; - } - - private PasswordRecord retrieveByGUID(String guid) throws NullCursorException, RemoteException { - final String[] guidArg = new String[] { guid }; - - // Check data table. - final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".store", BrowserContractHelpers.PasswordColumns, WHERE_GUID_IS, guidArg, null); - try { - if (data.moveToFirst()) { - return passwordRecordFromCursor(data); - } - } finally { - data.close(); - } - - // Check deleted table. - final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".retrieveByGuid", BrowserContractHelpers.DeletedColumns, WHERE_DELETED_GUID_IS, guidArg, null); - try { - if (deleted.moveToFirst()) { - return deletedPasswordRecordFromCursor(deleted); - } - } finally { - deleted.close(); - } - - return null; - } - - private static final String WHERE_RECORD_DATA = - Passwords.HOSTNAME + " = ? AND " + - Passwords.HTTP_REALM + " = ? AND " + - Passwords.FORM_SUBMIT_URL + " = ? AND " + - Passwords.USERNAME_FIELD + " = ? AND " + - Passwords.PASSWORD_FIELD + " = ?"; - - private PasswordRecord findExistingRecord(PasswordRecord record) throws NullCursorException, RemoteException { - PasswordRecord foundRecord = null; - Cursor cursor = null; - // Only check the data table. - // We can't encrypt username directly for query, so run a more general query and then filter. - final String[] whereArgs = new String[] { - record.hostname, - record.httpRealm, - record.formSubmitURL, - record.usernameField, - record.passwordField - }; - - try { - cursor = passwordsHelper.safeQuery(passwordsProvider, ".findRecord", getAllColumns(), WHERE_RECORD_DATA, whereArgs, null); - while (cursor.moveToNext()) { - foundRecord = passwordRecordFromCursor(cursor); - - // We don't directly query for username because the - // username/password values are encrypted in the db. - // We don't have the keys for encrypting our query, - // so we run a more general query and then filter - // the returned records for a matching username. - Logger.pii(LOG_TAG, "Checking incoming [" + record.encryptedUsername + "] to [" + foundRecord.encryptedUsername + "]"); - if (record.encryptedUsername.equals(foundRecord.encryptedUsername)) { - Logger.trace(LOG_TAG, "Found matching record: " + foundRecord.guid); - return foundRecord; - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - Logger.debug(LOG_TAG, "No matching records, returning null."); - return null; - } - - private void storeRecordDeletion(Record record) { - try { - deleteGUID(record.guid); - } catch (RemoteException e) { - Logger.error(LOG_TAG, "RemoteException in password delete."); - delegate.onRecordStoreFailed(e, record.guid); - return; - } - delegate.onRecordStoreSucceeded(record.guid); - } - - /** - * Make a PasswordRecord from a Cursor. - * @param cur - * Cursor from query. - * @param deleted - * true if creating a deleted Record, false if otherwise. - * @return - * PasswordRecord populated from Cursor. - */ - private static PasswordRecord passwordRecordFromCursor(Cursor cur) { - if (cur.isAfterLast()) { - return null; - } - String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.GUID); - long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED); - - PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, false); - rec.id = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ID); - rec.hostname = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HOSTNAME); - rec.httpRealm = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HTTP_REALM); - rec.formSubmitURL = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.FORM_SUBMIT_URL); - rec.usernameField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.USERNAME_FIELD); - rec.passwordField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.PASSWORD_FIELD); - rec.encType = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENC_TYPE); - - // TODO decryption of username/password here (Bug 711636) - rec.encryptedUsername = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_USERNAME); - rec.encryptedPassword = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_PASSWORD); - - rec.timeCreated = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_CREATED); - rec.timeLastUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_LAST_USED); - rec.timePasswordChanged = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED); - rec.timesUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIMES_USED); - return rec; - } - - private static PasswordRecord deletedPasswordRecordFromCursor(Cursor cur) { - if (cur.isAfterLast()) { - return null; - } - String guid = RepoUtils.getStringFromCursor(cur, DeletedColumns.GUID); - long lastModified = RepoUtils.getLongFromCursor(cur, DeletedColumns.TIME_DELETED); - PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, true); - rec.androidID = RepoUtils.getLongFromCursor(cur, DeletedColumns.ID); - return rec; - } - - private static ContentValues getContentValues(Record record) { - PasswordRecord rec = (PasswordRecord) record; - - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.Passwords.GUID, rec.guid); - cv.put(BrowserContract.Passwords.HOSTNAME, rec.hostname); - cv.put(BrowserContract.Passwords.HTTP_REALM, rec.httpRealm); - cv.put(BrowserContract.Passwords.FORM_SUBMIT_URL, rec.formSubmitURL); - cv.put(BrowserContract.Passwords.USERNAME_FIELD, rec.usernameField); - cv.put(BrowserContract.Passwords.PASSWORD_FIELD, rec.passwordField); - - // TODO Do encryption of username/password here. Bug 711636 - cv.put(BrowserContract.Passwords.ENC_TYPE, rec.encType); - cv.put(BrowserContract.Passwords.ENCRYPTED_USERNAME, rec.encryptedUsername); - cv.put(BrowserContract.Passwords.ENCRYPTED_PASSWORD, rec.encryptedPassword); - - cv.put(BrowserContract.Passwords.TIME_CREATED, rec.timeCreated); - cv.put(BrowserContract.Passwords.TIME_LAST_USED, rec.timeLastUsed); - cv.put(BrowserContract.Passwords.TIME_PASSWORD_CHANGED, rec.timePasswordChanged); - cv.put(BrowserContract.Passwords.TIMES_USED, rec.timesUsed); - return cv; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java deleted file mode 100644 index 9c29953f8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java +++ /dev/null @@ -1,290 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import android.content.ContentProviderClient; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.RemoteException; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.repositories.NullCursorException; -import org.mozilla.gecko.sync.repositories.domain.ClientRecord; -import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; - -import java.io.IOException; - -public class RepoUtils { - - private static final String LOG_TAG = "RepoUtils"; - - /** - * A helper class for monotonous SQL querying. Does timing and logging, - * offers a utility to throw on a null cursor. - * - * @author rnewman - * - */ - public static class QueryHelper { - private final Context context; - private final Uri uri; - private final String tag; - - public QueryHelper(Context context, Uri uri, String tag) { - this.context = context; - this.uri = uri; - this.tag = tag; - } - - // For ContentProvider queries. - public Cursor safeQuery(String label, String[] projection, - String selection, String[] selectionArgs, String sortOrder) throws NullCursorException { - long queryStart = android.os.SystemClock.uptimeMillis(); - Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder); - return checkAndLogCursor(label, queryStart, c); - } - - public Cursor safeQuery(String[] projection, String selection, String[] selectionArgs, String sortOrder) throws NullCursorException { - return this.safeQuery(null, projection, selection, selectionArgs, sortOrder); - } - - // For ContentProviderClient queries. - public Cursor safeQuery(ContentProviderClient client, String label, String[] projection, - String selection, String[] selectionArgs, String sortOrder) throws NullCursorException, RemoteException { - long queryStart = android.os.SystemClock.uptimeMillis(); - Cursor c = client.query(uri, projection, selection, selectionArgs, sortOrder); - return checkAndLogCursor(label, queryStart, c); - } - - // For SQLiteOpenHelper queries. - public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns, - String selection, String[] selectionArgs, - String groupBy, String having, String orderBy, String limit) throws NullCursorException { - long queryStart = android.os.SystemClock.uptimeMillis(); - Cursor c = db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); - return checkAndLogCursor(label, queryStart, c); - } - - public Cursor safeQuery(SQLiteDatabase db, String label, String table, String[] columns, - String selection, String[] selectionArgs) throws NullCursorException { - return safeQuery(db, label, table, columns, selection, selectionArgs, null, null, null, null); - } - - private Cursor checkAndLogCursor(String label, long queryStart, Cursor c) throws NullCursorException { - long queryEnd = android.os.SystemClock.uptimeMillis(); - String logLabel = (label == null) ? tag : (tag + label); - RepoUtils.queryTimeLogger(logLabel, queryStart, queryEnd); - return checkNullCursor(logLabel, c); - } - - public Cursor checkNullCursor(String logLabel, Cursor cursor) throws NullCursorException { - if (cursor == null) { - Logger.error(tag, "Got null cursor exception in " + logLabel); - throw new NullCursorException(null); - } - return cursor; - } - } - - /** - * This method exists because the behavior of <code>cur.getString()</code> is undefined - * when the value in the database is <code>NULL</code>. - * This method will return <code>null</code> in that case. - */ - public static String optStringFromCursor(final Cursor cur, final String colId) { - final int col = cur.getColumnIndex(colId); - if (cur.isNull(col)) { - return null; - } - return cur.getString(col); - } - - /** - * The behavior of this method when the value in the database is <code>NULL</code> is - * determined by the implementation of the {@link Cursor}. - */ - public static String getStringFromCursor(final Cursor cur, final String colId) { - // TODO: getColumnIndexOrThrow? - // TODO: don't look up columns by name! - return cur.getString(cur.getColumnIndex(colId)); - } - - public static long getLongFromCursor(Cursor cur, String colId) { - return cur.getLong(cur.getColumnIndex(colId)); - } - - public static int getIntFromCursor(Cursor cur, String colId) { - return cur.getInt(cur.getColumnIndex(colId)); - } - - public static JSONArray getJSONArrayFromCursor(Cursor cur, String colId) { - String jsonArrayAsString = getStringFromCursor(cur, colId); - if (jsonArrayAsString == null) { - return new JSONArray(); - } - try { - return ExtendedJSONObject.parseJSONArray(getStringFromCursor(cur, colId)); - } catch (NonArrayJSONException e) { - Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); - return null; - } catch (IOException e) { - Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); - return null; - } - } - - /** - * Return true if the provided URI is non-empty and acceptable to Fennec - * (i.e., not an undesirable scheme). - * - * This code is pilfered from Fennec, which pilfered from Places. - */ - public static boolean isValidHistoryURI(String uri) { - if (uri == null || uri.length() == 0) { - return false; - } - - // First, check the most common cases (HTTP, HTTPS) to avoid most of the work. - if (uri.startsWith("http:") || uri.startsWith("https:")) { - return true; - } - - String scheme = Uri.parse(uri).getScheme(); - if (scheme == null) { - return false; - } - - // Now check for all bad things. - if (scheme.equals("about") || - scheme.equals("imap") || - scheme.equals("news") || - scheme.equals("mailbox") || - scheme.equals("moz-anno") || - scheme.equals("view-source") || - scheme.equals("chrome") || - scheme.equals("resource") || - scheme.equals("data") || - scheme.equals("wyciwyg") || - scheme.equals("javascript")) { - return false; - } - - return true; - } - - /** - * Create a HistoryRecord object from a cursor row. - * - * @return a HistoryRecord, or null if this row would produce - * an invalid record (e.g., with a null URI or no visits). - */ - public static HistoryRecord historyFromMirrorCursor(Cursor cur) { - final String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); - if (guid == null) { - Logger.debug(LOG_TAG, "Skipping history record with null GUID."); - return null; - } - - final String historyURI = getStringFromCursor(cur, BrowserContract.History.URL); - if (!isValidHistoryURI(historyURI)) { - Logger.debug(LOG_TAG, "Skipping history record " + guid + " with unwanted/invalid URI " + historyURI); - return null; - } - - final long visitCount = getLongFromCursor(cur, BrowserContract.History.VISITS); - if (visitCount <= 0) { - Logger.debug(LOG_TAG, "Skipping history record " + guid + " with <= 0 visit count."); - return null; - } - - final String collection = "history"; - final long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED); - final boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1; - - final HistoryRecord rec = new HistoryRecord(guid, collection, lastModified, deleted); - - rec.androidID = getLongFromCursor(cur, BrowserContract.History._ID); - rec.fennecDateVisited = getLongFromCursor(cur, BrowserContract.History.DATE_LAST_VISITED); - rec.fennecVisitCount = visitCount; - rec.histURI = historyURI; - rec.title = getStringFromCursor(cur, BrowserContract.History.TITLE); - - return logHistory(rec); - } - - private static HistoryRecord logHistory(HistoryRecord rec) { - try { - Logger.debug(LOG_TAG, "Returning history record " + rec.guid + " (" + rec.androidID + ")"); - Logger.debug(LOG_TAG, "> Visited: " + rec.fennecDateVisited); - Logger.debug(LOG_TAG, "> Visits: " + rec.fennecVisitCount); - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(LOG_TAG, "> Title: " + rec.title); - Logger.pii(LOG_TAG, "> URI: " + rec.histURI); - } - } catch (Exception e) { - Logger.debug(LOG_TAG, "Exception logging history record " + rec, e); - } - return rec; - } - - public static void logClient(ClientRecord rec) { - if (Logger.shouldLogVerbose(LOG_TAG)) { - Logger.trace(LOG_TAG, "Returning client record " + rec.guid + " (" + rec.androidID + ")"); - Logger.trace(LOG_TAG, "Client Name: " + rec.name); - Logger.trace(LOG_TAG, "Client Type: " + rec.type); - Logger.trace(LOG_TAG, "Last Modified: " + rec.lastModified); - Logger.trace(LOG_TAG, "Deleted: " + rec.deleted); - } - } - - public static void queryTimeLogger(String methodCallingQuery, long queryStart, long queryEnd) { - long elapsedTime = queryEnd - queryStart; - Logger.debug(LOG_TAG, "Query timer: " + methodCallingQuery + " took " + elapsedTime + "ms."); - } - - public static boolean stringsEqual(String a, String b) { - // Check for nulls - if (a == b) return true; - if (a == null && b != null) return false; - if (a != null && b == null) return false; - - return a.equals(b); - } - - public static String computeSQLLongInClause(long[] items, String field) { - final StringBuilder builder = new StringBuilder(field); - builder.append(" IN ("); - int i = 0; - for (; i < items.length - 1; ++i) { - builder.append(items[i]); - builder.append(", "); - } - if (i < items.length) { - builder.append(items[i]); - } - builder.append(")"); - return builder.toString(); - } - - public static String computeSQLInClause(int items, String field) { - final StringBuilder builder = new StringBuilder(field); - builder.append(" IN ("); - int i = 0; - for (; i < items - 1; ++i) { - builder.append("?, "); - } - if (i < items) { - builder.append("?"); - } - builder.append(")"); - return builder.toString(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java deleted file mode 100644 index 9ba784759..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/VisitsHelper.java +++ /dev/null @@ -1,130 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.android; - -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; -import android.support.annotation.NonNull; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.db.BrowserContract.Visits; - -/** - * This class is used by History Sync code (see <code>AndroidBrowserHistoryDataAccessor</code> and <code>AndroidBrowserHistoryRepositorySession</code>, - * and provides utility functions for working with history visits. Primarily we're either inserting visits - * into local database based on data received from Sync, or we're preparing local visits for upload into Sync. - */ -public class VisitsHelper { - public static final boolean DEFAULT_IS_LOCAL_VALUE = false; - public static final String SYNC_TYPE_KEY = "type"; - public static final String SYNC_DATE_KEY = "date"; - - /** - * Returns a list of ContentValues of visits ready for insertion for a provided History GUID. - * Visits must have data and type. See <code>getVisitContentValues</code>. - * - * @param guid History GUID to use when inserting visit records - * @param visits <code>JSONArray</code> list of (date, type) tuples for visits - * @return visits ready for insertion - */ - public static ContentValues[] getVisitsContentValues(@NonNull String guid, @NonNull JSONArray visits) { - final ContentValues[] visitsToStore = new ContentValues[visits.size()]; - final int visitCount = visits.size(); - - if (visitCount == 0) { - return visitsToStore; - } - - for (int i = 0; i < visitCount; i++) { - visitsToStore[i] = getVisitContentValues( - guid, (JSONObject) visits.get(i), DEFAULT_IS_LOCAL_VALUE); - } - return visitsToStore; - } - - /** - * Maps up to <code>limit</code> visits for a given history GUID to an array of JSONObjects with "date" and "type" keys - * - * @param contentClient <code>ContentProviderClient</code> to use for querying Visits table - * @param guid History GUID for which to return visits - * @param limit Will return at most this number of visits - * @return <code>JSONArray</code> of all visits found for given History GUID - */ - public static JSONArray getRecentHistoryVisitsForGUID(@NonNull ContentProviderClient contentClient, - @NonNull String guid, int limit) throws RemoteException { - final JSONArray visits = new JSONArray(); - - final Cursor cursor = contentClient.query( - visitsUriWithLimit(limit), - new String[] {Visits.VISIT_TYPE, Visits.DATE_VISITED}, - Visits.HISTORY_GUID + " = ?", - new String[] {guid}, null); - if (cursor == null) { - return visits; - } - try { - if (!cursor.moveToFirst()) { - return visits; - } - - final int dateVisitedCol = cursor.getColumnIndexOrThrow(Visits.DATE_VISITED); - final int visitTypeCol = cursor.getColumnIndexOrThrow(Visits.VISIT_TYPE); - - while (!cursor.isAfterLast()) { - insertTupleIntoVisitsUnchecked(visits, - cursor.getLong(visitTypeCol), - cursor.getLong(dateVisitedCol) - ); - cursor.moveToNext(); - } - } finally { - cursor.close(); - } - - return visits; - } - - /** - * Constructs <code>ContentValues</code> object for a visit based on passed in parameters. - * - * @param visit <code>JSONObject</code> containing visit type and visit date keys for the visit - * @param guid History GUID with with to associate this visit - * @param isLocal Whether or not to mark this visit as local - * @return <code>ContentValues</code> with all visit values necessary for database insertion - * @throws IllegalArgumentException if visit object is missing date or type keys - */ - public static ContentValues getVisitContentValues(@NonNull String guid, @NonNull JSONObject visit, boolean isLocal) { - if (!visit.containsKey(SYNC_DATE_KEY) || !visit.containsKey(SYNC_TYPE_KEY)) { - throw new IllegalArgumentException("Visit missing required keys"); - } - - final ContentValues cv = new ContentValues(); - cv.put(Visits.HISTORY_GUID, guid); - cv.put(Visits.IS_LOCAL, isLocal ? Visits.VISIT_IS_LOCAL : Visits.VISIT_IS_REMOTE); - cv.put(Visits.VISIT_TYPE, (Long) visit.get(SYNC_TYPE_KEY)); - cv.put(Visits.DATE_VISITED, (Long) visit.get(SYNC_DATE_KEY)); - - return cv; - } - - @SuppressWarnings("unchecked") - private static void insertTupleIntoVisitsUnchecked(@NonNull final JSONArray visits, @NonNull Long type, @NonNull Long date) { - final JSONObject visit = new JSONObject(); - visit.put(SYNC_TYPE_KEY, type); - visit.put(SYNC_DATE_KEY, date); - visits.add(visit); - } - - private static Uri visitsUriWithLimit(int limit) { - return BrowserContractHelpers.VISITS_CONTENT_URI - .buildUpon() - .appendQueryParameter("limit", Integer.toString(limit)) - .build(); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java deleted file mode 100644 index f292600e4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java +++ /dev/null @@ -1,41 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import org.mozilla.gecko.sync.ThreadPool; -import org.mozilla.gecko.sync.repositories.RepositorySession; - -public abstract class DeferrableRepositorySessionCreationDelegate implements RepositorySessionCreationDelegate { - @Override - public RepositorySessionCreationDelegate deferredCreationDelegate() { - final RepositorySessionCreationDelegate self = this; - return new RepositorySessionCreationDelegate() { - - // TODO: rewrite to use ExecutorService. - @Override - public void onSessionCreated(final RepositorySession session) { - ThreadPool.run(new Runnable() { - @Override - public void run() { - self.onSessionCreated(session); - }}); - } - - @Override - public void onSessionCreateFailed(final Exception ex) { - ThreadPool.run(new Runnable() { - @Override - public void run() { - self.onSessionCreateFailed(ex); - }}); - } - - @Override - public RepositorySessionCreationDelegate deferredCreationDelegate() { - return this; - } - }; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java deleted file mode 100644 index 1ccdcce19..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java +++ /dev/null @@ -1,46 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.RepositorySession; - -public class DeferredRepositorySessionBeginDelegate implements RepositorySessionBeginDelegate { - private final RepositorySessionBeginDelegate inner; - private final 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."); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java deleted file mode 100644 index 1178d9b5b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java +++ /dev/null @@ -1,56 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public class DeferredRepositorySessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate { - private final RepositorySessionFetchRecordsDelegate inner; - private final 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 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 fetchEnd) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onFetchCompleted(fetchEnd); - } - }); - } - - @Override - public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService newExecutor) { - if (newExecutor == executor) { - return this; - } - throw new IllegalArgumentException("Can't re-defer this delegate."); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java deleted file mode 100644 index dbe7e4327..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java +++ /dev/null @@ -1,51 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.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."); - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java deleted file mode 100644 index 2f659c733..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java +++ /dev/null @@ -1,57 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -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 String guid) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onRecordStoreSucceeded(guid); - } - }); - } - - @Override - public void onRecordStoreFailed(final Exception ex, final String guid) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onRecordStoreFailed(ex, guid); - } - }); - } - - @Override - public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService newExecutor) { - if (newExecutor == executor) { - return this; - } - throw new IllegalArgumentException("Can't re-defer this delegate."); - } - - @Override - public void onStoreCompleted(final long storeEnd) { - executor.execute(new Runnable() { - @Override - public void run() { - inner.onStoreCompleted(storeEnd); - } - }); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java deleted file mode 100644 index f5853647f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionBeginDelegate.java +++ /dev/null @@ -1,23 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.RepositorySession; - -/** - * One of these two methods is guaranteed to be called after session.begin() is - * invoked (possibly during the invocation). The callback will be invoked prior - * to any other RepositorySession callbacks. - * - * @author rnewman - * - */ -public interface RepositorySessionBeginDelegate { - public void onBeginFailed(Exception ex); - public void onBeginSucceeded(RepositorySession session); - public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java deleted file mode 100644 index 139c561a0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCleanDelegate.java +++ /dev/null @@ -1,12 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import org.mozilla.gecko.sync.repositories.Repository; - -public interface RepositorySessionCleanDelegate { - public void onCleaned(Repository repo); - public void onCleanFailed(Repository repo, Exception ex); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java deleted file mode 100644 index 6ad4991c3..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionCreationDelegate.java +++ /dev/null @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import org.mozilla.gecko.sync.repositories.RepositorySession; - -// Used to provide the sessionCallback and storeCallback -// mechanism to repository instances. -public interface RepositorySessionCreationDelegate { - public void onSessionCreateFailed(Exception ex); - public void onSessionCreated(RepositorySession session); - public RepositorySessionCreationDelegate deferredCreationDelegate(); -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java deleted file mode 100644 index 589a093dc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java +++ /dev/null @@ -1,27 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -import org.mozilla.gecko.sync.repositories.domain.Record; - -public interface RepositorySessionFetchRecordsDelegate { - public void onFetchFailed(Exception ex, Record record); - public void onFetchedRecord(Record record); - - /** - * Called when all records in this fetch have been returned. - * - * @param fetchEnd - * A millisecond-resolution timestamp indicating the *remote* timestamp - * at the end of the range of records. Usually this is the timestamp at - * which the request was received. - * E.g., the (normalized) value of the X-Weave-Timestamp header. - */ - public void onFetchCompleted(final long fetchEnd); - - public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java deleted file mode 100644 index 40296dd4f..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionFinishDelegate.java +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.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(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java deleted file mode 100644 index 4f82768f1..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java +++ /dev/null @@ -1,10 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -public interface RepositorySessionGuidsSinceDelegate { - public void onGuidsSinceFailed(Exception ex); - public void onGuidsSinceSucceeded(String[] guids); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java deleted file mode 100644 index 01e44c3ae..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionStoreDelegate.java +++ /dev/null @@ -1,23 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -/** - * These methods *must* be invoked asynchronously. Use deferredStoreDelegate if you - * need help doing this. - * - * @author rnewman - * - */ -public interface RepositorySessionStoreDelegate { - public void onRecordStoreFailed(Exception ex, String recordGuid); - - // Called with a GUID when store has succeeded. - public void onRecordStoreSucceeded(String guid); - public void onStoreCompleted(long storeEnd); - public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java deleted file mode 100644 index cc8830729..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/delegates/RepositorySessionWipeDelegate.java +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.delegates; - -import java.util.concurrent.ExecutorService; - -public interface RepositorySessionWipeDelegate { - public void onWipeFailed(Exception ex); - public void onWipeSucceeded(); - public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService executor); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java deleted file mode 100644 index 27b8e7151..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecord.java +++ /dev/null @@ -1,488 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Map; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -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; - -/** - * Covers the fields used by all bookmark objects. - * @author rnewman - * - */ -public class BookmarkRecord extends Record { - public static final String PLACES_URI_PREFIX = "places:"; - - private static final String LOG_TAG = "BookmarkRecord"; - - public static final String COLLECTION_NAME = "bookmarks"; - public static final long BOOKMARKS_TTL = -1; // Never ttl bookmarks. - - public BookmarkRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = BOOKMARKS_TTL; - } - public BookmarkRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - public BookmarkRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - public BookmarkRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - public BookmarkRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - // Note: redundant accessors are evil. We're all grownups; let's just use - // public fields. - public String title; - public String bookmarkURI; - public String description; - public String keyword; - public String parentID; - public String parentName; - public long androidParentID; - public String type; - public long androidPosition; - - public JSONArray children; - public JSONArray tags; - - @Override - public String toString() { - return "#<Bookmark " + guid + " (" + androidID + "), parent " + - parentID + "/" + androidParentID + "/" + parentName + ">"; - } - - // Oh God, this is terribly thread-unsafe. These record objects should be immutable. - @SuppressWarnings("unchecked") - protected JSONArray copyChildren() { - if (this.children == null) { - return null; - } - JSONArray children = new JSONArray(); - children.addAll(this.children); - return children; - } - - @SuppressWarnings("unchecked") - protected JSONArray copyTags() { - if (this.tags == null) { - return null; - } - JSONArray tags = new JSONArray(); - tags.addAll(this.tags); - return tags; - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - BookmarkRecord out = new BookmarkRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - // Copy BookmarkRecord fields. - out.title = this.title; - out.bookmarkURI = this.bookmarkURI; - out.description = this.description; - out.keyword = this.keyword; - out.parentID = this.parentID; - out.parentName = this.parentName; - out.androidParentID = this.androidParentID; - out.type = this.type; - out.androidPosition = this.androidPosition; - - out.children = this.copyChildren(); - out.tags = this.copyTags(); - - return out; - } - - public boolean isBookmark() { - if (type == null) { - return false; - } - return type.equals("bookmark"); - } - - public boolean isFolder() { - if (type == null) { - return false; - } - return type.equals("folder"); - } - - public boolean isLivemark() { - if (type == null) { - return false; - } - return type.equals("livemark"); - } - - public boolean isSeparator() { - if (type == null) { - return false; - } - return type.equals("separator"); - } - - public boolean isMicrosummary() { - if (type == null) { - return false; - } - return type.equals("microsummary"); - } - - public boolean isQuery() { - if (type == null) { - return false; - } - return type.equals("query"); - } - - /** - * Return true if this record should have the Sync fields - * of a bookmark, microsummary, or query. - */ - private boolean isBookmarkIsh() { - if (type == null) { - return false; - } - return type.equals("bookmark") || - type.equals("microsummary") || - type.equals("query"); - } - - @Override - protected void initFromPayload(ExtendedJSONObject payload) { - this.type = payload.getString("type"); - this.title = payload.getString("title"); - this.description = payload.getString("description"); - this.parentID = payload.getString("parentid"); - this.parentName = payload.getString("parentName"); - - if (isFolder()) { - try { - this.children = payload.getArray("children"); - } catch (NonArrayJSONException e) { - Logger.error(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(); - } - return; - } - - final String bmkUri = payload.getString("bmkUri"); - - // bookmark, microsummary, query. - if (isBookmarkIsh()) { - this.keyword = payload.getString("keyword"); - try { - this.tags = payload.getArray("tags"); - } catch (NonArrayJSONException e) { - Logger.warn(LOG_TAG, "Got non-array tags in bookmark record " + this.guid, e); - this.tags = new JSONArray(); - } - } - - if (isBookmark()) { - this.bookmarkURI = bmkUri; - return; - } - - if (isLivemark()) { - String siteUri = payload.getString("siteUri"); - String feedUri = payload.getString("feedUri"); - this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri, - "siteUri", siteUri, - "feedUri", feedUri); - return; - } - if (isQuery()) { - String queryId = payload.getString("queryId"); - String folderName = payload.getString("folderName"); - this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri, - "queryId", queryId, - "folderName", folderName); - return; - } - if (isMicrosummary()) { - String generatorUri = payload.getString("generatorUri"); - String staticTitle = payload.getString("staticTitle"); - this.bookmarkURI = encodeUnsupportedTypeURI(bmkUri, - "generatorUri", generatorUri, - "staticTitle", staticTitle); - return; - } - if (isSeparator()) { - Object p = payload.get("pos"); - if (p instanceof Long) { - this.androidPosition = (Long) p; - } else if (p instanceof String) { - try { - this.androidPosition = Long.parseLong((String) p, 10); - } catch (NumberFormatException e) { - return; - } - } else { - Logger.warn(LOG_TAG, "Unsupported position value " + p); - return; - } - String pos = String.valueOf(this.androidPosition); - this.bookmarkURI = encodeUnsupportedTypeURI(null, "pos", pos, null, null); - return; - } - } - - @Override - protected void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, "type", this.type); - putPayload(payload, "title", this.title); - putPayload(payload, "description", this.description); - putPayload(payload, "parentid", this.parentID); - putPayload(payload, "parentName", this.parentName); - putPayload(payload, "keyword", this.keyword); - - if (isFolder()) { - payload.put("children", this.children); - return; - } - - // bookmark, microsummary, query. - if (isBookmarkIsh()) { - if (isBookmark()) { - payload.put("bmkUri", bookmarkURI); - } - - if (isQuery()) { - Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI); - putPayload(payload, "queryId", parts.get("queryId"), true); - putPayload(payload, "folderName", parts.get("folderName"), true); - putPayload(payload, "bmkUri", parts.get("uri")); - return; - } - - if (this.tags != null) { - payload.put("tags", this.tags); - } - - putPayload(payload, "keyword", this.keyword); - return; - } - - if (isLivemark()) { - Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI); - putPayload(payload, "siteUri", parts.get("siteUri")); - putPayload(payload, "feedUri", parts.get("feedUri")); - return; - } - if (isMicrosummary()) { - Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI); - putPayload(payload, "generatorUri", parts.get("generatorUri")); - putPayload(payload, "staticTitle", parts.get("staticTitle")); - return; - } - if (isSeparator()) { - Map<String, String> parts = Utils.extractURIComponents(PLACES_URI_PREFIX, this.bookmarkURI); - String pos = parts.get("pos"); - if (pos == null) { - return; - } - try { - payload.put("pos", Long.parseLong(pos, 10)); - } catch (NumberFormatException e) { - return; - } - return; - } - } - - private void trace(String s) { - Logger.trace(LOG_TAG, s); - } - - @Override - public boolean equalPayloads(Object o) { - trace("Calling BookmarkRecord.equalPayloads."); - if (!(o instanceof BookmarkRecord)) { - return false; - } - - BookmarkRecord other = (BookmarkRecord) o; - if (!super.equalPayloads(other)) { - return false; - } - - if (!RepoUtils.stringsEqual(this.type, other.type)) { - return false; - } - - // Check children. - 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) - && RepoUtils.stringsEqual(this.parentName, other.parentName) - && RepoUtils.stringsEqual(this.description, other.description) - && RepoUtils.stringsEqual(this.keyword, other.keyword) - && jsonArrayStringsEqual(this.tags, other.tags); - } - - // TODO: two records can be congruent if their child lists are different. - @Override - public boolean congruentWith(Object o) { - return this.equalPayloads(o) && - super.congruentWith(o); - } - - // Converts two JSONArrays to strings and checks if they are the same. - // This is only useful for stuff like tags where we aren't actually - // touching the data there (and therefore ordering won't change) - private boolean jsonArrayStringsEqual(JSONArray a, JSONArray b) { - // Check for nulls - if (a == b) return true; - if (a == null && b != null) return false; - if (a != null && b == null) return false; - return RepoUtils.stringsEqual(a.toJSONString(), b.toJSONString()); - } - - /** - * URL-encode the provided string. If the input is null, - * the empty string is returned. - * - * @param in the string to encode. - * @return a URL-encoded version of the input. - */ - protected static String encode(String in) { - if (in == null) { - return ""; - } - try { - return URLEncoder.encode(in, "UTF-8"); - } catch (UnsupportedEncodingException e) { - // Will never occur. - return null; - } - } - - /** - * Take the provided URI and two parameters, constructing a URI like - * - * places:uri=$uri&p1=$p1&p2=$p2 - * - * null values in either parameter or value result in the parameter being omitted. - */ - protected static String encodeUnsupportedTypeURI(String originalURI, String p1, String v1, String p2, String v2) { - StringBuilder b = new StringBuilder(PLACES_URI_PREFIX); - boolean previous = false; - if (originalURI != null) { - b.append("uri="); - b.append(encode(originalURI)); - previous = true; - } - if (p1 != null && v1 != null) { - if (previous) { - b.append("&"); - } - b.append(p1); - b.append("="); - b.append(encode(v1)); - previous = true; - } - if (p2 != null && v2 != null) { - if (previous) { - b.append("&"); - } - b.append(p2); - b.append("="); - b.append(encode(v2)); - previous = true; - } - return b.toString(); - } -} - - -/* -// Bookmark: -{cleartext: - {id: "l7p2xqOTMMXw", - type: "bookmark", - title: "Your Flight Status", - parentName: "mobile", - bmkUri: "http: //www.flightstats.com/go/Mobile/flightStatusByFlightProcess.do;jsessionid=13A6C8DCC9592AF141A43349040262CE.web3: 8009?utm_medium=cpc&utm_campaign=co-op&utm_source=airlineInformationAndStatus&id=212492593", - tags: [], - keyword: null, - description: null, - loadInSidebar: false, - parentid: "mobile"}, - data: {payload: {ciphertext: null}, - id: "l7p2xqOTMMXw", - sortindex: 107}, - collection: "bookmarks"} - -// Folder: -{cleartext: - {id: "mobile", - type: "folder", - parentName: "", - title: "mobile", - description: null, - children: ["1ROdlTuIoddD", "3Z_bMIHPSZQ8", "4mSDUuOo2iVB", "8aEdE9IIrJVr", - "9DzPTmkkZRDb", "Qwwb99HtVKsD", "s8tM36aGPKbq", "JMTi61hOO3JV", - "JQUDk0wSvYip", "LmVH-J1r3HLz", "NhgQlC5ykYGW", "OVanevUUaqO2", - "OtQVX0PMiWQj", "_GP5cF595iie", "fkRssjXSZDL3", "k7K_NwIA1Ya0", - "raox_QGzvqh1", "vXYL-xHjK06k", "QKHKUN6Dm-xv", "pmN2dYWT2MJ_", - "EVeO_J1SQiwL", "7N-qkepS7bec", "NIGa3ha-HVOE", "2Phv1I25wbuH", - "TTSIAH1fV0VE", "WOmZ8PfH39Da", "gDTXNg4m1AJZ", "ayI30OZslHbO", - "zSEs4O3n6CzQ", "oWTDR0gO2aWf", "wWHUoFaInXi9", "F7QTuVJDpsTM", - "FIboggegplk-", "G4HWrT5nfRYS", "MHA7y9bupDdv", "T_Ldzmj0Ttte", - "U9eYu3SxsE_U", "bk463Kl9IO_m", "brUfrqJjFNSR", "ccpawfWsD-bY", - "l7p2xqOTMMXw", "o-nSDKtXYln7"], - parentid: "places"}, - data: {payload: {ciphertext: null}, - id: "mobile", - sortindex: 1000000}, - collection: "bookmarks"} -*/ diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java deleted file mode 100644 index edf7b288c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/BookmarkRecordFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; - -/** - * Turns CryptoRecords into BookmarkRecords. - * - * @author rnewman - * - */ -public class BookmarkRecordFactory extends RecordFactory { - - @Override - public Record createRecord(Record record) { - BookmarkRecord r = new BookmarkRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java deleted file mode 100644 index 0c513a4a0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecord.java +++ /dev/null @@ -1,231 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -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; - -public class ClientRecord extends Record { - private static final String LOG_TAG = "ClientRecord"; - - public static final String CLIENT_TYPE = "mobile"; - public static final String COLLECTION_NAME = "clients"; - public static final long CLIENTS_TTL = 21 * 24 * 60 * 60; // 21 days in seconds. - public static final String DEFAULT_CLIENT_NAME = "Default Name"; - - public static final String PROTOCOL_LEGACY_SYNC = "1.1"; - public static final String PROTOCOL_FXA_SYNC = "1.5"; - - /** - * Each of these fields is 'owned' by the client it represents. For example, - * the "version" field is the Firefox version of that client; some time after - * that client upgrades, it'll upload a new record with its new version. - * - * The only exception is for commands. When a command is sent to a client, the - * sender will download its current record, append the command to the - * "commands" array, and reupload the record. After processing, the recipient - * will reupload its record with an empty commands array. - * - * Note that the version, then, will remain the version of the recipient, as - * with the other descriptive fields. - */ - public String name = ClientRecord.DEFAULT_CLIENT_NAME; - public String type = ClientRecord.CLIENT_TYPE; - public String version = null; // Free-form string, optional. - public JSONArray commands; - public JSONArray protocols; - - // Optional fields. - // See <https://github.com/mozilla-services/docs/blob/master/source/sync/objectformats.rst#user-content-clients> - // for full formats. - // If a value isn't known, the field is omitted. - public String formfactor; // "phone", "largetablet", "smalltablet", "desktop", "laptop", "tv". - public String os; // One of "Android", "Darwin", "WINNT", "Linux", "iOS", "Firefox OS". - public String application; // Display name, E.g., "Firefox Beta" - public String appPackage; // E.g., "org.mozilla.firefox_beta" - public String device; // E.g., "HTC One" - public String fxaDeviceId; // E.g., "525b624eaaf1e40d21ec8997c3116ad8" - - public ClientRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = CLIENTS_TTL; - } - - public ClientRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - - public ClientRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - - public ClientRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - - public ClientRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - @Override - protected void initFromPayload(ExtendedJSONObject payload) { - this.name = (String) payload.get("name"); - this.type = (String) payload.get("type"); - try { - this.version = (String) payload.get("version"); - } catch (Exception e) { - // Oh well. - } - - try { - commands = payload.getArray("commands"); - } catch (NonArrayJSONException e) { - Logger.debug(LOG_TAG, "Got non-array commands in client record " + guid, e); - commands = null; - } - - try { - protocols = payload.getArray("protocols"); - } catch (NonArrayJSONException e) { - Logger.debug(LOG_TAG, "Got non-array protocols in client record " + guid, e); - protocols = null; - } - - if (payload.containsKey("formfactor")) { - this.formfactor = payload.getString("formfactor"); - } - - if (payload.containsKey("os")) { - this.os = payload.getString("os"); - } - - if (payload.containsKey("application")) { - this.application = payload.getString("application"); - } - - if (payload.containsKey("appPackage")) { - this.appPackage = payload.getString("appPackage"); - } - - if (payload.containsKey("device")) { - this.device = payload.getString("device"); - } - - if (payload.containsKey("fxaDeviceId")) { - this.fxaDeviceId = payload.getString("fxaDeviceId"); - } - } - - @Override - protected void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, "id", this.guid); - putPayload(payload, "name", this.name); - putPayload(payload, "type", this.type); - putPayload(payload, "version", this.version); - - if (this.commands != null) { - payload.put("commands", this.commands); - } - - if (this.protocols != null) { - payload.put("protocols", this.protocols); - } - - if (this.formfactor != null) { - payload.put("formfactor", this.formfactor); - } - - if (this.os != null) { - payload.put("os", this.os); - } - - if (this.application != null) { - payload.put("application", this.application); - } - - if (this.appPackage != null) { - payload.put("appPackage", this.appPackage); - } - - if (this.device != null) { - payload.put("device", this.device); - } - - if (this.fxaDeviceId != null) { - payload.put("fxaDeviceId", this.fxaDeviceId); - } - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ClientRecord) || !super.equals(o)) { - return false; - } - - return this.equalPayloads(o); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public boolean equalPayloads(Object o) { - if (!(o instanceof ClientRecord) || !super.equalPayloads(o)) { - return false; - } - - // Don't compare versions, protocols, or other optional fields, no matter how much we might want to. - // They're not required by the spec. - ClientRecord other = (ClientRecord) o; - if (!RepoUtils.stringsEqual(other.name, this.name) || - !RepoUtils.stringsEqual(other.type, this.type)) { - return false; - } - return true; - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - ClientRecord out = new ClientRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - out.name = this.name; - out.type = this.type; - out.version = this.version; - out.protocols = this.protocols; - - out.formfactor = this.formfactor; - out.os = this.os; - out.application = this.application; - out.appPackage = this.appPackage; - out.device = this.device; - out.fxaDeviceId = this.fxaDeviceId; - - return out; - } - -/* -Example record: - -{id:"relf31w7B4F1", - name:"marina_mac", - type:"mobile" - commands:[{"args":["bookmarks"],"command":"wipeEngine"}, - {"args":["forms"],"command":"wipeEngine"}, - {"args":["history"],"command":"wipeEngine"}, - {"args":["passwords"],"command":"wipeEngine"}, - {"args":["prefs"],"command":"wipeEngine"}, - {"args":["tabs"],"command":"wipeEngine"}, - {"args":["addons"],"command":"wipeEngine"}]} -*/ -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java deleted file mode 100644 index 897d2859c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/ClientRecordFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; - -public class ClientRecordFactory extends RecordFactory { - @Override - public Record createRecord(Record record) { - ClientRecord r = new ClientRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java deleted file mode 100644 index e7ca70cb4..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/FormHistoryRecord.java +++ /dev/null @@ -1,139 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; - -/** - * A FormHistoryRecord represents a saved form element. - * - * I map a <code>fieldName</code> string to a <code>value</code> string. - * - * @see "<a href='http://dxr.mozilla.org/services-central/source/services-central/services/sync/modules/engines/forms.js'>http://dxr.mozilla.org/services-central/source/services-central/services/sync/modules/engines/forms.js</a>." - */ -public class FormHistoryRecord extends Record { - private static final String LOG_TAG = "FormHistoryRecord"; - - public static final String COLLECTION_NAME = "forms"; - private static final String PAYLOAD_NAME = "name"; - private static final String PAYLOAD_VALUE = "value"; - public static final long FORMS_TTL = 3 * 365 * 24 * 60 * 60; // Three years in seconds. - - /** - * The name of the saved form field. - */ - public String fieldName; - - /** - * The value of the saved form field. - */ - public String fieldValue; - - public FormHistoryRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = FORMS_TTL; - } - - public FormHistoryRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - - public FormHistoryRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - - public FormHistoryRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - - public FormHistoryRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - FormHistoryRecord out = new FormHistoryRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - - // Copy FormHistoryRecord fields. - out.fieldName = this.fieldName; - out.fieldValue = this.fieldValue; - - return out; - } - - @Override - public void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, PAYLOAD_NAME, this.fieldName); - putPayload(payload, PAYLOAD_VALUE, this.fieldValue); - } - - @Override - public void initFromPayload(ExtendedJSONObject payload) { - this.fieldName = payload.getString(PAYLOAD_NAME); - this.fieldValue = payload.getString(PAYLOAD_VALUE); - } - - /** - * We consider two form history records to be congruent if they represent the - * same form element regardless of times used. - */ - @Override - public boolean congruentWith(Object o) { - if (!(o instanceof FormHistoryRecord)) { - return false; - } - FormHistoryRecord other = (FormHistoryRecord) o; - if (!super.congruentWith(other)) { - return false; - } - return RepoUtils.stringsEqual(this.fieldName, other.fieldName) && - RepoUtils.stringsEqual(this.fieldValue, other.fieldValue); - } - - @Override - public boolean equalPayloads(Object o) { - if (!(o instanceof FormHistoryRecord)) { - Logger.debug(LOG_TAG, "Not a FormHistoryRecord: " + o.getClass()); - return false; - } - FormHistoryRecord other = (FormHistoryRecord) o; - if (!super.equalPayloads(other)) { - Logger.debug(LOG_TAG, "super.equalPayloads returned false."); - return false; - } - - if (this.deleted) { - // FormHistoryRecords are equal if they are both deleted (which - // they are, since super.equalPayloads is true) and have the - // same GUID. - if (other.deleted) { - return RepoUtils.stringsEqual(this.guid, other.guid); - } - return false; - } - - return RepoUtils.stringsEqual(this.fieldName, other.fieldName) && - RepoUtils.stringsEqual(this.fieldValue, other.fieldValue); - } - - public FormHistoryRecord log(String logTag) { - try { - Logger.debug(logTag, "Returning form history record " + guid + " (" + androidID + ")"); - Logger.debug(logTag, "> Last modified: " + lastModified); - if (Logger.LOG_PERSONAL_INFORMATION) { - Logger.pii(logTag, "> Field name: " + fieldName); - Logger.pii(logTag, "> Field value: " + fieldValue); - } - } catch (Exception e) { - Logger.debug(logTag, "Exception logging form history record " + this, e); - } - return this; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java deleted file mode 100644 index 94eae13a7..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecord.java +++ /dev/null @@ -1,217 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import java.util.HashMap; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -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; - -/** - * 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"; - public static final long HISTORY_TTL = 60 * 24 * 60 * 60; // 60 days in seconds. - - public HistoryRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = HISTORY_TTL; - } - public HistoryRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - public HistoryRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - public HistoryRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - public HistoryRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - public String title; - public String histURI; - public JSONArray visits; - public long fennecDateVisited; - public long fennecVisitCount; - - @SuppressWarnings("unchecked") - private JSONArray copyVisits() { - if (this.visits == null) { - return null; - } - JSONArray out = new JSONArray(); - out.addAll(this.visits); - return out; - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - HistoryRecord out = new HistoryRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - // Copy HistoryRecord fields. - out.title = this.title; - out.histURI = this.histURI; - out.fennecDateVisited = this.fennecDateVisited; - out.fennecVisitCount = this.fennecVisitCount; - out.visits = this.copyVisits(); - - return out; - } - - @Override - protected void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, "id", this.guid); - putPayload(payload, "title", this.title); - putPayload(payload, "histUri", this.histURI); // TODO: encoding? - payload.put("visits", this.visits); - } - - @Override - protected void initFromPayload(ExtendedJSONObject payload) { - this.histURI = (String) payload.get("histUri"); - this.title = (String) payload.get("title"); - try { - this.visits = payload.getArray("visits"); - } catch (NonArrayJSONException e) { - Logger.error(LOG_TAG, "Got non-array visits in history record " + this.guid, e); - this.visits = new JSONArray(); - } - } - - /** - * We consider two history records to be congruent if they represent the - * same history record regardless of visits. Titles are allowed to differ, - * but the URI must be the same. - */ - @Override - public boolean congruentWith(Object o) { - if (!(o instanceof HistoryRecord)) { - return false; - } - HistoryRecord other = (HistoryRecord) o; - if (!super.congruentWith(other)) { - return false; - } - return RepoUtils.stringsEqual(this.histURI, other.histURI); - } - - @Override - public boolean equalPayloads(Object o) { - if (!(o instanceof HistoryRecord)) { - Logger.debug(LOG_TAG, "Not a HistoryRecord: " + o.getClass()); - return false; - } - HistoryRecord other = (HistoryRecord) o; - if (!super.equalPayloads(other)) { - Logger.debug(LOG_TAG, "super.equalPayloads returned false."); - return false; - } - return RepoUtils.stringsEqual(this.title, other.title) && - RepoUtils.stringsEqual(this.histURI, other.histURI) && - checkVisitsEquals(other); - } - - @Override - public boolean equalAndroidIDs(Record other) { - return super.equalAndroidIDs(other) && - this.equalFennecVisits(other); - } - - private boolean equalFennecVisits(Record other) { - if (!(other instanceof HistoryRecord)) { - return false; - } - HistoryRecord h = (HistoryRecord) other; - return this.fennecDateVisited == h.fennecDateVisited && - this.fennecVisitCount == h.fennecVisitCount; - } - - private boolean checkVisitsEquals(HistoryRecord other) { - Logger.debug(LOG_TAG, "Checking visits."); - if (Logger.LOG_PERSONAL_INFORMATION) { - // Don't JSON-encode unless we're logging. - Logger.pii(LOG_TAG, ">> Mine: " + ((this.visits == null) ? "null" : this.visits.toJSONString())); - Logger.pii(LOG_TAG, ">> Theirs: " + ((other.visits == null) ? "null" : other.visits.toJSONString())); - } - - // 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 < bSize; i++) { - JSONObject visit = (JSONObject) other.visits.get(i); - otherVisits.put((Long) visit.get("date"), (Long) visit.get("type")); - } - - for (int i = 0; i < aSize; i++) { - JSONObject visit = (JSONObject) this.visits.get(i); - 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 (note microsecond resolution): -// -// {id:"--DUvUomABNq", -// histUri:"https://bugzilla.mozilla.org/show_bug.cgi?id=697634", -// title:"697634 \u2013 xpcshell test failures on 10.7", -// visits:[{date:1320087601465600, type:2}, -// {date:1320084970724990, type:1}, -// {date:1320084847035717, type:1}, -// {date:1319764134412287, type:1}, -// {date:1319757917982518, type:1}, -// {date:1319751664627351, type:1}, -// {date:1319681421072326, type:1}, -// {date:1319681306455594, type:1}, -// {date:1319678117125234, type:1}, -// {date:1319677508862901, type:1}] -// } -// -//"type" is a transition type: -// -//https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsINavHistoryService#Transition_type_constants - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java deleted file mode 100644 index ac2c6a1dc..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/HistoryRecordFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; - -/** - * Turns CryptoRecords into HistoryRecords. - * - * @author rnewman - * - */ -public class HistoryRecordFactory extends RecordFactory { - - @Override - public Record createRecord(Record record) { - HistoryRecord r = new HistoryRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } - -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java deleted file mode 100644 index b2de60f3c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecord.java +++ /dev/null @@ -1,205 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.repositories.android.RepoUtils; - -public class PasswordRecord extends Record { - private static final String LOG_TAG = "PasswordRecord"; - - public static final String COLLECTION_NAME = "passwords"; - public static long PASSWORDS_TTL = -1; // Never expire passwords. - - // Payload strings. - public static final String PAYLOAD_HOSTNAME = "hostname"; - public static final String PAYLOAD_FORM_SUBMIT_URL = "formSubmitURL"; - public static final String PAYLOAD_HTTP_REALM = "httpRealm"; - public static final String PAYLOAD_USERNAME = "username"; - public static final String PAYLOAD_PASSWORD = "password"; - public static final String PAYLOAD_USERNAME_FIELD = "usernameField"; - public static final String PAYLOAD_PASSWORD_FIELD = "passwordField"; - - public PasswordRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = PASSWORDS_TTL; - } - public PasswordRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - public PasswordRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - public PasswordRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - public PasswordRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - public String id; - public String hostname; - public String formSubmitURL; - public String httpRealm; - // TODO these are encrypted in the passwords content provider, - // need to figure out what we need to do here. - public String usernameField; - public String passwordField; - public String encryptedUsername; - public String encryptedPassword; - public String encType; - - public long timeCreated; - public long timeLastUsed; - public long timePasswordChanged; - public long timesUsed; - - - @Override - public Record copyWithIDs(String guid, long androidID) { - PasswordRecord out = new PasswordRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - // Copy PasswordRecord fields. - out.id = this.id; - out.hostname = this.hostname; - out.formSubmitURL = this.formSubmitURL; - out.httpRealm = this.httpRealm; - - out.usernameField = this.usernameField; - out.passwordField = this.passwordField; - out.encryptedUsername = this.encryptedUsername; - out.encryptedPassword = this.encryptedPassword; - out.encType = this.encType; - - out.timeCreated = this.timeCreated; - out.timeLastUsed = this.timeLastUsed; - out.timePasswordChanged = this.timePasswordChanged; - out.timesUsed = this.timesUsed; - - return out; - } - - @Override - public void initFromPayload(ExtendedJSONObject payload) { - this.hostname = payload.getString(PAYLOAD_HOSTNAME); - this.formSubmitURL = payload.getString(PAYLOAD_FORM_SUBMIT_URL); - this.httpRealm = payload.getString(PAYLOAD_HTTP_REALM); - this.encryptedUsername = payload.getString(PAYLOAD_USERNAME); - this.encryptedPassword = payload.getString(PAYLOAD_PASSWORD); - this.usernameField = payload.getString(PAYLOAD_USERNAME_FIELD); - this.passwordField = payload.getString(PAYLOAD_PASSWORD_FIELD); - } - - @Override - public void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, PAYLOAD_HOSTNAME, this.hostname); - putPayload(payload, PAYLOAD_FORM_SUBMIT_URL, this.formSubmitURL); - putPayload(payload, PAYLOAD_HTTP_REALM, this.httpRealm); - putPayload(payload, PAYLOAD_USERNAME, this.encryptedUsername); - putPayload(payload, PAYLOAD_PASSWORD, this.encryptedPassword); - putPayload(payload, PAYLOAD_USERNAME_FIELD, this.usernameField); - putPayload(payload, PAYLOAD_PASSWORD_FIELD, this.passwordField); - } - - @Override - public boolean congruentWith(Object o) { - if (!(o instanceof PasswordRecord)) { - return false; - } - PasswordRecord other = (PasswordRecord) o; - if (!super.congruentWith(other)) { - return false; - } - return RepoUtils.stringsEqual(this.hostname, other.hostname) - && RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL) - // Bug 738347 - SQLiteBridge does not check for nulls in ContentValues. - // && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm) - // && RepoUtils.stringsEqual(this.encType, other.encType) - && RepoUtils.stringsEqual(this.usernameField, other.usernameField) - && RepoUtils.stringsEqual(this.passwordField, other.passwordField) - && RepoUtils.stringsEqual(this.encryptedUsername, other.encryptedUsername) - && RepoUtils.stringsEqual(this.encryptedPassword, other.encryptedPassword); - } - - @Override - public boolean equalPayloads(Object o) { - if (!(o instanceof PasswordRecord)) { - return false; - } - - PasswordRecord other = (PasswordRecord) o; - Logger.debug("PasswordRecord", "thisRecord:" + this.toString()); - Logger.debug("PasswordRecord", "otherRecord:" + o.toString()); - - if (this.deleted) { - if (other.deleted) { - // Deleted records are equal if their guids match. - return RepoUtils.stringsEqual(this.guid, other.guid); - } - // One record is deleted, the other is not. Not equal. - return false; - } - - if (!super.equalPayloads(other)) { - Logger.debug(LOG_TAG, "super.equalPayloads returned false."); - return false; - } - - return RepoUtils.stringsEqual(this.hostname, other.hostname) - && RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL) - // Bug 738347 - SQLiteBridge does not check for nulls in ContentValues. - // && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm) - // && RepoUtils.stringsEqual(this.encType, other.encType) - && RepoUtils.stringsEqual(this.usernameField, other.usernameField) - && RepoUtils.stringsEqual(this.passwordField, other.passwordField) - && RepoUtils.stringsEqual(this.encryptedUsername, other.encryptedUsername) - && RepoUtils.stringsEqual(this.encryptedPassword, other.encryptedPassword); - // Desktop sync never sets timeCreated so this isn't relevant for sync records. - } - - @Override - public String toString() { - return "PasswordRecord {" - + "lastModified: " + this.lastModified + ", " - + "hostname null?: " + (this.hostname == null) + ", " - + "formSubmitURL null?: " + (this.formSubmitURL == null) + ", " - + "httpRealm null?: " + (this.httpRealm == null) + ", " - + "usernameField null?: " + (this.usernameField == null) + ", " - + "passwordField null?: " + (this.passwordField == null) + ", " - + "encryptedUsername null?: " + (this.encryptedUsername == null) + ", " - + "encryptedPassword null?: " + (this.encryptedPassword == null) + ", " - + "encType: " + this.encType + ", " - + "timeCreated: " + this.timeCreated + ", " - + "timeLastUsed: " + this.timeLastUsed + ", " - + "timePasswordChanged: " + this.timePasswordChanged + ", " - + "timesUsed: " + this.timesUsed; - } - - /** - * A PasswordRecord is considered valid if it abides by the database - * constraints of the PasswordsProvider (moz_logins). - * - * See toolkit/components/passwordmgr/storage-mozStorage.js for the - * definitions: - * - * http://hg.mozilla.org/mozilla-central/file/00955d61cc94/toolkit/components/passwordmgr/storage-mozStorage.js#l98 - */ - public boolean isValid() { - if (this.deleted) { - return true; - } - - return this.hostname != null && - this.encryptedUsername != null && - this.encryptedPassword != null && - this.usernameField != null && - this.passwordField != null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java deleted file mode 100644 index fc7ef916d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/PasswordRecordFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; -import org.mozilla.gecko.sync.repositories.domain.PasswordRecord; -import org.mozilla.gecko.sync.repositories.domain.Record; - -public class PasswordRecordFactory extends RecordFactory { - @Override - public Record createRecord(Record record) { - PasswordRecord r = new PasswordRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java deleted file mode 100644 index 145704c1c..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java +++ /dev/null @@ -1,308 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import java.io.UnsupportedEncodingException; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.ExtendedJSONObject; - -/** - * Record is the abstract base class for all entries that Sync processes: - * bookmarks, passwords, history, and such. - * - * A Record can be initialized from or serialized to a CryptoRecord for - * submission to an encrypted store. - * - * Records should be considered to be conventionally immutable: modifications - * should be completed before the new record object escapes its constructing - * scope. Note that this is a critically important part of equality. As Rich - * Hickey notes: - * - * … the only things you can really compare for equality are immutable things, - * because if you compare two things for equality that are mutable, and ever - * say true, and they're ever not the same thing, you are wrong. Or you will - * become wrong at some point in the future. - * - * Records have a layered definition of equality. Two records can be said to be - * "equal" if: - * - * * They have the same GUID and collection. Two crypto/keys records are in some - * way "the same". - * This is `equalIdentifiers`. - * - * * Their most significant fields are the same. That is to say, they share a - * GUID, a collection, deletion, and domain-specific fields. Two copies of - * crypto/keys, neither deleted, with the same encrypted data but different - * modified times and sortIndex are in a stronger way "the same". - * This is `equalPayloads`. - * - * * Their most significant fields are the same, and their local fields (e.g., - * the androidID to which we have decided that this record maps) are congruent. - * A record with the same androidID, or one whose androidID has not been set, - * can be considered "the same". - * This concept can be extended by Record subclasses. The key point is that - * reconciling should be applied to the contents of these records. For example, - * two history records with the same URI and GUID, but different visit arrays, - * can be said to be congruent. - * This is `congruentWith`. - * - * * They are strictly identical. Every field that is persisted, including - * lastModified and androidID, is equal. - * This is `equals`. - * - * Different parts of the codebase have use for different layers of this - * comparison hierarchy. For instance, lastModified times change every time a - * record is stored; a store followed by a retrieval will return a Record that - * shares its most significant fields with the input, but has a later - * lastModified time and might not yet have values set for others. Reconciling - * will thus ignore the modification time of a record. - * - * @author rnewman - * - */ -public abstract class Record { - - public String guid; - public String collection; - public long lastModified; - public boolean deleted; - public long androidID; - /** - * An integer indicating the relative importance of this item in the collection. - * <p> - * Default is 0. - */ - public long sortIndex; - /** - * The number of seconds to keep this record. After that time this item will - * no longer be returned in response to any request, and it may be pruned from - * the database. - * <p> - * Negative values mean never forget this record. - * <p> - * Default is 1 year. - */ - public long ttl; - - public Record(String guid, String collection, long lastModified, boolean deleted) { - this.guid = guid; - this.collection = collection; - this.lastModified = lastModified; - this.deleted = deleted; - this.sortIndex = 0; - this.ttl = 365 * 24 * 60 * 60; // Seconds. - this.androidID = -1; - } - - /** - * Return true iff the input is a Record and has the same - * collection and guid as this object. - */ - public boolean equalIdentifiers(Object o) { - if (!(o instanceof Record)) { - return false; - } - - Record other = (Record) o; - 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; - } - } - return true; - } - - /** - * @param o - * The object to which this object should be compared. - * @return - * true iff the input is a Record which is substantially the - * same as this object. - */ - public boolean equalPayloads(Object o) { - if (!this.equalIdentifiers(o)) { - return false; - } - Record other = (Record) o; - return this.deleted == other.deleted; - } - - /** - * - * - * @param o - * The object to which this object should be compared. - * @return - * true iff the input is a Record which is substantially the - * same as this object, considering the ability and desire to - * reconcile the two objects if possible. - */ - public boolean congruentWith(Object o) { - if (!this.equalIdentifiers(o)) { - return false; - } - Record other = (Record) o; - return congruentAndroidIDs(other) && - (this.deleted == other.deleted); - } - - public boolean congruentAndroidIDs(Record other) { - // We treat -1 as "unset", and treat this as - // congruent with any other value. - if (this.androidID != -1 && - other.androidID != -1 && - this.androidID != other.androidID) { - return false; - } - return true; - } - - /** - * Return true iff the input is both equal in terms of payload, - * and also shares transient values such as timestamps. - */ - @Override - public boolean equals(Object o) { - if (!(o instanceof Record)) { - return false; - } - - Record other = (Record) o; - return equalTimestamps(other) && - equalSortIndices(other) && - equalAndroidIDs(other) && - equalPayloads(o); - } - - public boolean equalAndroidIDs(Record other) { - return this.androidID == other.androidID; - } - - public boolean equalSortIndices(Record other) { - return this.sortIndex == other.sortIndex; - } - - public boolean equalTimestamps(Object o) { - if (!(o instanceof Record)) { - return false; - } - return ((Record) o).lastModified == this.lastModified; - } - - protected abstract void populatePayload(ExtendedJSONObject payload); - protected abstract void initFromPayload(ExtendedJSONObject payload); - - public void initFromEnvelope(CryptoRecord envelope) { - ExtendedJSONObject p = envelope.payload; - this.guid = envelope.guid; - checkGUIDs(p); - - this.collection = envelope.collection; - this.lastModified = envelope.lastModified; - - final Object del = p.get("deleted"); - if (del instanceof Boolean) { - this.deleted = (Boolean) del; - } else { - this.initFromPayload(p); - } - - } - - public CryptoRecord getEnvelope() { - CryptoRecord rec = new CryptoRecord(this); - ExtendedJSONObject payload = new ExtendedJSONObject(); - payload.put("id", this.guid); - - if (this.deleted) { - payload.put("deleted", true); - } else { - populatePayload(payload); - } - rec.payload = payload; - return rec; - } - - @SuppressWarnings("static-method") - 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; - } - } - - /** - * Utility for safely populating an output CryptoRecord. - * - * @param rec - * @param key - * @param value - */ - @SuppressWarnings("static-method") - protected void putPayload(CryptoRecord rec, String key, String value) { - if (value == null) { - return; - } - rec.payload.put(key, value); - } - - protected void putPayload(ExtendedJSONObject payload, String key, String value) { - this.putPayload(payload, key, value, false); - } - - @SuppressWarnings("static-method") - protected void putPayload(ExtendedJSONObject payload, String key, String value, boolean excludeEmpty) { - if (value == null) { - return; - } - if (excludeEmpty && value.equals("")) { - return; - } - payload.put(key, value); - } - - 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); - } - } - - /** - * Oh for persistent data structures. - * - * @param guid - * @param androidID - * @return - * An identical copy of this record with the provided two values. - */ - public abstract Record copyWithIDs(String guid, long androidID); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java deleted file mode 100644 index 0d8fe90b2..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/RecordParseException.java +++ /dev/null @@ -1,14 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - - -public class RecordParseException extends Exception { - private static final long serialVersionUID = -5145494854722254491L; - - public RecordParseException(String detailMessage) { - super(detailMessage); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java deleted file mode 100644 index eb3a4f6d0..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecord.java +++ /dev/null @@ -1,153 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import java.util.ArrayList; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.background.db.Tab; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.Utils; - -import android.content.ContentValues; - -/** - * Represents a client's collection of tabs. - * - * @author rnewman - * - */ -public class TabsRecord extends Record { - public static final String LOG_TAG = "TabsRecord"; - - public static final String COLLECTION_NAME = "tabs"; - public static final long TABS_TTL = 7 * 24 * 60 * 60; // 7 days in seconds. - - public TabsRecord(String guid, String collection, long lastModified, boolean deleted) { - super(guid, collection, lastModified, deleted); - this.ttl = TABS_TTL; - } - public TabsRecord(String guid, String collection, long lastModified) { - this(guid, collection, lastModified, false); - } - public TabsRecord(String guid, String collection) { - this(guid, collection, 0, false); - } - public TabsRecord(String guid) { - this(guid, COLLECTION_NAME, 0, false); - } - public TabsRecord() { - this(Utils.generateGuid(), COLLECTION_NAME, 0, false); - } - - public String clientName; - public ArrayList<Tab> tabs; - - @Override - public void initFromPayload(ExtendedJSONObject payload) { - clientName = (String) payload.get("clientName"); - try { - tabs = tabsFrom(payload.getArray("tabs")); - } catch (NonArrayJSONException e) { - // Oh well. - tabs = new ArrayList<Tab>(); - } - } - - @SuppressWarnings("unchecked") - protected static JSONArray tabsToJSON(ArrayList<Tab> tabs) { - JSONArray out = new JSONArray(); - for (Tab tab : tabs) { - out.add(tabToJSONObject(tab)); - } - return out; - } - - protected static ArrayList<Tab> tabsFrom(JSONArray in) { - ArrayList<Tab> tabs = new ArrayList<Tab>(in.size()); - for (Object o : in) { - if (o instanceof JSONObject) { - try { - tabs.add(TabsRecord.tabFromJSONObject((JSONObject) o)); - } catch (NonArrayJSONException e) { - Logger.warn(LOG_TAG, "urlHistory is not an array for this tab.", e); - } - } - } - return tabs; - } - - @Override - public void populatePayload(ExtendedJSONObject payload) { - putPayload(payload, "id", this.guid); - putPayload(payload, "clientName", this.clientName); - payload.put("tabs", tabsToJSON(this.tabs)); - } - - @Override - public Record copyWithIDs(String guid, long androidID) { - TabsRecord out = new TabsRecord(guid, this.collection, this.lastModified, this.deleted); - out.androidID = androidID; - out.sortIndex = this.sortIndex; - out.ttl = this.ttl; - - out.clientName = this.clientName; - out.tabs = new ArrayList<Tab>(this.tabs); - - return out; - } - - public ContentValues getClientsContentValues() { - ContentValues cv = new ContentValues(); - cv.put(BrowserContract.Clients.GUID, this.guid); - cv.put(BrowserContract.Clients.NAME, this.clientName); - cv.put(BrowserContract.Clients.LAST_MODIFIED, this.lastModified); - return cv; - } - - public ContentValues[] getTabsContentValues() { - int c = tabs.size(); - ContentValues[] out = new ContentValues[c]; - for (int i = 0; i < c; i++) { - out[i] = tabs.get(i).toContentValues(this.guid, i); - } - return out; - } - - public static Tab tabFromJSONObject(JSONObject o) throws NonArrayJSONException { - ExtendedJSONObject obj = new ExtendedJSONObject(o); - String title = obj.getString("title"); - String icon = obj.getString("icon"); - JSONArray history = obj.getArray("urlHistory"); - - // Last used is inexplicably a string in seconds. Most of the time. - long lastUsed = 0; - Object lU = obj.get("lastUsed"); - if (lU instanceof Number) { - lastUsed = ((Long) lU) * 1000L; - } else if (lU instanceof String) { - try { - lastUsed = Long.parseLong((String) lU, 10) * 1000L; - } catch (NumberFormatException e) { - Logger.debug(TabsRecord.LOG_TAG, "Invalid number format in lastUsed: " + lU); - } - } - return new Tab(title, icon, history, lastUsed); - } - - @SuppressWarnings("unchecked") - public static JSONObject tabToJSONObject(Tab tab) { - JSONObject o = new JSONObject(); - o.put("title", tab.title); - o.put("icon", tab.icon); - o.put("urlHistory", tab.history); - o.put("lastUsed", tab.lastUsed / 1000); - return o; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java deleted file mode 100644 index 9504434d8..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/TabsRecordFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.repositories.RecordFactory; - -public class TabsRecordFactory extends RecordFactory { - @Override - public Record createRecord(Record record) { - TabsRecord r = new TabsRecord(); - r.initFromEnvelope((CryptoRecord) record); - return r; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java deleted file mode 100644 index 2d3d4fd32..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/VersionConstants.java +++ /dev/null @@ -1,14 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.domain; - -public class VersionConstants { - public static final int BOOKMARKS_ENGINE_VERSION = 2; - public static final int CLIENTS_ENGINE_VERSION = 1; - public static final int FORMS_ENGINE_VERSION = 1; - public static final int HISTORY_ENGINE_VERSION = 1; - public static final int PASSWORDS_ENGINE_VERSION = 1; - public static final int TABS_ENGINE_VERSION = 1; -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java deleted file mode 100644 index 5c3037e4d..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloader.java +++ /dev/null @@ -1,310 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.downloaders; - -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.DelayedWorkTracker; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.repositories.Server11Repository; -import org.mozilla.gecko.sync.repositories.Server11RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * Batching Downloader, which implements batching protocol as supported by Sync 1.5. - * - * Downloader's batching behaviour is configured via two parameters, obtained from the repository: - * - Per-batch limit, which specified how many records may be fetched in an individual GET request. - * - Total limit, which controls number of batch GET requests we will make. - * - * - * Batching is implemented via specifying a 'limit' GET parameter, and looking for an 'offset' token - * in the response. If offset token is present, this indicates that there are more records than what - * we've received so far, and we perform an additional fetch. Batching stops when either we hit a total - * limit, or offset token is no longer present (indicating that we're done). - * - * For unlimited repositories (such as passwords), both of these value will be -1. Downloader will not - * specify a limit parameter in this case, and the response will contain every record available and no - * offset token, thus fully completing in one go. - * - * In between batches, we maintain a Last-Modified timestamp, based off the value return in the header - * of the first response. Every response will have a Last-Modified header, indicating when the collection - * was modified last. We pass along this header in our subsequent requests in a X-If-Unmodified-Since - * header. Server will ensure that our collection did not change while we are batching, if it did it will - * fail our fetch with a 412 (Consequent Modification) error. Additionally, we perform the same checks - * locally. - */ -public class BatchingDownloader { - public static final String LOG_TAG = "BatchingDownloader"; - - protected final Server11Repository repository; - private final Server11RepositorySession repositorySession; - private final DelayedWorkTracker workTracker = new DelayedWorkTracker(); - // Used to track outstanding requests, so that we can abort them as needed. - @VisibleForTesting - protected final Set<SyncStorageCollectionRequest> pending = Collections.synchronizedSet(new HashSet<SyncStorageCollectionRequest>()); - /* @GuardedBy("this") */ private String lastModified; - /* @GuardedBy("this") */ private long numRecords = 0; - - public BatchingDownloader(final Server11Repository repository, final Server11RepositorySession repositorySession) { - this.repository = repository; - this.repositorySession = repositorySession; - } - - @VisibleForTesting - protected static String flattenIDs(String[] guids) { - // Consider using Utils.toDelimitedString if and when the signature changes - // to Collection<String> guids. - if (guids.length == 0) { - return ""; - } - if (guids.length == 1) { - return guids[0]; - } - // Assuming 12-char GUIDs. There should be a -1 in there, but we accumulate one comma too many. - StringBuilder b = new StringBuilder(guids.length * 12 + guids.length); - for (String guid : guids) { - b.append(guid); - b.append(","); - } - return b.substring(0, b.length() - 1); - } - - @VisibleForTesting - protected void fetchWithParameters(long newer, - long batchLimit, - boolean full, - String sort, - String ids, - SyncStorageCollectionRequest request, - RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) - throws URISyntaxException, UnsupportedEncodingException { - if (batchLimit > repository.getDefaultTotalLimit()) { - throw new IllegalArgumentException("Batch limit should not be greater than total limit"); - } - - request.delegate = new BatchingDownloaderDelegate(this, fetchRecordsDelegate, request, - newer, batchLimit, full, sort, ids); - this.pending.add(request); - request.get(); - } - - @VisibleForTesting - @Nullable - protected String encodeParam(String param) throws UnsupportedEncodingException { - if (param != null) { - return URLEncoder.encode(param, "UTF-8"); - } - return null; - } - - @VisibleForTesting - protected SyncStorageCollectionRequest makeSyncStorageCollectionRequest(long newer, - long batchLimit, - boolean full, - String sort, - String ids, - String offset) - throws URISyntaxException, UnsupportedEncodingException { - URI collectionURI = repository.collectionURI(full, newer, batchLimit, sort, ids, encodeParam(offset)); - Logger.debug(LOG_TAG, collectionURI.toString()); - - return new SyncStorageCollectionRequest(collectionURI); - } - - public void fetchSince(long timestamp, RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { - this.fetchSince(timestamp, null, fetchRecordsDelegate); - } - - private void fetchSince(long timestamp, String offset, - RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { - long batchLimit = repository.getDefaultBatchLimit(); - String sort = repository.getDefaultSort(); - - try { - SyncStorageCollectionRequest request = makeSyncStorageCollectionRequest(timestamp, - batchLimit, true, sort, null, offset); - this.fetchWithParameters(timestamp, batchLimit, true, sort, null, request, fetchRecordsDelegate); - } catch (URISyntaxException | UnsupportedEncodingException e) { - fetchRecordsDelegate.onFetchFailed(e, null); - } - } - - public void fetch(String[] guids, RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { - String ids = flattenIDs(guids); - String index = "index"; - - try { - SyncStorageCollectionRequest request = makeSyncStorageCollectionRequest( - -1, -1, true, index, ids, null); - this.fetchWithParameters(-1, -1, true, index, ids, request, fetchRecordsDelegate); - } catch (URISyntaxException | UnsupportedEncodingException e) { - fetchRecordsDelegate.onFetchFailed(e, null); - } - } - - public Server11Repository getServerRepository() { - return this.repository; - } - - public void onFetchCompleted(SyncStorageResponse response, - final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate, - final SyncStorageCollectionRequest request, long newer, - long limit, boolean full, String sort, String ids) { - removeRequestFromPending(request); - - // When we process our first request, we get back a X-Last-Modified header indicating when collection was modified last. - // We pass it to the server with every subsequent request (if we need to make more) as the X-If-Unmodified-Since header, - // and server is supposed to ensure that this pre-condition is met, and fail our request with a 412 error code otherwise. - // So, if all of this happens, these checks should never fail. - // However, we also track this header in client side, and can defensively validate against it here as well. - final String currentLastModifiedTimestamp = response.lastModified(); - Logger.debug(LOG_TAG, "Last modified timestamp " + currentLastModifiedTimestamp); - - // Sanity check. We also did a null check in delegate before passing it into here. - if (currentLastModifiedTimestamp == null) { - this.abort(fetchRecordsDelegate, "Last modified timestamp is missing"); - return; - } - - final boolean lastModifiedChanged; - synchronized (this) { - if (this.lastModified == null) { - // First time seeing last modified timestamp. - this.lastModified = currentLastModifiedTimestamp; - } - lastModifiedChanged = !this.lastModified.equals(currentLastModifiedTimestamp); - } - - if (lastModifiedChanged) { - this.abort(fetchRecordsDelegate, "Last modified timestamp has changed unexpectedly"); - return; - } - - final boolean hasNotReachedLimit; - synchronized (this) { - this.numRecords += response.weaveRecords(); - hasNotReachedLimit = this.numRecords < repository.getDefaultTotalLimit(); - } - - final String offset = response.weaveOffset(); - final SyncStorageCollectionRequest newRequest; - try { - newRequest = makeSyncStorageCollectionRequest(newer, - limit, full, sort, ids, offset); - } catch (final URISyntaxException | UnsupportedEncodingException e) { - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Delayed onFetchCompleted running."); - fetchRecordsDelegate.onFetchFailed(e, null); - } - }); - return; - } - - if (offset != null && hasNotReachedLimit) { - try { - this.fetchWithParameters(newer, limit, full, sort, ids, newRequest, fetchRecordsDelegate); - } catch (final URISyntaxException | UnsupportedEncodingException e) { - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Delayed onFetchCompleted running."); - fetchRecordsDelegate.onFetchFailed(e, null); - } - }); - } - return; - } - - final long normalizedTimestamp = response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED); - Logger.debug(LOG_TAG, "Fetch completed. Timestamp is " + normalizedTimestamp); - - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Delayed onFetchCompleted running."); - fetchRecordsDelegate.onFetchCompleted(normalizedTimestamp); - } - }); - } - - public void onFetchFailed(final Exception ex, - final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate, - final SyncStorageCollectionRequest request) { - removeRequestFromPending(request); - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Running onFetchFailed."); - fetchRecordsDelegate.onFetchFailed(ex, null); - } - }); - } - - public void onFetchedRecord(CryptoRecord record, - RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { - this.workTracker.incrementOutstanding(); - try { - fetchRecordsDelegate.onFetchedRecord(record); - } catch (Exception ex) { - Logger.warn(LOG_TAG, "Got exception calling onFetchedRecord with WBO.", ex); - throw new RuntimeException(ex); - } finally { - this.workTracker.decrementOutstanding(); - } - } - - private void removeRequestFromPending(SyncStorageCollectionRequest request) { - if (request == null) { - return; - } - this.pending.remove(request); - } - - @VisibleForTesting - protected void abortRequests() { - this.repositorySession.abort(); - synchronized (this.pending) { - for (SyncStorageCollectionRequest request : this.pending) { - request.abort(); - } - this.pending.clear(); - } - } - - @Nullable - protected synchronized String getLastModified() { - return this.lastModified; - } - - private void abort(final RepositorySessionFetchRecordsDelegate delegate, final String msg) { - Logger.error(LOG_TAG, msg); - this.abortRequests(); - this.workTracker.delayWorkItem(new Runnable() { - @Override - public void run() { - Logger.debug(LOG_TAG, "Delayed onFetchCompleted running."); - delegate.onFetchFailed( - new IllegalStateException(msg), - null); - } - }); - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java deleted file mode 100644 index eb9f76d6b..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegate.java +++ /dev/null @@ -1,91 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.downloaders; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.net.WBOCollectionRequestDelegate; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; - -/** - * Delegate that gets passed into fetch methods to handle server response from fetch. - */ -public class BatchingDownloaderDelegate extends WBOCollectionRequestDelegate { - public static final String LOG_TAG = "BatchingDownloaderDelegate"; - - private BatchingDownloader downloader; - private RepositorySessionFetchRecordsDelegate fetchRecordsDelegate; - public SyncStorageCollectionRequest request; - // Used to pass back to BatchDownloader to start another fetch with these parameters if needed. - private long newer; - private long batchLimit; - private boolean full; - private String sort; - private String ids; - - public BatchingDownloaderDelegate(final BatchingDownloader downloader, - final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate, - final SyncStorageCollectionRequest request, long newer, - long batchLimit, boolean full, String sort, String ids) { - this.downloader = downloader; - this.fetchRecordsDelegate = fetchRecordsDelegate; - this.request = request; - this.newer = newer; - this.batchLimit = batchLimit; - this.full = full; - this.sort = sort; - this.ids = ids; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return this.downloader.getServerRepository().getAuthHeaderProvider(); - } - - @Override - public String ifUnmodifiedSince() { - return this.downloader.getLastModified(); - } - - @Override - public void handleRequestSuccess(SyncStorageResponse response) { - Logger.debug(LOG_TAG, "Fetch done."); - if (response.lastModified() != null) { - this.downloader.onFetchCompleted(response, this.fetchRecordsDelegate, this.request, - this.newer, this.batchLimit, this.full, this.sort, this.ids); - return; - } - this.downloader.onFetchFailed( - new IllegalStateException("Missing last modified header from response"), - this.fetchRecordsDelegate, - this.request); - } - - @Override - public void handleRequestFailure(SyncStorageResponse response) { - this.handleRequestError(new HTTPFailureException(response)); - } - - @Override - public void handleRequestError(final Exception ex) { - Logger.warn(LOG_TAG, "Got request error.", ex); - this.downloader.onFetchFailed(ex, this.fetchRecordsDelegate, this.request); - } - - @Override - public void handleWBO(CryptoRecord record) { - this.downloader.onFetchedRecord(record, this.fetchRecordsDelegate); - } - - @Override - public KeyBundle keyBundle() { - return null; - } -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java deleted file mode 100644 index 951588586..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchMeta.java +++ /dev/null @@ -1,165 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.support.annotation.CheckResult; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import org.mozilla.gecko.background.common.log.Logger; - -import java.util.ArrayList; -import java.util.List; - -import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.TokenModifiedException; -import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.LastModifiedChangedUnexpectedly; -import org.mozilla.gecko.sync.repositories.uploaders.BatchingUploader.LastModifiedDidNotChange; - -/** - * Keeps track of token, Last-Modified value and GUIDs of succeeded records. - */ -/* @ThreadSafe */ -public class BatchMeta extends BufferSizeTracker { - private static final String LOG_TAG = "BatchMeta"; - - // Will be set once first payload upload succeeds. We don't expect this to change until we - // commit the batch, and which point it must change. - /* @GuardedBy("this") */ private Long lastModified; - - // Will be set once first payload upload succeeds. We don't expect this to ever change until - // a commit succeeds, at which point this gets set to null. - /* @GuardedBy("this") */ private String token; - - /* @GuardedBy("accessLock") */ private boolean isUnlimited = false; - - // Accessed by synchronously running threads. - /* @GuardedBy("accessLock") */ private final List<String> successRecordGuids = new ArrayList<>(); - - /* @GuardedBy("accessLock") */ private boolean needsCommit = false; - - protected final Long collectionLastModified; - - public BatchMeta(@NonNull Object payloadLock, long maxBytes, long maxRecords, @Nullable Long collectionLastModified) { - super(payloadLock, maxBytes, maxRecords); - this.collectionLastModified = collectionLastModified; - } - - protected void setIsUnlimited(boolean isUnlimited) { - synchronized (accessLock) { - this.isUnlimited = isUnlimited; - } - } - - @Override - protected boolean canFit(long recordDeltaByteCount) { - synchronized (accessLock) { - return isUnlimited || super.canFit(recordDeltaByteCount); - } - } - - @Override - @CheckResult - protected boolean addAndEstimateIfFull(long recordDeltaByteCount) { - synchronized (accessLock) { - needsCommit = true; - boolean isFull = super.addAndEstimateIfFull(recordDeltaByteCount); - return !isUnlimited && isFull; - } - } - - protected boolean needToCommit() { - synchronized (accessLock) { - return needsCommit; - } - } - - protected synchronized String getToken() { - return token; - } - - protected synchronized void setToken(final String newToken, boolean isCommit) throws TokenModifiedException { - // Set token once in a batching mode. - // In a non-batching mode, this.token and newToken will be null, and this is a no-op. - if (token == null) { - token = newToken; - return; - } - - // Sanity checks. - if (isCommit) { - // We expect token to be null when commit payload succeeds. - if (newToken != null) { - throw new TokenModifiedException(); - } else { - token = null; - } - return; - } - - // We expect new token to always equal current token for non-commit payloads. - if (!token.equals(newToken)) { - throw new TokenModifiedException(); - } - } - - protected synchronized Long getLastModified() { - if (lastModified == null) { - return collectionLastModified; - } - return lastModified; - } - - protected synchronized void setLastModified(final Long newLastModified, final boolean expectedToChange) throws LastModifiedChangedUnexpectedly, LastModifiedDidNotChange { - if (lastModified == null) { - lastModified = newLastModified; - return; - } - - if (!expectedToChange && !lastModified.equals(newLastModified)) { - Logger.debug(LOG_TAG, "Last-Modified timestamp changed when we didn't expect it"); - throw new LastModifiedChangedUnexpectedly(); - - } else if (expectedToChange && lastModified.equals(newLastModified)) { - Logger.debug(LOG_TAG, "Last-Modified timestamp did not change when we expected it to"); - throw new LastModifiedDidNotChange(); - - } else { - lastModified = newLastModified; - } - } - - protected ArrayList<String> getSuccessRecordGuids() { - synchronized (accessLock) { - return new ArrayList<>(this.successRecordGuids); - } - } - - protected void recordSucceeded(final String recordGuid) { - // Sanity check. - if (recordGuid == null) { - throw new IllegalStateException(); - } - - synchronized (accessLock) { - successRecordGuids.add(recordGuid); - } - } - - @Override - protected boolean canFitRecordByteDelta(long byteDelta, long recordCount, long byteCount) { - return isUnlimited || super.canFitRecordByteDelta(byteDelta, recordCount, byteCount); - } - - @Override - protected void reset() { - synchronized (accessLock) { - super.reset(); - token = null; - lastModified = null; - successRecordGuids.clear(); - needsCommit = false; - } - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java deleted file mode 100644 index 26efbd136..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploader.java +++ /dev/null @@ -1,344 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.net.Uri; -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.InfoConfiguration; -import org.mozilla.gecko.sync.Server11RecordPostFailedException; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageResponse; -import org.mozilla.gecko.sync.repositories.Server11RepositorySession; -import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; -import org.mozilla.gecko.sync.repositories.domain.Record; - -import java.util.ArrayList; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Uploader which implements batching introduced in Sync 1.5. - * - * Batch vs payload terminology: - * - batch is comprised of a series of payloads, which are all committed at the same time. - * -- identified via a "batch token", which is returned after first payload for the batch has been uploaded. - * - payload is a collection of records which are uploaded together. Associated with a batch. - * -- last payload, identified via commit=true, commits the batch. - * - * Limits for how many records can fit into a payload and into a batch are defined in the passed-in - * InfoConfiguration object. - * - * If we can't fit everything we'd like to upload into one batch (according to max-total-* limits), - * then we commit that batch, and start a new one. There are no explicit limits on total number of - * batches we might use, although at some point we'll start to run into storage limit errors from the API. - * - * Once we go past using one batch this uploader is no longer "atomic". Partial state is exposed - * to other clients after our first batch is committed and before our last batch is committed. - * However, our per-batch limits are high, X-I-U-S mechanics help protect downloading clients - * (as long as they implement X-I-U-S) with 412 error codes in case of interleaving upload and download, - * and most mobile clients will not be uploading large-enough amounts of data (especially structured - * data, such as bookmarks). - * - * Last-Modified header returned with the first batch payload POST success is maintained for a batch, - * to guard against concurrent-modification errors (different uploader commits before we're done). - * - * Non-batching mode notes: - * We also support Sync servers which don't enable batching for uploads. In this case, we respect - * payload limits for individual uploads, and every upload is considered a commit. Batching limits - * do not apply, and batch token is irrelevant. - * We do keep track of Last-Modified and send along X-I-U-S with our uploads, to protect against - * concurrent modifications by other clients. - */ -public class BatchingUploader { - private static final String LOG_TAG = "BatchingUploader"; - - private final Uri collectionUri; - - private volatile boolean recordUploadFailed = false; - - private final BatchMeta batchMeta; - private final Payload payload; - - // Accessed by synchronously running threads, OK to not synchronize and just make it volatile. - private volatile Boolean inBatchingMode; - - // Used to ensure we have thread-safe access to the following: - // - byte and record counts in both Payload and BatchMeta objects - // - buffers in the Payload object - private final Object payloadLock = new Object(); - - protected Executor workQueue; - protected final RepositorySessionStoreDelegate sessionStoreDelegate; - protected final Server11RepositorySession repositorySession; - - protected AtomicLong uploadTimestamp = new AtomicLong(0); - - protected static final int PER_RECORD_OVERHEAD_BYTE_COUNT = RecordUploadRunnable.RECORD_SEPARATOR.length; - protected static final int PER_PAYLOAD_OVERHEAD_BYTE_COUNT = RecordUploadRunnable.RECORDS_END.length; - - // Sanity check. RECORD_SEPARATOR and RECORD_START are assumed to be of the same length. - static { - if (RecordUploadRunnable.RECORD_SEPARATOR.length != RecordUploadRunnable.RECORDS_START.length) { - throw new IllegalStateException("Separator and start tokens must be of the same length"); - } - } - - public BatchingUploader(final Server11RepositorySession repositorySession, final Executor workQueue, final RepositorySessionStoreDelegate sessionStoreDelegate) { - this.repositorySession = repositorySession; - this.workQueue = workQueue; - this.sessionStoreDelegate = sessionStoreDelegate; - this.collectionUri = Uri.parse(repositorySession.getServerRepository().collectionURI().toString()); - - InfoConfiguration config = repositorySession.getServerRepository().getInfoConfiguration(); - this.batchMeta = new BatchMeta( - payloadLock, config.maxTotalBytes, config.maxTotalRecords, - repositorySession.getServerRepository().getCollectionLastModified() - ); - this.payload = new Payload(payloadLock, config.maxPostBytes, config.maxPostRecords); - } - - public void process(final Record record) { - final String guid = record.guid; - final byte[] recordBytes = record.toJSONBytes(); - final long recordDeltaByteCount = recordBytes.length + PER_RECORD_OVERHEAD_BYTE_COUNT; - - Logger.debug(LOG_TAG, "Processing a record with guid: " + guid); - - // We can't upload individual records which exceed our payload byte limit. - if ((recordDeltaByteCount + PER_PAYLOAD_OVERHEAD_BYTE_COUNT) > payload.maxBytes) { - sessionStoreDelegate.onRecordStoreFailed(new RecordTooLargeToUpload(), guid); - return; - } - - synchronized (payloadLock) { - final boolean canFitRecordIntoBatch = batchMeta.canFit(recordDeltaByteCount); - final boolean canFitRecordIntoPayload = payload.canFit(recordDeltaByteCount); - - // Record fits! - if (canFitRecordIntoBatch && canFitRecordIntoPayload) { - Logger.debug(LOG_TAG, "Record fits into the current batch and payload"); - addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid); - - // Payload won't fit the record. - } else if (canFitRecordIntoBatch) { - Logger.debug(LOG_TAG, "Current payload won't fit incoming record, uploading payload."); - flush(false, false); - - Logger.debug(LOG_TAG, "Recording the incoming record into a new payload"); - - // Keep track of the overflow record. - addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid); - - // Batch won't fit the record. - } else { - Logger.debug(LOG_TAG, "Current batch won't fit incoming record, committing batch."); - flush(true, false); - - Logger.debug(LOG_TAG, "Recording the incoming record into a new batch"); - batchMeta.reset(); - - // Keep track of the overflow record. - addAndFlushIfNecessary(recordDeltaByteCount, recordBytes, guid); - } - } - } - - // Convenience function used from the process method; caller must hold a payloadLock. - private void addAndFlushIfNecessary(long byteCount, byte[] recordBytes, String guid) { - boolean isPayloadFull = payload.addAndEstimateIfFull(byteCount, recordBytes, guid); - boolean isBatchFull = batchMeta.addAndEstimateIfFull(byteCount); - - // Preemptive commit batch or upload a payload if they're estimated to be full. - if (isBatchFull) { - flush(true, false); - batchMeta.reset(); - } else if (isPayloadFull) { - flush(false, false); - } - } - - public void noMoreRecordsToUpload() { - Logger.debug(LOG_TAG, "Received 'no more records to upload' signal."); - - // Run this after the last payload succeeds, so that we know for sure if we're in a batching - // mode and need to commit with a potentially empty payload. - workQueue.execute(new Runnable() { - @Override - public void run() { - commitIfNecessaryAfterLastPayload(); - } - }); - } - - @VisibleForTesting - protected void commitIfNecessaryAfterLastPayload() { - // Must be called after last payload upload finishes. - synchronized (payload) { - // If we have any pending records in the Payload, flush them! - if (!payload.isEmpty()) { - flush(true, true); - - // If we have an empty payload but need to commit the batch in the batching mode, flush! - } else if (batchMeta.needToCommit() && Boolean.TRUE.equals(inBatchingMode)) { - flush(true, true); - - // Otherwise, we're done. - } else { - finished(uploadTimestamp); - } - } - } - - /** - * We've been told by our upload delegate that a payload succeeded. - * Depending on the type of payload and batch mode status, inform our delegate of progress. - * - * @param response success response to our commit post - * @param isCommit was this a commit upload? - * @param isLastPayload was this a very last payload we'll upload? - */ - public void payloadSucceeded(final SyncStorageResponse response, final boolean isCommit, final boolean isLastPayload) { - // Sanity check. - if (inBatchingMode == null) { - throw new IllegalStateException("Can't process payload success until we know if we're in a batching mode"); - } - - // We consider records to have been committed if we're not in a batching mode or this was a commit. - // If records have been committed, notify our store delegate. - if (!inBatchingMode || isCommit) { - for (String guid : batchMeta.getSuccessRecordGuids()) { - sessionStoreDelegate.onRecordStoreSucceeded(guid); - } - } - - // If this was our very last commit, we're done storing records. - // Get Last-Modified timestamp from the response, and pass it upstream. - if (isLastPayload) { - finished(response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED)); - } - } - - public void lastPayloadFailed() { - finished(uploadTimestamp); - } - - private void finished(long lastModifiedTimestamp) { - bumpTimestampTo(uploadTimestamp, lastModifiedTimestamp); - finished(uploadTimestamp); - } - - private void finished(AtomicLong lastModifiedTimestamp) { - repositorySession.storeDone(lastModifiedTimestamp.get()); - } - - public BatchMeta getCurrentBatch() { - return batchMeta; - } - - public void setInBatchingMode(boolean inBatchingMode) { - this.inBatchingMode = inBatchingMode; - - // If we know for sure that we're not in a batching mode, - // consider our batch to be of unlimited size. - this.batchMeta.setIsUnlimited(!inBatchingMode); - } - - public Boolean getInBatchingMode() { - return inBatchingMode; - } - - public void setLastModified(final Long lastModified, final boolean isCommit) throws BatchingUploaderException { - // Sanity check. - if (inBatchingMode == null) { - throw new IllegalStateException("Can't process Last-Modified before we know we're in a batching mode."); - } - - // In non-batching mode, every time we receive a Last-Modified timestamp, we expect it to change - // since records are "committed" (become visible to other clients) on every payload. - // In batching mode, we only expect Last-Modified to change when we commit a batch. - batchMeta.setLastModified(lastModified, isCommit || !inBatchingMode); - } - - public void recordSucceeded(final String recordGuid) { - Logger.debug(LOG_TAG, "Record store succeeded: " + recordGuid); - batchMeta.recordSucceeded(recordGuid); - } - - public void recordFailed(final String recordGuid) { - recordFailed(new Server11RecordPostFailedException(), recordGuid); - } - - public void recordFailed(final Exception e, final String recordGuid) { - Logger.debug(LOG_TAG, "Record store failed for guid " + recordGuid + " with exception: " + e.toString()); - recordUploadFailed = true; - sessionStoreDelegate.onRecordStoreFailed(e, recordGuid); - } - - public Server11RepositorySession getRepositorySession() { - return repositorySession; - } - - private static void bumpTimestampTo(final AtomicLong current, long newValue) { - while (true) { - long existing = current.get(); - if (existing > newValue) { - return; - } - if (current.compareAndSet(existing, newValue)) { - return; - } - } - } - - private void flush(final boolean isCommit, final boolean isLastPayload) { - final ArrayList<byte[]> outgoing; - final ArrayList<String> outgoingGuids; - final long byteCount; - - // Even though payload object itself is thread-safe, we want to ensure we get these altogether - // as a "unit". Another approach would be to create a wrapper object for these values, but this works. - synchronized (payloadLock) { - outgoing = payload.getRecordsBuffer(); - outgoingGuids = payload.getRecordGuidsBuffer(); - byteCount = payload.getByteCount(); - } - - workQueue.execute(new RecordUploadRunnable( - new BatchingAtomicUploaderMayUploadProvider(), - collectionUri, - batchMeta, - new PayloadUploadDelegate(this, outgoingGuids, isCommit, isLastPayload), - outgoing, - byteCount, - isCommit - )); - - payload.reset(); - } - - private class BatchingAtomicUploaderMayUploadProvider implements MayUploadProvider { - public boolean mayUpload() { - return !recordUploadFailed; - } - } - - public static class BatchingUploaderException extends Exception { - private static final long serialVersionUID = 1L; - } - public static class RecordTooLargeToUpload extends BatchingUploaderException { - private static final long serialVersionUID = 1L; - } - public static class LastModifiedDidNotChange extends BatchingUploaderException { - private static final long serialVersionUID = 1L; - } - public static class LastModifiedChangedUnexpectedly extends BatchingUploaderException { - private static final long serialVersionUID = 1L; - } - public static class TokenModifiedException extends BatchingUploaderException { - private static final long serialVersionUID = 1L; - }; -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java deleted file mode 100644 index 7f4c305f3..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/BufferSizeTracker.java +++ /dev/null @@ -1,103 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.support.annotation.CallSuper; -import android.support.annotation.CheckResult; - -/** - * Implements functionality shared by BatchMeta and Payload objects, namely: - * - keeping track of byte and record counts - * - incrementing those counts when records are added - * - checking if a record can fit - */ -/* @ThreadSafe */ -public abstract class BufferSizeTracker { - protected final Object accessLock; - - /* @GuardedBy("accessLock") */ private long byteCount = BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT; - /* @GuardedBy("accessLock") */ private long recordCount = 0; - /* @GuardedBy("accessLock") */ protected Long smallestRecordByteCount; - - protected final long maxBytes; - protected final long maxRecords; - - public BufferSizeTracker(Object accessLock, long maxBytes, long maxRecords) { - this.accessLock = accessLock; - this.maxBytes = maxBytes; - this.maxRecords = maxRecords; - } - - @CallSuper - protected boolean canFit(long recordDeltaByteCount) { - synchronized (accessLock) { - return canFitRecordByteDelta(recordDeltaByteCount, recordCount, byteCount); - } - } - - protected boolean isEmpty() { - synchronized (accessLock) { - return recordCount == 0; - } - } - - /** - * Adds a record and returns a boolean indicating whether batch is estimated to be full afterwards. - */ - @CheckResult - protected boolean addAndEstimateIfFull(long recordDeltaByteCount) { - synchronized (accessLock) { - // Sanity check. Calling this method when buffer won't fit the record is an error. - if (!canFitRecordByteDelta(recordDeltaByteCount, recordCount, byteCount)) { - throw new IllegalStateException("Buffer size exceeded"); - } - - byteCount += recordDeltaByteCount; - recordCount += 1; - - if (smallestRecordByteCount == null || smallestRecordByteCount > recordDeltaByteCount) { - smallestRecordByteCount = recordDeltaByteCount; - } - - // See if we're full or nearly full after adding a record. - // We're halving smallestRecordByteCount because we're erring - // on the side of "can hopefully fit". We're trying to upload as soon as we know we - // should, but we also need to be mindful of minimizing total number of uploads we make. - return !canFitRecordByteDelta(smallestRecordByteCount / 2, recordCount, byteCount); - } - } - - protected long getByteCount() { - synchronized (accessLock) { - // Ensure we account for payload overhead twice when the batch is empty. - // Payload overhead is either RECORDS_START ("[") or RECORDS_END ("]"), - // and for an empty payload we need account for both ("[]"). - if (recordCount == 0) { - return byteCount + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT; - } - return byteCount; - } - } - - protected long getRecordCount() { - synchronized (accessLock) { - return recordCount; - } - } - - @CallSuper - protected void reset() { - synchronized (accessLock) { - byteCount = BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT; - recordCount = 0; - } - } - - @CallSuper - protected boolean canFitRecordByteDelta(long byteDelta, long recordCount, long byteCount) { - return recordCount < maxRecords - && (byteCount + byteDelta) <= maxBytes; - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java deleted file mode 100644 index a1994cf62..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/MayUploadProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -public interface MayUploadProvider { - boolean mayUpload(); -} diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java deleted file mode 100644 index 1ed9b5798..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/Payload.java +++ /dev/null @@ -1,66 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.support.annotation.CheckResult; - -import java.util.ArrayList; - -/** - * Owns per-payload record byte and recordGuid buffers. - */ -/* @ThreadSafe */ -public class Payload extends BufferSizeTracker { - // Data of outbound records. - /* @GuardedBy("accessLock") */ private final ArrayList<byte[]> recordsBuffer = new ArrayList<>(); - - // GUIDs of outbound records. Used to fail entire payloads. - /* @GuardedBy("accessLock") */ private final ArrayList<String> recordGuidsBuffer = new ArrayList<>(); - - public Payload(Object payloadLock, long maxBytes, long maxRecords) { - super(payloadLock, maxBytes, maxRecords); - } - - @Override - protected boolean addAndEstimateIfFull(long recordDelta) { - throw new UnsupportedOperationException(); - } - - @CheckResult - protected boolean addAndEstimateIfFull(long recordDelta, byte[] recordBytes, String guid) { - synchronized (accessLock) { - recordsBuffer.add(recordBytes); - recordGuidsBuffer.add(guid); - return super.addAndEstimateIfFull(recordDelta); - } - } - - @Override - protected void reset() { - synchronized (accessLock) { - super.reset(); - recordsBuffer.clear(); - recordGuidsBuffer.clear(); - } - } - - protected ArrayList<byte[]> getRecordsBuffer() { - synchronized (accessLock) { - return new ArrayList<>(recordsBuffer); - } - } - - protected ArrayList<String> getRecordGuidsBuffer() { - synchronized (accessLock) { - return new ArrayList<>(recordGuidsBuffer); - } - } - - protected boolean isEmpty() { - synchronized (accessLock) { - return recordsBuffer.isEmpty(); - } - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java deleted file mode 100644 index e8bbb7df6..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java +++ /dev/null @@ -1,185 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import org.json.simple.JSONArray; -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.HTTPFailureException; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.sync.Utils; -import org.mozilla.gecko.sync.net.AuthHeaderProvider; -import org.mozilla.gecko.sync.net.SyncResponse; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; -import org.mozilla.gecko.sync.net.SyncStorageResponse; - -import java.util.ArrayList; - -public class PayloadUploadDelegate implements SyncStorageRequestDelegate { - private static final String LOG_TAG = "PayloadUploadDelegate"; - - private static final String KEY_BATCH = "batch"; - - private final BatchingUploader uploader; - private ArrayList<String> postedRecordGuids; - private final boolean isCommit; - private final boolean isLastPayload; - - public PayloadUploadDelegate(BatchingUploader uploader, ArrayList<String> postedRecordGuids, boolean isCommit, boolean isLastPayload) { - this.uploader = uploader; - this.postedRecordGuids = postedRecordGuids; - this.isCommit = isCommit; - this.isLastPayload = isLastPayload; - } - - @Override - public AuthHeaderProvider getAuthHeaderProvider() { - return uploader.getRepositorySession().getServerRepository().getAuthHeaderProvider(); - } - - @Override - public String ifUnmodifiedSince() { - final Long lastModified = uploader.getCurrentBatch().getLastModified(); - if (lastModified == null) { - return null; - } - return Utils.millisecondsToDecimalSecondsString(lastModified); - } - - @Override - public void handleRequestSuccess(final SyncStorageResponse response) { - // First, do some sanity checking. - if (response.getStatusCode() != 200 && response.getStatusCode() != 202) { - handleRequestError( - new IllegalStateException("handleRequestSuccess received a non-200/202 response: " + response.getStatusCode()) - ); - return; - } - - // We always expect to see a Last-Modified header. It's returned with every success response. - if (!response.httpResponse().containsHeader(SyncResponse.X_LAST_MODIFIED)) { - handleRequestError( - new IllegalStateException("Response did not have a Last-Modified header") - ); - return; - } - - // We expect to be able to parse the response as a JSON object. - final ExtendedJSONObject body; - try { - body = response.jsonObjectBody(); // jsonObjectBody() throws or returns non-null. - } catch (Exception e) { - Logger.error(LOG_TAG, "Got exception parsing POST success body.", e); - this.handleRequestError(e); - return; - } - - // If we got a 200, it could be either a non-batching result, or a batch commit. - // - if we're in a batching mode, we expect this to be a commit. - // If we got a 202, we expect there to be a token present in the response - if (response.getStatusCode() == 200 && uploader.getCurrentBatch().getToken() != null) { - if (uploader.getInBatchingMode() && !isCommit) { - handleRequestError( - new IllegalStateException("Got 200 OK in batching mode, but this was not a commit payload") - ); - return; - } - } else if (response.getStatusCode() == 202) { - if (!body.containsKey(KEY_BATCH)) { - handleRequestError( - new IllegalStateException("Batch response did not have a batch ID") - ); - return; - } - } - - // With sanity checks out of the way, can now safely say if we're in a batching mode or not. - // We only do this once per session. - if (uploader.getInBatchingMode() == null) { - uploader.setInBatchingMode(body.containsKey(KEY_BATCH)); - } - - // Tell current batch about the token we've received. - // Throws if token changed after being set once, or if we got a non-null token after a commit. - try { - uploader.getCurrentBatch().setToken(body.getString(KEY_BATCH), isCommit); - } catch (BatchingUploader.BatchingUploaderException e) { - handleRequestError(e); - return; - } - - // Will throw if Last-Modified changed when it shouldn't have. - try { - uploader.setLastModified( - response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED), - isCommit); - } catch (BatchingUploader.BatchingUploaderException e) { - handleRequestError(e); - return; - } - - // All looks good up to this point, let's process success and failed arrays. - JSONArray success; - try { - success = body.getArray("success"); - } catch (NonArrayJSONException e) { - handleRequestError(e); - return; - } - - if (success != null && !success.isEmpty()) { - Logger.trace(LOG_TAG, "Successful records: " + success.toString()); - for (Object o : success) { - try { - uploader.recordSucceeded((String) o); - } catch (ClassCastException e) { - Logger.error(LOG_TAG, "Got exception parsing POST success guid.", e); - // Not much to be done. - } - } - } - // GC - success = null; - - ExtendedJSONObject failed; - try { - failed = body.getObject("failed"); - } catch (NonObjectJSONException e) { - handleRequestError(e); - return; - } - - if (failed != null && !failed.object.isEmpty()) { - Logger.debug(LOG_TAG, "Failed records: " + failed.object.toString()); - for (String guid : failed.keySet()) { - uploader.recordFailed(guid); - } - } - // GC - failed = null; - - // And we're done! Let uploader finish up. - uploader.payloadSucceeded(response, isCommit, isLastPayload); - } - - @Override - public void handleRequestFailure(final SyncStorageResponse response) { - this.handleRequestError(new HTTPFailureException(response)); - } - - @Override - public void handleRequestError(Exception e) { - for (String guid : postedRecordGuids) { - uploader.recordFailed(e, guid); - } - // GC - postedRecordGuids = null; - - if (isLastPayload) { - uploader.lastPayloadFailed(); - } - } -}
\ No newline at end of file diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java deleted file mode 100644 index ce2955102..000000000 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnable.java +++ /dev/null @@ -1,176 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.sync.repositories.uploaders; - -import android.net.Uri; -import android.support.annotation.VisibleForTesting; - -import org.mozilla.gecko.background.common.log.Logger; -import org.mozilla.gecko.sync.Server11PreviousPostFailedException; -import org.mozilla.gecko.sync.net.SyncStorageRequest; -import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; - -import ch.boye.httpclientandroidlib.entity.ContentProducer; -import ch.boye.httpclientandroidlib.entity.EntityTemplate; - -/** - * Responsible for creating and posting a <code>SyncStorageRequest</code> request object. - */ -public class RecordUploadRunnable implements Runnable { - public final String LOG_TAG = "RecordUploadRunnable"; - - public final static byte[] RECORDS_START = { 91 }; // [ in UTF-8 - public final static byte[] RECORD_SEPARATOR = { 44 }; // , in UTF-8 - public final static byte[] RECORDS_END = { 93 }; // ] in UTF-8 - - private static final String QUERY_PARAM_BATCH = "batch"; - private static final String QUERY_PARAM_TRUE = "true"; - private static final String QUERY_PARAM_BATCH_COMMIT = "commit"; - - private final MayUploadProvider mayUploadProvider; - private final SyncStorageRequestDelegate uploadDelegate; - - private final ArrayList<byte[]> outgoing; - private final long byteCount; - - // Used to construct POST URI during run(). - @VisibleForTesting - public final boolean isCommit; - private final Uri collectionUri; - private final BatchMeta batchMeta; - - public RecordUploadRunnable(MayUploadProvider mayUploadProvider, - Uri collectionUri, - BatchMeta batchMeta, - SyncStorageRequestDelegate uploadDelegate, - ArrayList<byte[]> outgoing, - long byteCount, - boolean isCommit) { - this.mayUploadProvider = mayUploadProvider; - this.uploadDelegate = uploadDelegate; - this.outgoing = outgoing; - this.byteCount = byteCount; - this.batchMeta = batchMeta; - this.collectionUri = collectionUri; - this.isCommit = isCommit; - } - - public static 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(RECORDS_START); - if (count > 0) { - outstream.write(outgoing.get(0)); - for (int i = 1; i < count; ++i) { - outstream.write(RECORD_SEPARATOR); - outstream.write(outgoing.get(i)); - } - } - outstream.write(RECORDS_END); - } - - public static long outgoingBytesCount(ArrayList<byte[]> outgoing) { - final long numberOfRecords = outgoing.size(); - - // Account for start and end tokens. - long count = RECORDS_START.length + RECORDS_END.length; - - // Account for all the records. - for (int i = 0; i < numberOfRecords; i++) { - count += outgoing.get(i).length; - } - - // Account for a separator between the records. - // There's one less separator than there are records. - if (numberOfRecords > 1) { - count += RECORD_SEPARATOR.length * (numberOfRecords - 1); - } - - return count; - } - } - - public static class ByteArraysEntity extends EntityTemplate { - private final 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. - - // Sanity check our byte counts. - long realByteCount = ByteArraysContentProducer.outgoingBytesCount(arrays); - if (realByteCount != totalBytes) { - throw new IllegalStateException("Mismatched byte counts. Received " + totalBytes + " while real byte count is " + realByteCount); - } - } - - @Override - public long getContentLength() { - return count; - } - - @Override - public boolean isRepeatable() { - return true; - } - } - - @Override - public void run() { - if (!mayUploadProvider.mayUpload()) { - Logger.info(LOG_TAG, "Told not to proceed by the uploader. Cancelling upload, failing records."); - uploadDelegate.handleRequestError(new Server11PreviousPostFailedException()); - return; - } - - Logger.trace(LOG_TAG, "Running upload task. Outgoing records: " + outgoing.size()); - - // 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. - - final URI postURI = buildPostURI(isCommit, batchMeta, collectionUri); - final SyncStorageRequest request = new SyncStorageRequest(postURI); - request.delegate = uploadDelegate; - - ByteArraysEntity body = new ByteArraysEntity(outgoing, byteCount); - request.post(body); - } - - @VisibleForTesting - public static URI buildPostURI(boolean isCommit, BatchMeta batchMeta, Uri collectionUri) { - final Uri.Builder uriBuilder = collectionUri.buildUpon(); - final String batchToken = batchMeta.getToken(); - - if (batchToken != null) { - uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH, batchToken); - } else { - uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH, QUERY_PARAM_TRUE); - } - - if (isCommit) { - uriBuilder.appendQueryParameter(QUERY_PARAM_BATCH_COMMIT, QUERY_PARAM_TRUE); - } - - try { - return new URI(uriBuilder.build().toString()); - } catch (URISyntaxException e) { - throw new IllegalStateException("Failed to construct a collection URI", e); - } - } -} |