/* * * * Copyright (C) 2014 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * / */ package org.anhonesteffort.flock.sync; import android.accounts.Account; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentUris; import android.content.OperationApplicationException; import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; import android.util.Log; import android.util.Pair; import org.anhonesteffort.flock.util.guava.Optional; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.UUID; /** * rhodey */ // TODO: I really don't think we need all of these null cursor checks... public abstract class AbstractLocalComponentCollection<T> implements LocalComponentCollection<T> { private static final String TAG = "org.anhonesteffort.flock.sync.AbstractLocalComponentCollection"; protected final ContentProviderClient client; protected final ContentProviderOperationQueue operationQueue; protected final Account account; protected final String remotePath; protected final Long localId; public AbstractLocalComponentCollection(ContentProviderClient client, Account account, String remotePath, Long localId) { this.client = client; operationQueue = new ContentProviderOperationQueue(client); this.account = account; this.remotePath = remotePath; this.localId = localId; } public Account getAccount() { return account; } @Override public String getPath() { return remotePath; } public Long getLocalId() { return localId; } protected abstract Uri getSyncAdapterUri(Uri base); protected abstract Uri handleAddAccountQueryParams(Uri uri); protected abstract Uri getUriForComponents(); protected abstract String getColumnNameCollectionLocalId(); protected abstract String getColumnNameComponentLocalId(); protected abstract String getColumnNameComponentUid(); protected abstract String getColumnNameComponentETag(); protected abstract String getColumnNameDirty(); protected abstract String getColumnNameDeleted(); protected abstract String getColumnNameAccountType(); public List<Long> getNewComponentIds() throws RemoteException { final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), getColumnNameComponentUid()}; final String SELECTION = getColumnNameComponentUid() + " IS NULL AND " + getColumnNameCollectionLocalId() + "=" + localId; Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); List<Long> newIds = new LinkedList<Long>(); if (cursor == null) throw new RemoteException("Content provider client gave us a null cursor!"); while (cursor.moveToNext()) { if (!newIds.contains(cursor.getLong(0))) // android gets weird sometimes :( newIds.add(cursor.getLong(0)); } cursor.close(); return newIds; } public List<Pair<Long, String>> getUpdatedComponentIds() throws RemoteException { final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), getColumnNameComponentUid()}; final String SELECTION = getColumnNameDirty() + "=1 AND " + getColumnNameComponentUid() + " IS NOT NULL AND " + getColumnNameCollectionLocalId() + "=" + localId; Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); List<Pair<Long, String>> idPairs = new LinkedList<Pair<Long, String>>(); if (cursor == null) throw new RemoteException("Content provider client gave us a null cursor!"); while (cursor.moveToNext()) { if (!idPairs.contains(new Pair<Long, String>(cursor.getLong(0), cursor.getString(1)))) // android gets weird sometimes :( idPairs.add(new Pair<Long, String>(cursor.getLong(0), cursor.getString(1))); } cursor.close(); return idPairs; } public List<Pair<Long, String>> getDeletedComponentIds() throws RemoteException { final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), getColumnNameComponentUid()}; final String SELECTION = getColumnNameDeleted() + "=1 AND " + getColumnNameComponentUid() + " IS NOT NULL AND " + getColumnNameCollectionLocalId() + "=" + localId; Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); List<Pair<Long, String>> idPairs = new LinkedList<Pair<Long, String>>(); if (cursor == null) throw new RemoteException("Content provider client gave us a null cursor!"); while (cursor.moveToNext()) { if (!idPairs.contains(new Pair<Long, String>(cursor.getLong(0), cursor.getString(1)))) // android gets weird sometimes :( idPairs.add(new Pair<Long, String>(cursor.getLong(0), cursor.getString(1))); } cursor.close(); return idPairs; } public boolean hasChanges() throws RemoteException { final String[] PROJECTION = new String[]{}; final String SELECTION = "(" + getColumnNameComponentUid() + " IS NULL OR " + getColumnNameDirty() + "=1 OR " + getColumnNameDeleted() + "=1) AND " + getColumnNameCollectionLocalId() + "=" + localId; Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); boolean hasChanges = cursor.moveToNext(); cursor.close(); return hasChanges; } public List<Long> getComponentIds() throws RemoteException { final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), getColumnNameComponentUid()}; String selection = null; if (account != null) { selection = getColumnNameDeleted() + "=0 AND " + getColumnNameCollectionLocalId() + "=" + localId; } else { selection = getColumnNameDeleted() + "=0 AND " + getColumnNameCollectionLocalId() + "=" + localId + " AND " + getColumnNameAccountType() + " IS NULL"; } Cursor cursor = client.query(getUriForComponents(), PROJECTION, selection, null, null); List<Long> componentIds = new LinkedList<Long>(); if (cursor == null) throw new RemoteException("Content provider client gave us a null cursor!"); while (cursor.moveToNext()) { if (!componentIds.contains(cursor.getLong(0))) // android gets weird sometimes :( componentIds.add(cursor.getLong(0)); } cursor.close(); return componentIds; } public Optional<Long> getLocalIdForUid(String uid) throws RemoteException { final String[] PROJECTION = new String[]{getColumnNameComponentLocalId()}; final String[] SELECTION_ARGS = new String[]{uid}; String selection = null; if (account != null) { selection = getColumnNameComponentUid() + "=? AND " + getColumnNameCollectionLocalId() + "=" + localId; } else { selection = getColumnNameComponentUid() + "=? AND " + getColumnNameCollectionLocalId() + "=" + localId + " AND " + getColumnNameAccountType() + " IS NULL"; } Cursor cursor = client.query(getUriForComponents(), PROJECTION, selection, SELECTION_ARGS, null); if (cursor == null) throw new RemoteException("Content provider client gave us a null cursor!"); Optional<Long> result = Optional.absent(); if (cursor.moveToNext()) result = Optional.fromNullable(cursor.getLong(0)); cursor.close(); return result; } public Optional<String> getETagForUid(String uid) throws RemoteException { final String[] PROJECTION = new String[]{getColumnNameComponentETag()}; final String SELECTION = getColumnNameComponentUid() + "=? AND " + getColumnNameCollectionLocalId() + "=" + localId; final String[] SELECTION_ARGS = new String[]{uid}; Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, SELECTION_ARGS, null); if (cursor == null) throw new RemoteException("Content provider client gave us a null cursor!"); Optional<String> result = Optional.absent(); if (cursor.moveToNext()) result = Optional.fromNullable(cursor.getString(0)); cursor.close(); return result; } public Optional<String> getUidForLocalId(Long localId) throws RemoteException { final String[] PROJECTION = new String[]{getColumnNameComponentUid()}; final String SELECTION = getColumnNameComponentLocalId() + "=" + localId + " AND " + getColumnNameCollectionLocalId() + "=" + this.localId; Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); if (cursor == null) throw new RemoteException("Content provider client gave us a null cursor!"); Optional<String> result = Optional.absent(); if (cursor.moveToNext()) result = Optional.fromNullable(cursor.getString(0)); cursor.close(); return result; } public String populateComponentUid(Long localId) throws OperationApplicationException, RemoteException { Optional<String> uid = getUidForLocalId(localId); if (uid.isPresent()) { Log.d(TAG, "populateComponentUid() uid already exists, ignoring"); return uid.get(); } String rand = UUID.randomUUID().toString(); Log.d(TAG, "populateComponentUid() gonna populate " + localId + " with " + rand); operationQueue.queue(ContentProviderOperation .newUpdate(ContentUris.withAppendedId(getUriForComponents(), localId)) .withValue(getColumnNameComponentUid(), rand) .withYieldAllowed(false) .build()); commitPendingOperations(); return rand; } public void removeComponent(Long localId) { Log.d(TAG, "removeComponent() localId " + localId); operationQueue.queue(ContentProviderOperation .newDelete(ContentUris.withAppendedId(getUriForComponents(), localId)) .withYieldAllowed(true) .build()); } @Override public void removeComponent(String remoteUId) throws RemoteException { final String SELECTION = getColumnNameComponentUid() + "=? AND " + getColumnNameCollectionLocalId() + "=" + localId; final String[] SELECTION_ARGS = new String[]{remoteUId}; operationQueue.queue(ContentProviderOperation .newDelete(getUriForComponents()) .withSelection(SELECTION, SELECTION_ARGS) .withYieldAllowed(true) .build()); } @Override public void removeAllComponents() throws RemoteException { final String SELECTION = getColumnNameCollectionLocalId() + "=" + localId; final Uri COMPONENT_URI = getUriForComponents().buildUpon().clearQuery().build(); final Uri CONTENT_URI = handleAddAccountQueryParams(COMPONENT_URI); operationQueue.queue(ContentProviderOperation .newDelete(CONTENT_URI) .withSelection(SELECTION, null) .withYieldAllowed(true) .build()); } @Override public HashMap<String, String> getComponentETags() throws RemoteException { final String[] PROJECTION = new String[]{getColumnNameComponentUid(), getColumnNameComponentETag()}; final String SELECTION = getColumnNameComponentUid() + " IS NOT NULL " + "AND " + getColumnNameComponentETag() + " IS NOT NULL " + "AND " + getColumnNameDeleted() + "=0 AND " + getColumnNameCollectionLocalId() + "=" + localId; Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); HashMap<String, String> pairs = new HashMap<String, String>(); if (cursor == null) throw new RemoteException("Content provider client gave us a null cursor!"); while (cursor.moveToNext()) pairs.put(cursor.getString(0), cursor.getString(1)); cursor.close(); return pairs; } public void cleanComponent(Long localId) { Log.d(TAG, "cleanComponent() localId " + localId); operationQueue.queue(ContentProviderOperation .newUpdate(ContentUris.withAppendedId(getUriForComponents(), localId)) .withValue(getColumnNameDirty(), 0) .build()); } public void dirtyComponent(Long localId) { Log.d(TAG, "dirtyComponent() localId " + localId); operationQueue.queue(ContentProviderOperation .newUpdate(ContentUris.withAppendedId(getUriForComponents(), localId)) .withValue(getColumnNameDirty(), 1).build()); } public void setUidToNull(Long localId) { Log.d(TAG, "setUidToNull() localId " + localId); operationQueue.queue(ContentProviderOperation .newUpdate(ContentUris.withAppendedId(getUriForComponents(), localId)) .withValue(getColumnNameComponentUid(), null) .build()); } public int commitPendingOperations() throws OperationApplicationException, RemoteException { return operationQueue.commit(); } }