diff options
author | Stefan Niedermann <info@niedermann.it> | 2020-12-09 19:59:07 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2020-12-09 19:59:07 +0300 |
commit | df900e53492c7b30cbb90b9180d6c3cdf59f38d9 (patch) | |
tree | b88dc0386e6b448c95cf4fcb46fbeaf8edc8780f /app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync | |
parent | 034ae108ae4ab4c273ef4d74f1bfd39fbc4d8a84 (diff) | |
parent | f29eed9db4c0906fa7887e446cf0325718ef6827 (diff) |
Merge branch 'master' into fastlanefastlane
# Conflicts:
# fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
# fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
# fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
Diffstat (limited to 'app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync')
38 files changed, 1995 insertions, 578 deletions
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java index 9ed9035cf..d93835423 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java @@ -1,31 +1,43 @@ package it.niedermann.nextcloud.deck.persistence.sync; +import android.annotation.SuppressLint; import android.content.Context; import android.database.sqlite.SQLiteConstraintException; import androidx.annotation.AnyThread; +import androidx.annotation.ColorInt; +import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.Size; import androidx.annotation.WorkerThread; import androidx.core.util.Pair; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; +import com.nextcloud.android.sso.api.ParsedResponse; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import java.io.File; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.GsonConfig; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.api.LastSyncUtil; +import it.niedermann.nextcloud.deck.exceptions.DeckException; import it.niedermann.nextcloud.deck.exceptions.OfflineException; import it.niedermann.nextcloud.deck.model.AccessControl; import it.niedermann.nextcloud.deck.model.Account; @@ -36,9 +48,11 @@ import it.niedermann.nextcloud.deck.model.JoinCardWithUser; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.internal.FilterInformation; @@ -46,12 +60,12 @@ import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; -import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; -import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData; import it.niedermann.nextcloud.deck.persistence.sync.helpers.DataPropagationHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AbstractSyncDataProvider; @@ -64,20 +78,23 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.CardPropa import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.DeckCommentsDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.LabelDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.StackDataProvider; -import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWitAclDownSyncDataProvider; -import it.niedermann.nextcloud.deck.util.DateUtil; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithAclDownSyncDataProvider; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithStacksAndLabelsUpSyncDataProvider; +import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; @SuppressWarnings("WeakerAccess") public class SyncManager { @NonNull - private Context appContext; + private final Context appContext; @NonNull - private DataBaseAdapter dataBaseAdapter; + private final DataBaseAdapter dataBaseAdapter; @NonNull - private ServerAdapter serverAdapter; + private final ServerAdapter serverAdapter; + + private static final Map<Long, List<IResponseCallback<Boolean>>> RUNNING_SYNCS = new ConcurrentHashMap<>(); @AnyThread public SyncManager(@NonNull Context context) { @@ -98,62 +115,35 @@ public class SyncManager { } @AnyThread - public MutableLiveData<FullCard> synchronizeCardByRemoteId(long cardRemoteId, @NonNull Account account) { - MutableLiveData<FullCard> liveData = new MutableLiveData<>(); - doAsync(() -> { - Long accountId = account.getId(); - Card card = dataBaseAdapter.getCardByRemoteIdDirectly(accountId, cardRemoteId); - FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); - // only sync this one card. - stack.setCards(Collections.singletonList(card)); - Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - new SyncHelper(serverAdapter, dataBaseAdapter, new Date()).setResponseCallback(new IResponseCallback<Boolean>(account) { - @Override - public void onResponse(Boolean response) { - FullCard fullCard = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, card.getLocalId()); - liveData.postValue(fullCard); - } - - @Override - public void onError(Throwable throwable) { - liveData.postValue(null); - } - }).doSyncFor(new CardDataProvider(null, board, stack)); - }); - return liveData; - } - - // TODO if the card does not exist yet, try to synchronize it first, instead of directly returning null. If sync failed, return null. - @AnyThread public LiveData<Long> getLocalBoardIdByCardRemoteIdAndAccount(long cardRemoteId, @NonNull Account account) { return dataBaseAdapter.getLocalBoardIdByCardRemoteIdAndAccountId(cardRemoteId, account.getId()); } - @AnyThread + @WorkerThread public boolean synchronizeEverything() { List<Account> accounts = dataBaseAdapter.getAllAccountsDirectly(); if (accounts.size() > 0) { - final BooleanResultHolder success = new BooleanResultHolder(); + final AtomicBoolean success = new AtomicBoolean(); CountDownLatch latch = new CountDownLatch(accounts.size()); try { for (Account account : accounts) { new SyncManager(dataBaseAdapter.getContext(), account.getName()).synchronize(new IResponseCallback<Boolean>(account) { @Override public void onResponse(Boolean response) { - success.result = success.result && Boolean.TRUE.equals(response); + success.set(success.get() && Boolean.TRUE.equals(response)); latch.countDown(); } @Override public void onError(Throwable throwable) { - success.result = false; + success.set(false); super.onError(throwable); latch.countDown(); } }); } latch.await(); - return success.result; + return success.get(); } catch (InterruptedException e) { DeckLog.logError(e); return false; @@ -164,76 +154,160 @@ public class SyncManager { @AnyThread public void synchronize(@NonNull IResponseCallback<Boolean> responseCallback) { - if(responseCallback.getAccount() == null) { + synchronize(Collections.singletonList(responseCallback)); + } + + @AnyThread + public void synchronizeBoard(@NonNull IResponseCallback<Boolean> responseCallback, long localBoadId) { + doAsync(() -> { + FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoadId); + try { + new SyncHelper(serverAdapter, dataBaseAdapter, null).setResponseCallback(responseCallback).doSyncFor(new StackDataProvider(null, board)); + } catch (OfflineException e) { + responseCallback.onError(e); + } + }); + } + + @AnyThread + public void synchronizeCard(@NonNull IResponseCallback<Boolean> responseCallback, Card card) { + doAsync(() -> { + FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); + Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); + try { + new SyncHelper(serverAdapter, dataBaseAdapter, null).setResponseCallback(responseCallback).doSyncFor(new CardDataProvider(null, board, stack)); + } catch (OfflineException e) { + responseCallback.onError(e); + } + }); + } + + private void synchronize(@NonNull @Size(min = 1) List<IResponseCallback<Boolean>> responseCallbacks) { + if (responseCallbacks == null || responseCallbacks.size() < 1) { + return; + } + IResponseCallback<Boolean> responseCallback = responseCallbacks.get(0); + Account callbackAccount = responseCallback.getAccount(); + if (callbackAccount == null) { throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + IResponseCallback.class.getSimpleName() + " must not be null."); } - if(responseCallback.getAccount().getId() == null) { + Long callbackAccountId = callbackAccount.getId(); + if (callbackAccountId == null) { throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + IResponseCallback.class.getSimpleName() + " must contain a valid id, but given id was null."); } - doAsync(() -> refreshCapabilities(new IResponseCallback<Capabilities>(responseCallback.getAccount()) { - @Override - public void onResponse(Capabilities response) { - if (!response.isMaintenanceEnabled()) { - if (response.getDeckVersion().isSupported(appContext)) { - long accountId = responseCallback.getAccount().getId(); - Date lastSyncDate = LastSyncUtil.getLastSyncDate(responseCallback.getAccount().getId()); - Date now = DateUtil.nowInGMT(); - - final SyncHelper syncHelper = new SyncHelper(serverAdapter, dataBaseAdapter, lastSyncDate); + List<IResponseCallback<Boolean>> queuedCallbacks = RUNNING_SYNCS.get(callbackAccountId); + if (queuedCallbacks != null) { + queuedCallbacks.addAll(responseCallbacks); + return; + } else { + RUNNING_SYNCS.put(callbackAccountId, new ArrayList<>(responseCallbacks)); + } + doAsync(() -> { + List<IResponseCallback<Boolean>> existingQueue = RUNNING_SYNCS.get(callbackAccountId); + List<IResponseCallback<Boolean>> callbacksQueueForSync = existingQueue == null ? new ArrayList<>() : new ArrayList<>(existingQueue); + refreshCapabilities(new IResponseCallback<Capabilities>(responseCallback.getAccount()) { + @Override + public void onResponse(Capabilities response) { + if (response != null && !response.isMaintenanceEnabled()) { + if (response.getDeckVersion().isSupported(appContext)) { + long accountId = callbackAccountId; + Instant lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId); - IResponseCallback<Boolean> callback = new IResponseCallback<Boolean>(responseCallback.getAccount()) { - @Override - public void onResponse(Boolean response) { - syncHelper.setResponseCallback(new IResponseCallback<Boolean>(account) { - @Override - public void onResponse(Boolean response) { - // TODO deactivate for dev - LastSyncUtil.setLastSyncDate(accountId, now); - responseCallback.onResponse(response); - } + final SyncHelper syncHelper = new SyncHelper(serverAdapter, dataBaseAdapter, lastSyncDate); - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - responseCallback.onError(throwable); - } - }); - doAsync(() -> { - try { - syncHelper.doUpSyncFor(new BoardDataProvider()); - } catch (Throwable e) { - DeckLog.logError(e); - responseCallback.onError(e); - } - }); + IResponseCallback<Boolean> callback = new IResponseCallback<Boolean>(callbackAccount) { + @Override + public void onResponse(Boolean response) { + syncHelper.setResponseCallback(new IResponseCallback<Boolean>(account) { + @Override + public void onResponse(Boolean response) { + // TODO deactivate for dev + LastSyncUtil.setLastSyncDate(accountId, Instant.now()); + respondCallbacksAfterSync(callbacksQueueForSync, response, null); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + respondCallbacksAfterSync(callbacksQueueForSync, null, throwable); + } + }); + doAsync(() -> { + try { + syncHelper.doUpSyncFor(new BoardDataProvider()); + } catch (Throwable e) { + DeckLog.logError(e); + respondCallbacksAfterSync(callbacksQueueForSync, null, e); + } + }); - } + } - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - responseCallback.onError(throwable); - } - }; + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + respondCallbacksAfterSync(callbacksQueueForSync, null, throwable); + } + }; - syncHelper.setResponseCallback(callback); + syncHelper.setResponseCallback(callback); - try { - syncHelper.doSyncFor(new BoardDataProvider()); - } catch (Throwable e) { - DeckLog.logError(e); - responseCallback.onError(e); + try { + syncHelper.doSyncFor(new BoardDataProvider()); + } catch (Throwable e) { + DeckLog.logError(e); + respondCallbacksAfterSync(callbacksQueueForSync, null, e); + } + } else { + respondCallbacksAfterSync(callbacksQueueForSync, Boolean.FALSE, null); + DeckLog.warn("No sync. Server version not supported: " + response.getDeckVersion().getOriginalVersion()); } } else { - responseCallback.onResponse(false); - DeckLog.warn("No sync. Server version not supported: " + response.getDeckVersion().getOriginalVersion()); + respondCallbacksAfterSync(callbacksQueueForSync, Boolean.FALSE, null); + if (response != null) { + DeckLog.warn("No sync. Status maintenance mode: " + response.isMaintenanceEnabled()); + } } - } else { - responseCallback.onResponse(false); - DeckLog.warn("No sync. Status maintenance mode: " + response.isMaintenanceEnabled()); } + }); + }); + } + + private void respondCallbacksAfterSync(List<IResponseCallback<Boolean>> callbacksQueueForSync, Boolean response, Throwable throwable) { + if (callbacksQueueForSync == null || callbacksQueueForSync.isEmpty()) { + return; + } + // notify done callbacks + DeckLog.info("SyncQueue: responding sync for " + callbacksQueueForSync.size() + " queued callbacks!"); + List<IResponseCallback<Boolean>> callbacksQueue = new ArrayList<>(callbacksQueueForSync); + if (throwable == null) { + //success: + for (IResponseCallback<Boolean> callback : callbacksQueue) { + if (callback != null) callback.onResponse(response); } - })); + } else { + // failure: + for (IResponseCallback<Boolean> callback : callbacksQueue) { + if (callback != null) callback.onError(throwable); + } + } + // remove done callbacks from queue + IResponseCallback<Boolean> firstCallbackOfAccount = callbacksQueue.iterator().next(); + List<IResponseCallback<Boolean>> queuedCallbacks = RUNNING_SYNCS.get(firstCallbackOfAccount.getAccount().getId()); + if (queuedCallbacks == null) { + return; + } + for (IResponseCallback<Boolean> callback : callbacksQueue) { + queuedCallbacks.remove(callback); + } + // cleanup if done, or proceed if not + if (queuedCallbacks.isEmpty()) { + RUNNING_SYNCS.remove(firstCallbackOfAccount.getAccount().getId()); + } else { + DeckLog.info("SyncQueue: starting sync for " + queuedCallbacks.size() + " queued callbacks!"); + RUNNING_SYNCS.remove(firstCallbackOfAccount.getAccount().getId()); + synchronize(queuedCallbacks); + } } // @@ -274,14 +348,15 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<Account> createAccount(@NonNull Account accout) { - return dataBaseAdapter.createAccount(accout); + public WrappedLiveData<Account> createAccount(@NonNull Account account) { + return dataBaseAdapter.createAccount(account); } public boolean hasInternetConnection() { return serverAdapter.hasInternetConnection(); } + @AnyThread public void deleteAccount(long id) { doAsync(() -> { dataBaseAdapter.deleteAccount(id); @@ -289,10 +364,6 @@ public class SyncManager { }); } - public void updateAccount(Account account) { - dataBaseAdapter.updateAccount(account); - } - @AnyThread public LiveData<Account> readAccount(long id) { return dataBaseAdapter.readAccount(id); @@ -320,7 +391,7 @@ public class SyncManager { * - located at the given {@param host} * - and have the permission to read the board with the given {@param boardRemoteId} (aka the {@link Board} is shared with this {@link User}). */ - @AnyThread + @MainThread public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) { MediatorLiveData<List<Account>> liveData = new MediatorLiveData<>(); liveData.addSource(dataBaseAdapter.readAccountsForHostWithReadAccessToBoard(host, boardRemoteId), accounts -> { @@ -333,7 +404,7 @@ public class SyncManager { public void onResponse(Boolean response) { liveData.postValue(dataBaseAdapter.readAccountsForHostWithReadAccessToBoardDirectly(host, boardRemoteId)); } - }).doSyncFor(new BoardWitAclDownSyncDataProvider()); + }).doSyncFor(new BoardWithAclDownSyncDataProvider()); } }); }); @@ -345,15 +416,17 @@ public class SyncManager { public void refreshCapabilities(@NonNull IResponseCallback<Capabilities> callback) { doAsync(() -> { try { - serverAdapter.getCapabilities(new IResponseCallback<Capabilities>(callback.getAccount()) { + Account accountForEtag = dataBaseAdapter.getAccountByIdDirectly(callback.getAccount().getId()); + serverAdapter.getCapabilities(accountForEtag.getEtag(), new IResponseCallback<ParsedResponse<Capabilities>>(callback.getAccount()) { @Override - public void onResponse(Capabilities response) { + public void onResponse(ParsedResponse<Capabilities> response) { Account acc = dataBaseAdapter.getAccountByIdDirectly(account.getId()); - acc.applyCapabilities(response); + acc.applyCapabilities(response.getResponse(), response.getHeaders().get("ETag")); dataBaseAdapter.updateAccount(acc); - callback.onResponse(response); + callback.onResponse(response.getResponse()); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { if (throwable instanceof NextcloudHttpRequestFailedException) { @@ -361,13 +434,28 @@ public class SyncManager { if (requestFailedException.getStatusCode() == HTTP_UNAVAILABLE && requestFailedException.getCause() != null) { String errorString = requestFailedException.getCause().getMessage(); Capabilities capabilities = GsonConfig.getGson().fromJson(errorString, Capabilities.class); + DeckLog.verbose("HTTP Status " + HTTP_UNAVAILABLE + ": This server seems to be in maintenance mode."); if (capabilities.isMaintenanceEnabled()) { - doAsync(() -> { - onResponse(capabilities); - }); + doAsync(() -> onResponse(ParsedResponse.of(capabilities))); } else { onError(throwable); } + } else if (requestFailedException.getStatusCode() == HTTP_NOT_MODIFIED) { + DeckLog.verbose("HTTP Status " + HTTP_NOT_MODIFIED + ": There haven't been any changes on the server side for this request."); + //could be after maintenance. so we have to at least revert the maintenance flag + doAsync(() -> { + Account acc = dataBaseAdapter.getAccountByIdDirectly(account.getId()); + if (acc.isMaintenanceEnabled()) { + acc.setMaintenanceEnabled(false); + dataBaseAdapter.updateAccount(acc); + } + Capabilities capabilities = new Capabilities(); + capabilities.setMaintenanceEnabled(false); + capabilities.setDeckVersion(acc.getServerDeckVersionAsObject()); + capabilities.setTextColor(acc.getTextColor()); + capabilities.setColor(acc.getColor()); + callback.onResponse(capabilities); + }); } } else { callback.onError(throwable); @@ -377,39 +465,30 @@ public class SyncManager { } catch (OfflineException e) { callback.onError(e); } - - try { - serverAdapter.getAllOcsUsers(new IResponseCallback<OcsUserList>(callback.getAccount()) { - @Override - public void onResponse(OcsUserList response) { - Long accountId = callback.getAccount().getId(); - for (String ocsUserName : response) { - User existingUser = dataBaseAdapter.getUserByUidDirectly(accountId, ocsUserName); - if (existingUser == null) { - // we don't know this user, lets get some details... - serverAdapter.getOcsUserDetails(ocsUserName, new IResponseCallback<OcsUser>(callback.getAccount()) { - @Override - public void onResponse(OcsUser response) { - User newUser = new User(); - newUser.setStatus(DBStatus.UP_TO_DATE.getId()); - newUser.setPrimaryKey(ocsUserName); - newUser.setUid(ocsUserName); - newUser.setDisplayname(response.getDisplayName()); - dataBaseAdapter.createUser(accountId, newUser); - } - }); - } - } - } - }); - } catch (OfflineException ignored) { - // Nothing to do here... - } }); } /** * @param accountId ID of the account + * @return all {@link Board}s no matter if {@link Board#archived} or not. + */ + @SuppressWarnings("JavadocReference") + @AnyThread + public LiveData<List<Board>> getBoards(long accountId) { + return dataBaseAdapter.getBoards(accountId); + } + + /** + * @param localProjectId LocalId of the OcsProject + * @return all {@link OcsProjectResource}s of the Project + */ + @AnyThread + public LiveData<List<OcsProjectResource>> getResourcesForProject(long localProjectId) { + return dataBaseAdapter.getResourcesByLocalProjectId(localProjectId); + } + + /** + * @param accountId ID of the account * @param archived Decides whether only archived or not-archived boards for the specified account will be returned * @return all archived or non-archived <code>Board</code>s depending on <code>archived</code> parameter */ @@ -445,8 +524,8 @@ public class SyncManager { } @AnyThread - public LiveData<FullBoard> createBoard(long accountId, @NonNull Board board) { - MutableLiveData<FullBoard> liveData = new MutableLiveData<>(); + public WrappedLiveData<FullBoard> createBoard(long accountId, @NonNull Board board) { + WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); User owner = dataBaseAdapter.getUserByUidDirectly(accountId, account.getUserName()); @@ -465,6 +544,12 @@ public class SyncManager { public void onResponse(FullBoard response) { liveData.postValue(response); } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable, FullBoard entity) { + liveData.postError(throwable, entity); + } }); } }); @@ -474,58 +559,145 @@ public class SyncManager { /** * Creates a new {@link Board} and adds the same {@link Label} and {@link Stack} as in the origin {@link Board}. * Owner of the target {@link Board} will be the {@link User} with the {@link Account} of {@param targetAccountId}. - * Does <strong>not</strong> clone any {@link Card} or {@link AccessControl} from the origin {@link Board}. + * + * @param cloneCards determines whether or not the cards in this {@link Board} shall be cloned or not + * Does <strong>not</strong> clone any {@link Card} or {@link AccessControl} from the origin {@link Board}. + * <p> + * TODO implement https://github.com/stefan-niedermann/nextcloud-deck/issues/608 */ @AnyThread - public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, String targetBoardTitle, String targetBoardColor) { - WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); + public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards) { + final WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); doAsync(() -> { Account originAccount = dataBaseAdapter.getAccountByIdDirectly(originAccountId); User newOwner = dataBaseAdapter.getUserByUidDirectly(originAccountId, originAccount.getUserName()); + if (newOwner == null) { + liveData.postError(new DeckException(DeckException.Hint.UNKNOWN_ACCOUNT_USER_ID, "User with Account-UID \"" + originAccount.getUserName() + "\" not found.")); + return; + } FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(originAccountId, originBoardLocalId); + String newBoardTitleBaseName = originalBoard.getBoard().getTitle().trim(); + int newBoardTitleCopyIndex = 0; + //already a copy? + String regex = " \\(copy [0-9]+\\)$"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(originalBoard.getBoard().getTitle()); + if (matcher.find()) { + String found = matcher.group(); + newBoardTitleBaseName = newBoardTitleBaseName.substring(0, newBoardTitleBaseName.length() - found.length()); + Matcher indexMatcher = Pattern.compile("[0-9]+").matcher(found); + //noinspection ResultOfMethodCallIgnored + indexMatcher.find(); + String oldIndexString = indexMatcher.group(); + newBoardTitleCopyIndex = Integer.parseInt(oldIndexString); + } + + String newBoardTitle; + do { + newBoardTitleCopyIndex++; + newBoardTitle = newBoardTitleBaseName + " (copy " + newBoardTitleCopyIndex + ")"; + + } while (dataBaseAdapter.getBoardForAccountByNameDirectly(targetAccountId, newBoardTitle) != null); + + originalBoard.setAccountId(targetAccountId); - originalBoard.getBoard().setTitle(targetBoardTitle); - originalBoard.getBoard().setColor(targetBoardColor); - originalBoard.getBoard().setOwnerId(newOwner.getId()); - originalBoard.setStatusEnum(DBStatus.LOCAL_EDITED); - originalBoard.setOwner(newOwner); originalBoard.setId(null); originalBoard.setLocalId(null); + originalBoard.getBoard().setTitle(newBoardTitle); + originalBoard.getBoard().setColor(String.format("%06X", 0xFFFFFF & targetBoardColor)); + originalBoard.getBoard().setOwnerId(newOwner.getLocalId()); + originalBoard.setStatusEnum(DBStatus.LOCAL_EDITED); + originalBoard.setOwner(newOwner); long newBoardId = dataBaseAdapter.createBoardDirectly(originAccountId, originalBoard.getBoard()); originalBoard.setLocalId(newBoardId); - for (Stack stack : originalBoard.getStacks()) { - stack.setLocalId(null); - stack.setId(null); - stack.setStatusEnum(DBStatus.LOCAL_EDITED); - stack.setAccountId(targetAccountId); - stack.setBoardId(newBoardId); - dataBaseAdapter.createStack(targetAccountId, stack); + boolean isSameAccount = targetAccountId == originAccountId; + + if (isSameAccount) { + List<AccessControl> aclList = originalBoard.getParticipants(); + for (AccessControl acl : aclList) { + acl.setLocalId(null); + acl.setId(null); + acl.setBoardId(newBoardId); + dataBaseAdapter.createAccessControl(targetAccountId, acl); + } } + + Map<Long, Long> oldToNewLabelIdsDictionary = new HashMap<>(); + for (Label label : originalBoard.getLabels()) { + Long oldLocalId = label.getLocalId(); label.setLocalId(null); label.setId(null); label.setAccountId(targetAccountId); label.setStatusEnum(DBStatus.LOCAL_EDITED); label.setBoardId(newBoardId); - dataBaseAdapter.createLabel(targetAccountId, label); + long newLocalId = dataBaseAdapter.createLabelDirectly(targetAccountId, label); + oldToNewLabelIdsDictionary.put(oldLocalId, newLocalId); } - Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId); - new SyncHelper(serverAdapter, dataBaseAdapter, null) - .setResponseCallback(new IResponseCallback<Boolean>(targetAccount) { - @Override - public void onResponse(Boolean response) { - liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); - } - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - liveData.postError(throwable); + List<Stack> oldStacks = originalBoard.getStacks(); + for (Stack stack : oldStacks) { + Long oldStackId = stack.getLocalId(); + stack.setLocalId(null); + stack.setId(null); + stack.setStatusEnum(DBStatus.LOCAL_EDITED); + stack.setAccountId(targetAccountId); + stack.setBoardId(newBoardId); + long createdStackId = dataBaseAdapter.createStack(targetAccountId, stack); + if (cloneCards) { + List<FullCard> oldCards = dataBaseAdapter.getFullCardsForStackDirectly(originAccountId, oldStackId, null); + for (FullCard oldCard : oldCards) { + Card newCard = oldCard.getCard(); + newCard.setId(null); + newCard.setUserId(newOwner.getLocalId()); + newCard.setLocalId(null); + newCard.setStackId(createdStackId); + newCard.setAccountId(targetAccountId); + newCard.setStatusEnum(DBStatus.LOCAL_EDITED); + long createdCardId = dataBaseAdapter.createCardDirectly(targetAccountId, newCard); + if (oldCard.getLabels() != null) { + for (Label oldLabel : oldCard.getLabels()) { + Long newLabelId = oldToNewLabelIdsDictionary.get(oldLabel.getLocalId()); + if (newLabelId != null) { + dataBaseAdapter.createJoinCardWithLabel(newLabelId, createdCardId, DBStatus.LOCAL_EDITED); + } else + DeckLog.error("ID of created Label is null! Skipping assignment of \"" + oldLabel.getTitle() + "\"..."); + } + } + if (isSameAccount && oldCard.getAssignedUsers() != null) { + for (User assignedUser : oldCard.getAssignedUsers()) { + dataBaseAdapter.createJoinCardWithUser(assignedUser.getLocalId(), createdCardId, DBStatus.LOCAL_EDITED); + } } - }).doSyncFor(new BoardDataProvider()); + } + } + } + // dont trigger concurrent syncs! + List<IResponseCallback<Boolean>> queuedSync = RUNNING_SYNCS.get(targetAccountId); + if ((queuedSync == null || queuedSync.isEmpty()) && serverAdapter.hasInternetConnection()) { + Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId); + ServerAdapter serverAdapterToUse = this.serverAdapter; + if (originAccountId != targetAccountId) { + serverAdapterToUse = new ServerAdapter(appContext, targetAccount.getName()); + } + new SyncHelper(serverAdapterToUse, dataBaseAdapter, null) + .setResponseCallback(new IResponseCallback<Boolean>(targetAccount) { + @Override + public void onResponse(Boolean response) { + liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); + } + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + liveData.postError(throwable); + } + }).doUpSyncFor(new BoardWithStacksAndLabelsUpSyncDataProvider(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId))); + } else { + liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); + } }); return liveData; } @@ -624,6 +796,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -633,8 +806,8 @@ public class SyncManager { return liveData; } - public LiveData<List<FullStack>> getStacksForBoard(long accountId, long localBoardId) { - return dataBaseAdapter.getFullStacksForBoard(accountId, localBoardId); + public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { + return dataBaseAdapter.getStacksForBoard(accountId, localBoardId); } public LiveData<FullStack> getStack(long accountId, long localStackId) { @@ -651,8 +824,7 @@ public class SyncManager { new AccessControlDataProvider(null, board, Collections.singletonList(entity)), entity, getCallbackToLiveDataConverter(account, liveData), ((entity1, response) -> { response.setBoardId(entity.getBoardId()); response.setUserId(entity.getUser().getLocalId()); - } - ) + }) ); }); return liveData; @@ -687,6 +859,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -711,6 +884,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -725,18 +899,30 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<FullStack> createStack(long accountId, @NonNull Stack stack) { + public WrappedLiveData<FullStack> createStack(long accountId, @NonNull String title, long boardLocalId) { WrappedLiveData<FullStack> liveData = new WrappedLiveData<>(); doAsync(() -> { + Stack stack = new Stack(title, boardLocalId); Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId()); FullStack fullStack = new FullStack(); - // TODO set stack order to (highest stack-order from board) + 1 and remove logic from caller + stack.setOrder(dataBaseAdapter.getHighestStackOrderInBoard(stack.getBoardId()) + 1); stack.setAccountId(accountId); stack.setBoardId(board.getLocalId()); fullStack.setStack(stack); fullStack.setAccountId(accountId); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new StackDataProvider(null, board), fullStack, getCallbackToLiveDataConverter(account, liveData)); + new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new StackDataProvider(null, board), fullStack, new IResponseCallback<FullStack>(account) { + @Override + public void onResponse(FullStack response) { + liveData.postValue(response); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable, FullStack entity) { + liveData.postError(throwable, entity); + } + }); }); return liveData; } @@ -754,36 +940,36 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<FullStack> updateStack(@NonNull FullStack stack) { + public WrappedLiveData<FullStack> updateStackTitle(long localStackId, @NonNull String newTitle) { WrappedLiveData<FullStack> liveData = new WrappedLiveData<>(); doAsync(() -> { + FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(localStackId); + FullBoard fullBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(stack.getAccountId(), stack.getStack().getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(stack.getAccountId()); - FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(stack.getAccountId(), stack.getStack().getBoardId()); - updateStack(account, board, stack, liveData); + stack.getStack().setTitle(newTitle); + updateStack(account, fullBoard, stack, liveData); }); return liveData; - } @AnyThread private void updateStack(@NonNull Account account, @NonNull FullBoard board, @NonNull FullStack stack, @Nullable WrappedLiveData<FullStack> liveData) { - doAsync(() -> { - new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new StackDataProvider(null, board), stack, new IResponseCallback<FullStack>(account) { - @Override - public void onResponse(FullStack response) { - if (liveData != null) { - liveData.postValue(response); - } + doAsync(() -> new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new StackDataProvider(null, board), stack, new IResponseCallback<FullStack>(account) { + @Override + public void onResponse(FullStack response) { + if (liveData != null) { + liveData.postValue(response); } + } - @Override - public void onError(Throwable throwable) { - if (liveData != null) { - liveData.postError(throwable); - } + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + if (liveData != null) { + liveData.postError(throwable); } - }); - }); + } + })); } /** @@ -813,8 +999,8 @@ public class SyncManager { }); } - public LiveData<FullCard> getCardByLocalId(long accountId, long cardLocalId) { - return dataBaseAdapter.getCardByLocalId(accountId, cardLocalId); + public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { + return dataBaseAdapter.getCardWithProjectsByLocalId(accountId, cardLocalId); } public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { @@ -863,8 +1049,8 @@ public class SyncManager { // } @AnyThread - public LiveData<FullCard> createFullCard(long accountId, long localBoardId, long localStackId, @NonNull FullCard card) { - MutableLiveData<FullCard> liveData = new MutableLiveData<>(); + public WrappedLiveData<FullCard> createFullCard(long accountId, long localBoardId, long localStackId, @NonNull FullCard card) { + WrappedLiveData<FullCard> liveData = new WrappedLiveData<>(); doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); User owner = dataBaseAdapter.getUserByUidDirectly(accountId, account.getUserName()); @@ -875,7 +1061,7 @@ public class SyncManager { card.getCard().setAccountId(accountId); card.getCard().setStatusEnum(DBStatus.LOCAL_EDITED); card.getCard().setOrder(dataBaseAdapter.getHighestCardOrderInStack(localStackId) + 1); - long localCardId = dataBaseAdapter.createCard(accountId, card.getCard()); + long localCardId = dataBaseAdapter.createCardDirectly(accountId, card.getCard()); card.getCard().setLocalId(localCardId); List<User> assignedUsers = card.getAssignedUsers(); @@ -901,11 +1087,28 @@ public class SyncManager { } } - liveData.postValue(card); + if (serverAdapter.hasInternetConnection()) { new SyncHelper(serverAdapter, dataBaseAdapter, null) - .setResponseCallback(IResponseCallback.getDefaultResponseCallback(account)) + .setResponseCallback(new IResponseCallback<Boolean>(account) { + @Override + public void onResponse(Boolean response) { + liveData.postValue(card); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + if (throwable.getClass() == DeckException.class && ((DeckException)throwable).getHint().equals(DeckException.Hint.DEPENDENCY_NOT_SYNCED_YET)) { + liveData.postValue(card); + } else { + liveData.postError(throwable); + } + } + }) .doUpSyncFor(new CardDataProvider(null, board, stack)); + } else { + liveData.postValue(card); } }); return liveData; @@ -917,7 +1120,7 @@ public class SyncManager { doAsync(() -> { FullCard fullCard = dataBaseAdapter.getFullCardByLocalIdDirectly(card.getAccountId(), card.getLocalId()); if (fullCard == null) { - throw new IllegalArgumentException("card to delete does not exist."); + throw new IllegalArgumentException("card with id " + card.getLocalId() + " to delete does not exist."); } Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); @@ -935,12 +1138,12 @@ public class SyncManager { FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); card.getCard().setArchived(true); - updateCardForArchive(account, stack, board, card, getCallbackToLiveDataConverter(account, liveData)); + updateCardForArchive(stack, board, card, getCallbackToLiveDataConverter(account, liveData)); }); return liveData; } - private void updateCardForArchive(Account account, FullStack stack, Board board, FullCard card, @NonNull IResponseCallback<FullCard> callback) { + private void updateCardForArchive(FullStack stack, Board board, FullCard card, @NonNull IResponseCallback<FullCard> callback) { new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new CardDataProvider(null, board, stack), card, callback); } @@ -952,29 +1155,34 @@ public class SyncManager { FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); card.getCard().setArchived(false); - updateCardForArchive(account, stack, board, card, getCallbackToLiveDataConverter(account, liveData)); + updateCardForArchive(stack, board, card, getCallbackToLiveDataConverter(account, liveData)); }); return liveData; } @AnyThread - public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId) { + public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation) { WrappedLiveData<Void> liveData = new WrappedLiveData<>(); doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackLocalId); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - List<FullCard> cards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stackLocalId); + List<FullCard> cards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stackLocalId, filterInformation); if (cards.size() > 0) { CountDownLatch latch = new CountDownLatch(cards.size()); for (FullCard card : cards) { + if (card.getCard().isArchived()) { + latch.countDown(); + continue; + } card.getCard().setArchived(true); - updateCardForArchive(account, stack, board, card, new IResponseCallback<FullCard>(account) { + updateCardForArchive(stack, board, card, new IResponseCallback<FullCard>(account) { @Override public void onResponse(FullCard response) { latch.countDown(); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { latch.countDown(); @@ -996,21 +1204,35 @@ public class SyncManager { } @AnyThread - public void archiveBoard(@NonNull Board board) { + public WrappedLiveData<FullBoard> archiveBoard(@NonNull Board board) { + WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); doAsync(() -> { - FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); - b.getBoard().setArchived(true); - updateBoard(b); + try { + FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); + b.getBoard().setArchived(true); + updateBoard(b); + liveData.postValue(b); + } catch (Throwable e) { + liveData.postError(e); + } }); + return liveData; } @AnyThread - public void dearchiveBoard(@NonNull Board board) { + public WrappedLiveData<FullBoard> dearchiveBoard(@NonNull Board board) { + WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); doAsync(() -> { - FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); - b.getBoard().setArchived(false); - updateBoard(b); + try { + FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); + b.getBoard().setArchived(false); + updateBoard(b); + liveData.postValue(b); + } catch (Throwable e) { + liveData.postError(e); + } }); + return liveData; } @AnyThread @@ -1055,6 +1277,7 @@ public class SyncManager { liveData.postValue(dataBaseAdapter.getFullCardByLocalIdDirectly(card.getAccountId(), card.getLocalId())); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -1092,39 +1315,44 @@ public class SyncManager { public WrappedLiveData<Void> moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) { return LiveDataHelper.wrapInLiveData(() -> { - FullCard originalCard = dataBaseAdapter.getFullCardByLocalIdDirectly(originAccountId, originCardLocalId); + final FullCard originalCard = dataBaseAdapter.getFullCardByLocalIdDirectly(originAccountId, originCardLocalId); int newIndex = dataBaseAdapter.getHighestCardOrderInStack(targetStackLocalId) + 1; - FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalCardIdDirectly(originCardLocalId); + final FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalCardIdDirectly(originCardLocalId); // ### maybe shortcut possible? (just moved to another stack) if (targetBoardLocalId == originalBoard.getLocalId()) { reorder(originAccountId, originalCard, targetStackLocalId, newIndex); return null; } // ### get rid of original card where it is now. - Card originalInnerCard = originalCard.getCard(); - deleteCard(originalInnerCard); + final Card originalInnerCard = originalCard.getCard(); + deleteCard(new Card(originalInnerCard)); // ### clone card itself - Card targetCard = originalInnerCard; - targetCard.setAccountId(targetAccountId); - targetCard.setId(null); - targetCard.setLocalId(null); - targetCard.setStatusEnum(DBStatus.LOCAL_EDITED); - targetCard.setStackId(targetStackLocalId); - targetCard.setOrder(newIndex); - //TODO: this needs to propagate to server as well, since anything else propagates as well (otherwise card isn't known on server) - FullCard fullCardForServerPropagation = new FullCard(); - fullCardForServerPropagation.setCard(targetCard); - - Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId); - FullBoard targetBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, targetBoardLocalId); - FullStack targetFullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(targetStackLocalId); - User userOfTargetAccount = dataBaseAdapter.getUserByUidDirectly(targetAccountId, targetAccount.getUserName()); - CountDownLatch latch = new CountDownLatch(1); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new IResponseCallback<FullCard>(targetAccount) { + originalInnerCard.setAccountId(targetAccountId); + originalInnerCard.setId(null); + originalInnerCard.setLocalId(null); + originalInnerCard.setStatusEnum(DBStatus.LOCAL_EDITED); + originalInnerCard.setStackId(targetStackLocalId); + originalInnerCard.setOrder(newIndex); + originalInnerCard.setArchived(false); + originalInnerCard.setAttachmentCount(0); + originalInnerCard.setCommentsUnread(0); + final FullCard fullCardForServerPropagation = new FullCard(); + fullCardForServerPropagation.setCard(originalInnerCard); + + final Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId); + final FullBoard targetBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, targetBoardLocalId); + final FullStack targetFullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(targetStackLocalId); + final User userOfTargetAccount = dataBaseAdapter.getUserByUidDirectly(targetAccountId, targetAccount.getUserName()); + final CountDownLatch latch = new CountDownLatch(1); + ServerAdapter serverToUse = serverAdapter; + if (originAccountId != targetAccountId) { + serverToUse = new ServerAdapter(appContext, targetAccount.getName()); + } + new DataPropagationHelper(serverToUse, dataBaseAdapter).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new IResponseCallback<FullCard>(targetAccount) { @Override public void onResponse(FullCard response) { - targetCard.setId(response.getId()); - targetCard.setLocalId(response.getLocalId()); + originalInnerCard.setId(response.getId()); + originalInnerCard.setLocalId(response.getLocalId()); latch.countDown(); } @@ -1134,8 +1362,10 @@ public class SyncManager { throw new RuntimeException("unable to create card in moveCard target", throwable); } }, (FullCard entity, FullCard response) -> { - response.getCard().setUserId(entity.getCard().getUserId()); + response.getCard().setUserId(userOfTargetAccount.getLocalId()); response.getCard().setStackId(targetFullStack.getLocalId()); + entity.getCard().setUserId(userOfTargetAccount.getLocalId()); + entity.getCard().setStackId(targetFullStack.getLocalId()); }); try { @@ -1145,7 +1375,7 @@ public class SyncManager { throw new RuntimeException("error fulfilling countDownLatch", e); } - long newCardId = targetCard.getLocalId(); + long newCardId = originalInnerCard.getLocalId(); // ### clone labels, assign them // prepare @@ -1154,7 +1384,7 @@ public class SyncManager { List<AccessControl> aclOfTargetBoard = dataBaseAdapter.getAccessControlByLocalBoardIdDirectly(targetAccountId, targetBoard.getLocalId()); if (!hasManagePermission) { for (AccessControl accessControl : aclOfTargetBoard) { - if (accessControl.getUserId() == userOfTargetAccount.getLocalId() && accessControl.isPermissionManage()) { + if (accessControl.getUserId().equals(userOfTargetAccount.getLocalId()) && accessControl.isPermissionManage()) { hasManagePermission = true; break; } @@ -1178,10 +1408,10 @@ public class SyncManager { originalLabel.setLocalId(null); originalLabel.setStatusEnum(DBStatus.LOCAL_EDITED); originalLabel.setAccountId(targetBoard.getAccountId()); - createAndAssignLabelToCard(originalBoard.getAccountId(), originalLabel, newCardId); + createAndAssignLabelToCard(targetBoard.getAccountId(), originalLabel, newCardId, serverToUse); } } else { - assignLabelToCard(existingMatch, targetCard); + assignLabelToCard(existingMatch, originalInnerCard, serverToUse); } } @@ -1194,7 +1424,7 @@ public class SyncManager { boolean hasViewPermission = targetBoard.getBoard().getOwnerId() == assignedUser.getLocalId(); if (!hasViewPermission) { for (AccessControl accessControl : aclOfTargetBoard) { - if (accessControl.getUserId() == userOfTargetAccount.getLocalId()) { + if (accessControl.getUserId().equals(userOfTargetAccount.getLocalId())) { // ACL exists, so viewing is granted hasViewPermission = true; break; @@ -1202,7 +1432,7 @@ public class SyncManager { } } if (hasViewPermission) { - assignUserToCard(assignedUser, targetCard); + assignUserToCard(assignedUser, originalInnerCard); } } } @@ -1229,25 +1459,28 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); } - }, (entity, response) -> { - response.setBoardId(board.getLocalId()); - }); + }, (entity, response) -> response.setBoardId(board.getLocalId())); }); return liveData; } - @AnyThread public MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId) { + return createAndAssignLabelToCard(accountId, label, localCardId, serverAdapter); + } + + @AnyThread + private MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId, ServerAdapter serverAdapterToUse) { MutableLiveData<Label> liveData = new MutableLiveData<>(); doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); Board board = dataBaseAdapter.getBoardByLocalCardIdDirectly(localCardId); label.setAccountId(accountId); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new LabelDataProvider(null, board, null), label, new IResponseCallback<Label>(account) { + new DataPropagationHelper(serverAdapterToUse, dataBaseAdapter).createEntity(new LabelDataProvider(null, board, null), label, new IResponseCallback<Label>(account) { @Override public void onResponse(Label response) { assignLabelToCard(response, dataBaseAdapter.getCardByLocalIdDirectly(accountId, localCardId)); @@ -1317,6 +1550,11 @@ public class SyncManager { @AnyThread public void assignLabelToCard(@NonNull Label label, @NonNull Card card) { + assignLabelToCard(label, card, serverAdapter); + } + + @AnyThread + public void assignLabelToCard(@NonNull Label label, @NonNull Card card, ServerAdapter serverAdapterToUse) { doAsync(() -> { final long localLabelId = label.getLocalId(); final long localCardId = card.getLocalId(); @@ -1327,8 +1565,8 @@ public class SyncManager { Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); - if (serverAdapter.hasInternetConnection()) { - serverAdapter.assignLabelToCard(board.getId(), stack.getId(), card.getId(), label.getId(), new IResponseCallback<Void>(account) { + if (serverAdapterToUse.hasInternetConnection()) { + serverAdapterToUse.assignLabelToCard(board.getId(), stack.getId(), card.getId(), label.getId(), new IResponseCallback<Void>(account) { @Override public void onResponse(Void response) { @@ -1395,11 +1633,6 @@ public class SyncManager { return findProposalsForLabelsToAssign(accountId, boardId, -1L); } - // TODO Difference to getFullBoardByid() ??? I think those methods are equal, we should drop one of them. - public LiveData<FullBoard> getFullBoard(Long accountId, Long localId) { - return dataBaseAdapter.getFullBoardById(accountId, localId); - } - public LiveData<User> getUserByLocalId(long accountId, long localId) { return dataBaseAdapter.getUserByLocalId(accountId, localId); } @@ -1417,12 +1650,12 @@ public class SyncManager { return dataBaseAdapter.searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, searchTerm); } - public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedInACL, final String searchTerm) { - return dataBaseAdapter.searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedInACL, searchTerm); + public UserSearchLiveData searchUserByUidOrDisplayNameForACL() { + return new UserSearchLiveData(dataBaseAdapter, serverAdapter); } - public LiveData<Board> getBoard(long accountId, long remoteId) { - return dataBaseAdapter.getBoard(accountId, remoteId); + public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { + return dataBaseAdapter.getBoardByRemoteId(accountId, remoteId); } public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) { @@ -1445,10 +1678,6 @@ public class SyncManager { return dataBaseAdapter.searchNotYetAssignedLabelsByTitle(accountId, boardId, notYetAssignedToLocalCardId, searchTerm); } - public String getServerUrl() { - return serverAdapter.getServerUrl(); - } - /** * @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/360">reenable reorder</a> */ @@ -1456,7 +1685,7 @@ public class SyncManager { public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) { doAsync(() -> { // read cards of new stack - List<FullCard> cardsOfNewStack = dataBaseAdapter.getFullCardsForStackDirectly(accountId, newStackId); + List<FullCard> cardsOfNewStack = dataBaseAdapter.getFullCardsForStackDirectly(accountId, newStackId, null); int newOrder = newIndex; if (cardsOfNewStack.size() > newIndex) { newOrder = cardsOfNewStack.get(newIndex).getCard().getOrder(); @@ -1526,7 +1755,7 @@ public class SyncManager { Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(movedCard.getCard().getStackId()); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(movedCard.getCard().getAccountId()); - new SyncHelper(serverAdapter, dataBaseAdapter, new Date()).setResponseCallback(new IResponseCallback<Boolean>(account) { + new SyncHelper(serverAdapter, dataBaseAdapter, Instant.now()).setResponseCallback(new IResponseCallback<Boolean>(account) { @Override public void onResponse(Boolean response) { // doNothing(); @@ -1597,7 +1826,7 @@ public class SyncManager { } private void reorderAscending(@NonNull Card movedCard, @NonNull List<Card> cardsToReorganize, int startingAtOrder) { - Date now = new Date(); + final Instant now = Instant.now(); for (Card card : cardsToReorganize) { card.setOrder(startingAtOrder); if (card.getStatus() == DBStatus.UP_TO_DATE.getId()) { @@ -1625,7 +1854,7 @@ public class SyncManager { WrappedLiveData<Attachment> liveData = new WrappedLiveData<>(); doAsync(() -> { Attachment attachment = populateAttachmentEntityForFile(new Attachment(), localCardId, mimeType, file); - Date now = new Date(); + final Instant now = Instant.now(); attachment.setLastModifiedLocal(now); attachment.setCreatedAt(now); FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, localCardId); @@ -1645,7 +1874,7 @@ public class SyncManager { WrappedLiveData<Attachment> liveData = new WrappedLiveData<>(); doAsync(() -> { Attachment attachment = populateAttachmentEntityForFile(existing, existing.getCardId(), mimeType, file); - attachment.setLastModifiedLocal(new Date()); + attachment.setLastModifiedLocal(Instant.now()); if (serverAdapter.hasInternetConnection()) { FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, existing.getCardId()); Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getCard().getStackId()); @@ -1658,6 +1887,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -1670,15 +1900,14 @@ public class SyncManager { @AnyThread private static Attachment populateAttachmentEntityForFile(@NonNull Attachment target, long localCardId, @NonNull String mimeType, @NonNull File file) { - Attachment attachment = target; - attachment.setCardId(localCardId); - attachment.setMimetype(mimeType); - attachment.setData(file.getName()); - attachment.setFilename(file.getName()); - attachment.setBasename(file.getName()); - attachment.setLocalPath(file.getAbsolutePath()); - attachment.setFilesize(file.length()); - return attachment; + target.setCardId(localCardId); + target.setMimetype(mimeType); + target.setData(file.getName()); + target.setFilename(file.getName()); + target.setBasename(file.getName()); + target.setLocalPath(file.getAbsolutePath()); + target.setFilesize(file.length()); + return target; } @AnyThread @@ -1726,7 +1955,27 @@ public class SyncManager { doAsync(() -> dataBaseAdapter.deleteSingleCardWidget(widgetId)); } - private static class BooleanResultHolder { - public boolean result = true; + public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { + doAsync(() -> dataBaseAdapter.createStackWidget(appWidgetId, accountId, stackId, darkTheme)); + } + + @WorkerThread + public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { + final StackWidgetModel model = dataBaseAdapter.getStackWidgetModelDirectly(appWidgetId); + if (model == null) { + throw new NoSuchElementException(); + } + return model; + } + + public void deleteStackWidgetModel(int appWidgetId) { + doAsync(() -> dataBaseAdapter.deleteStackWidget(appWidgetId)); + } + + /** + * FIXME https://github.com/stefan-niedermann/nextcloud-deck/issues/640 + */ + public static boolean ignoreExceptionOnVoidError(Throwable t) { + return t instanceof NullPointerException && "Attempt to invoke interface method 'void io.reactivex.disposables.Disposable.dispose()' on a null object reference".equals(t.getMessage()); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java index 4f41173e9..bff8ddfdb 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java @@ -13,18 +13,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; +import com.nextcloud.android.sso.api.ParsedResponse; + import java.io.File; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.List; -import java.util.Locale; -import java.util.TimeZone; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.ApiProvider; import it.niedermann.nextcloud.deck.api.IResponseCallback; -import it.niedermann.nextcloud.deck.api.LastSyncUtil; import it.niedermann.nextcloud.deck.api.RequestHelper; import it.niedermann.nextcloud.deck.exceptions.OfflineException; import it.niedermann.nextcloud.deck.model.AccessControl; @@ -39,11 +35,12 @@ import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectList; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; import it.niedermann.nextcloud.deck.model.propagation.CardUpdate; import it.niedermann.nextcloud.deck.model.propagation.Reorder; -import it.niedermann.nextcloud.deck.util.DateUtil; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; @@ -53,18 +50,11 @@ import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN; public class ServerAdapter { - private String prefKeyWifiOnly; - - private static final DateFormat API_FORMAT = - new SimpleDateFormat("E, dd MMM yyyy hh:mm:ss z", Locale.US); - - static { - API_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); - } + private final String prefKeyWifiOnly; @NonNull - private Context applicationContext; - private ApiProvider provider; + private final Context applicationContext; + private final ApiProvider provider; public ServerAdapter(@NonNull Context applicationContext) { this(applicationContext, null); @@ -135,32 +125,35 @@ public class ServerAdapter { // return lastSyncHeader; } - // TODO not used - private Date getLastSync(long accountId) { - Date lastSync = DateUtil.nowInGMT(); - lastSync.setTime(LastSyncUtil.getLastSync(accountId)); + public void getBoards(IResponseCallback<ParsedResponse<List<FullBoard>>> responseCallback) { + RequestHelper.request(provider, () -> + provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId()), responseCallback.getAccount().getBoardsEtag()), + responseCallback); + } - return lastSync; + public void getCapabilities(String eTag, IResponseCallback<ParsedResponse<Capabilities>> responseCallback) { + ensureInternetConnection(); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().getCapabilities(eTag), responseCallback); } - public void getBoards(IResponseCallback<List<FullBoard>> responseCallback) { - RequestHelper.request(provider, () -> - provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId())), - responseCallback); + public void getProjectsForCard(long remoteCardId, IResponseCallback<OcsProjectList> responseCallback) { + ensureInternetConnection(); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().getProjectsForCard(remoteCardId), responseCallback); } - public void getCapabilities(IResponseCallback<Capabilities> responseCallback) { + public void searchUser(String searchTerm, IResponseCallback<OcsUserList> responseCallback) { ensureInternetConnection(); - RequestHelper.request(provider, () -> provider.getNextcloudAPI().getCapabilities(), responseCallback); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().searchUser(searchTerm), responseCallback); } - public void getAllOcsUsers(IResponseCallback<OcsUserList> responseCallback) { + + public void getSingleUserData(String userUid, IResponseCallback<OcsUser> responseCallback) { ensureInternetConnection(); - RequestHelper.request(provider, () -> provider.getNextcloudAPI().getAllUsers(), responseCallback); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().getSingleUserData(userUid), responseCallback); } - public void getOcsUserDetails(String ocsUserName, IResponseCallback<OcsUser> responseCallback) { + public void searchGroupMembers(String groupUID, IResponseCallback<GroupMemberUIDs> responseCallback) { ensureInternetConnection(); - RequestHelper.request(provider, () -> provider.getNextcloudAPI().getUserDetails(ocsUserName), responseCallback); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().searchGroupMembers(groupUID), responseCallback); } public void getActivitiesForCard(long cardId, IResponseCallback<List<it.niedermann.nextcloud.deck.model.ocs.Activity>> responseCallback) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java index c643289bd..982844f06 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java @@ -2,16 +2,15 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db; import android.content.Context; +import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.lifecycle.LiveData; import androidx.sqlite.db.SimpleSQLiteQuery; -import org.jetbrains.annotations.NotNull; - +import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; @@ -26,10 +25,12 @@ import it.niedermann.nextcloud.deck.model.JoinCardWithUser; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.enums.EDueType; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @@ -39,10 +40,16 @@ import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; +import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.model.relations.UserInBoard; +import it.niedermann.nextcloud.deck.model.relations.UserInGroup; import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.widget.singlecard.SingleCardWidget; +import it.niedermann.nextcloud.deck.ui.widget.stack.StackWidget; import static androidx.lifecycle.Transformations.distinctUntilChanged; @@ -57,28 +64,28 @@ public class DataBaseAdapter { this.db = DeckDatabase.getInstance(applicationContext); } - @NotNull + @NonNull public Context getContext() { return context; } private <T extends AbstractRemoteEntity> void markAsEditedIfNeeded(T entity, boolean setStatus) { if (!setStatus) return; - entity.setLastModifiedLocal(new Date()); // now. + entity.setLastModifiedLocal(Instant.now()); entity.setStatusEnum(DBStatus.LOCAL_EDITED); } private <T extends AbstractRemoteEntity> void markAsDeletedIfNeeded(T entity, boolean setStatus) { if (!setStatus) return; entity.setStatusEnum(DBStatus.LOCAL_DELETED); - entity.setLastModifiedLocal(new Date()); // now. + entity.setLastModifiedLocal(Instant.now()); } public LiveData<Boolean> hasAccounts() { return LiveDataHelper.postCustomValue(db.getAccountDao().countAccounts(), data -> data != null && data > 0); } - public LiveData<Board> getBoard(long accountId, long remoteId) { + public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { return distinctUntilChanged(db.getBoardDao().getBoardByRemoteId(accountId, remoteId)); } @@ -180,24 +187,66 @@ public class DataBaseAdapter { if (filter == null) { return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardsForStack(accountId, localStackId), this::filterRelationsForCard); } + return LiveDataHelper.interceptLiveData(db.getCardDao().getFilteredFullCardsForStack(getQueryForFilter(filter, accountId, localStackId)), this::filterRelationsForCard); + + } + + private void fillSqlWithListValues(StringBuilder query, List<Object> args, @NonNull List<? extends IRemoteEntity> entities) { + for (int i = 0; i < entities.size(); i++) { + if (i > 0) { + query.append(", "); + } + query.append("?"); + args.add(entities.get(i).getLocalId()); + } + } + + @WorkerThread + public List<FullCard> getFullCardsForStackDirectly(long accountId, long localStackId, FilterInformation filter) { + if (filter == null) { + return db.getCardDao().getFullCardsForStackDirectly(accountId, localStackId); + } + List<Object> args = new ArrayList<>(); + args.add(accountId); + args.add(localStackId); + + return db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, accountId, localStackId)); + } + @AnyThread + private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, long accountId, long localStackId) { List<Object> args = new ArrayList<>(); - StringBuilder query = new StringBuilder("SELECT * FROM card c " + - "WHERE accountId = ? AND stackId = ? "); args.add(accountId); args.add(localStackId); + StringBuilder query = new StringBuilder("SELECT * FROM card c " + + "WHERE accountId = ? AND stackId = ? "); if (!filter.getLabels().isEmpty()) { - query.append("and exists(select 1 from joincardwithlabel j where c.localId = cardId and labelId in ("); + query.append("and (exists(select 1 from joincardwithlabel j where c.localId = cardId and labelId in ("); fillSqlWithListValues(query, args, filter.getLabels()); query.append(") and j.status<>3) "); + if (filter.isNoAssignedLabel()) { + query.append("or not exists(select 1 from joincardwithlabel j where c.localId = cardId and j.status<>3)) "); + } else { + query.append(") "); + } + } else if (filter.isNoAssignedLabel()) { + query.append("and not exists(select 1 from joincardwithlabel j where c.localId = cardId and j.status<>3) "); } if (!filter.getUsers().isEmpty()) { - query.append("and exists(select 1 from joincardwithuser j where c.localId = cardId and userId in ("); + query.append("and (exists(select 1 from joincardwithuser j where c.localId = cardId and userId in ("); fillSqlWithListValues(query, args, filter.getUsers()); query.append(") and j.status<>3) "); + if (filter.isNoAssignedUser()) { + query.append("or not exists(select 1 from joincardwithuser j where c.localId = cardId and j.status<>3)) "); + } else { + query.append(") "); + } + } else if (filter.isNoAssignedUser()) { + query.append("and not exists(select 1 from joincardwithuser j where c.localId = cardId and j.status<>3) "); } + if (filter.getDueType() != EDueType.NO_FILTER) { switch (filter.getDueType()) { case NO_DUE: @@ -219,24 +268,11 @@ public class DataBaseAdapter { throw new IllegalArgumentException("Xou need to add your new EDueType value\"" + filter.getDueType() + "\" here!"); } } - query.append(" and status<>3 order by `order`, createdAt asc;"); - return LiveDataHelper.interceptLiveData(db.getCardDao().getFilteredFullCardsForStack(new SimpleSQLiteQuery(query.toString(), args.toArray())), this::filterRelationsForCard); - - } - - private void fillSqlWithListValues(StringBuilder query, List<Object> args, @NonNull List<? extends IRemoteEntity> entities) { - for (int i = 0; i < entities.size(); i++) { - if (i > 0) { - query.append(", "); - } - query.append("?"); - args.add(entities.get(i).getLocalId()); + if (filter.getArchiveStatus() != FilterInformation.EArchiveStatus.ALL) { + query.append(" and c.archived = " + (filter.getArchiveStatus() == FilterInformation.EArchiveStatus.ARCHIVED ? 1 : 0)); } - } - - @WorkerThread - public List<FullCard> getFullCardsForStackDirectly(long accountId, long localStackId) { - return db.getCardDao().getFullCardsForStackDirectly(accountId, localStackId); + query.append(" and status<>3 order by `order`, createdAt asc;"); + return new SimpleSQLiteQuery(query.toString(), args.toArray()); } @WorkerThread @@ -244,17 +280,20 @@ public class DataBaseAdapter { return db.getUserDao().getUserByUidDirectly(accountId, uid); } + @WorkerThread public long createUser(long accountId, User user) { user.setAccountId(accountId); return db.getUserDao().insert(user); } + @WorkerThread public void updateUser(long accountId, User user, boolean setStatus) { markAsEditedIfNeeded(user, setStatus); user.setAccountId(accountId); db.getUserDao().update(user); } + @AnyThread public LiveData<Label> getLabelByRemoteId(long accountId, long remoteId) { return distinctUntilChanged(db.getLabelDao().getLabelByRemoteId(accountId, remoteId)); } @@ -264,7 +303,8 @@ public class DataBaseAdapter { return db.getLabelDao().getLabelByRemoteIdDirectly(accountId, remoteId); } - public long createLabel(long accountId, @NonNull Label label) { + @WorkerThread + public long createLabelDirectly(long accountId, @NonNull Label label) { label.setAccountId(accountId); return db.getLabelDao().insert(label); } @@ -320,6 +360,8 @@ public class DataBaseAdapter { // readded! existing.setStatusEnum(DBStatus.LOCAL_EDITED); db.getJoinCardWithUserDao().update(existing); + } else if (existing != null) { + return; } else { JoinCardWithUser join = new JoinCardWithUser(); join.setCardId(localCardId); @@ -344,6 +386,28 @@ public class DataBaseAdapter { db.getJoinBoardWithLabelDao().deleteByBoardId(localBoardId); } + public void deleteGroupMembershipsOfGroup(Long localGroupUserId) { + db.getUserInGroupDao().deleteByGroupId(localGroupUserId); + } + + public void deleteBoardMembershipsOfBoard(Long localBoardId) { + db.getUserInBoardDao().deleteByBoardId(localBoardId); + } + + public void addUserToGroup(Long localGroupUserId, Long localGroupMemberId) { + UserInGroup relation = new UserInGroup(); + relation.setGroupId(localGroupUserId); + relation.setMemberId(localGroupMemberId); + db.getUserInGroupDao().insert(relation); + } + + public void addUserToBoard(Long localUserId, Long localBoardId) { + UserInBoard relation = new UserInBoard(); + relation.setBoardId(localBoardId); + relation.setUserId(localUserId); + db.getUserInBoardDao().insert(relation); + } + public void updateLabel(Label label, boolean setStatus) { markAsEditedIfNeeded(label, setStatus); db.getLabelDao().update(label); @@ -391,6 +455,10 @@ public class DataBaseAdapter { return distinctUntilChanged(db.getAccountDao().getAllAccounts()); } + public LiveData<List<Board>> getBoards(long accountId) { + return distinctUntilChanged(db.getBoardDao().getBoardsForAccount(accountId)); + } + public LiveData<List<Board>> getBoards(long accountId, boolean archived) { return distinctUntilChanged( archived @@ -431,8 +499,8 @@ public class DataBaseAdapter { db.getBoardDao().update(board); } - public LiveData<List<FullStack>> getFullStacksForBoard(long accountId, long localBoardId) { - return distinctUntilChanged(db.getStackDao().getFullStacksForBoard(accountId, localBoardId)); + public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { + return distinctUntilChanged(db.getStackDao().getStacksForBoard(accountId, localBoardId)); } @WorkerThread @@ -440,27 +508,36 @@ public class DataBaseAdapter { return db.getStackDao().getFullStacksForBoardDirectly(accountId, localBoardId); } + @AnyThread public LiveData<FullStack> getStack(long accountId, long localStackId) { return distinctUntilChanged(db.getStackDao().getFullStack(accountId, localStackId)); } + @WorkerThread public long createStack(long accountId, Stack stack) { stack.setAccountId(accountId); return db.getStackDao().insert(stack); } + @WorkerThread public void deleteStack(Stack stack, boolean setStatus) { markAsDeletedIfNeeded(stack, setStatus); db.getStackDao().update(stack); } + @WorkerThread public void deleteStackPhysically(Stack stack) { db.getStackDao().delete(stack); } + @WorkerThread public void updateStack(Stack stack, boolean setStatus) { markAsEditedIfNeeded(stack, setStatus); db.getStackDao().update(stack); + if (db.getStackWidgetModelDao().containsStackLocalId(stack.getLocalId())) { + DeckLog.info("Notifying " + StackWidget.class.getSimpleName() + " about card changes for \"" + stack.getTitle() + "\""); + StackWidget.notifyDatasetChanged(context); + } } @WorkerThread @@ -468,10 +545,16 @@ public class DataBaseAdapter { return db.getCardDao().getCardByLocalIdDirectly(accountId, localCardId); } + @AnyThread public LiveData<FullCard> getCardByLocalId(long accountId, long localCardId) { return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardByLocalId(accountId, localCardId), this::filterRelationsForCard); } + @AnyThread + public LiveData<FullCardWithProjects> getCardWithProjectsByLocalId(long accountId, long localCardId) { + return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardWithProjectsByLocalId(accountId, localCardId), this::filterRelationsForCard); + } + @WorkerThread public List<FullCard> getLocallyChangedCardsDirectly(long accountId) { return db.getCardDao().getLocallyChangedCardsDirectly(accountId); @@ -482,15 +565,27 @@ public class DataBaseAdapter { return db.getCardDao().getLocallyChangedCardsByLocalStackIdDirectly(accountId, localStackId); } - public long createCard(long accountId, Card card) { + @WorkerThread + public long createCardDirectly(long accountId, Card card) { card.setAccountId(accountId); - return db.getCardDao().insert(card); + long newCardId = db.getCardDao().insert(card); + + notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId()); + + return newCardId; } - public int getHighestCardOrderInStack(long localStackId){ + @WorkerThread + public int getHighestCardOrderInStack(long localStackId) { return db.getCardDao().getHighestOrderInStack(localStackId); } + @WorkerThread + public int getHighestStackOrderInBoard(long localBoardId) { + return db.getStackDao().getHighestStackOrderInBoard(localBoardId); + } + + @WorkerThread public void deleteCard(Card card, boolean setStatus) { markAsDeletedIfNeeded(card, setStatus); if (setStatus) { @@ -498,21 +593,35 @@ public class DataBaseAdapter { } else { deleteCardPhysically(card); } + + notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId()); } + @WorkerThread public void deleteCardPhysically(Card card) { db.getCardDao().delete(card); } + @WorkerThread public void updateCard(@NonNull Card card, boolean setStatus) { markAsEditedIfNeeded(card, setStatus); + Long originalStackLocalId = db.getCardDao().getLocalStackIdByLocalCardId(card.getLocalId()); db.getCardDao().update(card); if (db.getSingleCardWidgetModelDao().containsCardLocalId(card.getLocalId())) { - DeckLog.info("Notifying widget about card changes for \"" + card.getTitle() + "\""); + DeckLog.info("Notifying " + SingleCardWidget.class.getSimpleName() + " about card changes for \"" + card.getTitle() + "\""); SingleCardWidget.notifyDatasetChanged(context); } + notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId(), originalStackLocalId); + } + + private void notifyStackWidgetsIfNeeded(String cardTitle, long... affectedStackIds) { + if (db.getStackWidgetModelDao().containsStackLocalId(affectedStackIds)) { + DeckLog.info("Notifying " + StackWidget.class.getSimpleName() + " about card changes for \"" + cardTitle + "\""); + StackWidget.notifyDatasetChanged(context); + } } + @WorkerThread public long createAccessControl(long accountId, @NonNull AccessControl entity) { entity.setAccountId(accountId); return db.getAccessControlDao().insert(entity); @@ -571,9 +680,9 @@ public class DataBaseAdapter { return db.getUserDao().searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%"); } - public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedToACL, final String searchTerm) { + public List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long notYetAssignedToACL, final String searchTerm) { validateSearchTerm(searchTerm); - return db.getUserDao().searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%"); + return db.getUserDao().searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%"); } public LiveData<List<Label>> searchNotYetAssignedLabelsByTitle(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, String searchTerm) { @@ -618,9 +727,14 @@ public class DataBaseAdapter { return db.getAttachmentDao().getLocallyChangedAttachmentsDirectly(accountId); } + @WorkerThread + public List<Attachment> getLocallyChangedAttachmentsForStackDirectly(long localStackId) { + return db.getAttachmentDao().getLocallyChangedAttachmentsForStackDirectly(localStackId); + } + public long createAttachment(long accountId, @NonNull Attachment attachment) { attachment.setAccountId(accountId); - attachment.setCreatedAt(new Date()); + attachment.setCreatedAt(Instant.now()); return db.getAttachmentDao().insert(attachment); } @@ -720,16 +834,24 @@ public class DataBaseAdapter { return db.getJoinCardWithLabelDao().getAllDeletedJoinsWithRemoteIDs(); } - public List<JoinCardWithLabel> getAllChangedJoins() { + public List<JoinCardWithLabel> getAllChangedLabelJoins() { return db.getJoinCardWithLabelDao().getAllChangedJoins(); } - public JoinCardWithLabel getRemoteIdsForJoin(Long localCardId, Long localLabelId) { + public List<JoinCardWithLabel> getAllChangedLabelJoinsForStack(Long localStackId) { + return db.getJoinCardWithLabelDao().getAllChangedJoinsForStack(localStackId); + } + + public JoinCardWithLabel getAllChangedLabelJoinsWithRemoteIDs(Long localCardId, Long localLabelId) { return db.getJoinCardWithLabelDao().getRemoteIdsForJoin(localCardId, localLabelId); } - public List<JoinCardWithUser> getAllDeletedUserJoinsWithRemoteIDs() { - return db.getJoinCardWithUserDao().getDeletedJoinsWithRemoteIDs(); + public List<JoinCardWithUser> getAllChangedUserJoinsWithRemoteIDs() { + return db.getJoinCardWithUserDao().getChangedJoinsWithRemoteIDs(); + } + + public List<JoinCardWithUser> getAllChangedUserJoinsWithRemoteIDsForStack(Long localStackId) { + return db.getJoinCardWithUserDao().getChangedJoinsWithRemoteIDsForStack(localStackId); } public void deleteJoinedLabelForCardPhysicallyByRemoteIDs(Long accountId, Long remoteCardId, Long remoteLabelId) { @@ -837,11 +959,17 @@ public class DataBaseAdapter { return db.getCommentDao().getCommentByLocalCardIdDirectly(localCardId); } + @WorkerThread public List<Card> getCardsWithLocallyChangedCommentsDirectly(Long accountId) { return db.getCardDao().getCardsWithLocallyChangedCommentsDirectly(accountId); } @WorkerThread + public List<Card> getCardsWithLocallyChangedCommentsForStackDirectly(Long localStackId) { + return db.getCardDao().getCardsWithLocallyChangedCommentsForStackDirectly(localStackId); + } + + @WorkerThread public Long getLocalStackIdByRemoteStackIdDirectly(long accountId, Long stackId) { return db.getStackDao().getLocalStackIdByRemoteStackIdDirectly(accountId, stackId); } @@ -910,11 +1038,89 @@ public class DataBaseAdapter { db.getSingleCardWidgetModelDao().delete(model); } + public long createStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { + StackWidgetModel model = new StackWidgetModel(); + model.setAppWidgetId(appWidgetId); + model.setAccountId(accountId); + model.setStackId(stackId); + model.setDarkTheme(darkTheme); + + return db.getStackWidgetModelDao().insert(model); + } + + public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) { + return db.getStackWidgetModelDao().getStackWidgetByAppWidgetIdDirectly(appWidgetId); + } + + public void deleteStackWidget(int appWidgetId) { + StackWidgetModel model = new StackWidgetModel(); + model.setAppWidgetId(appWidgetId); + db.getStackWidgetModelDao().delete(model); + } + public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) { - return db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%"+host+"%", boardRemoteId); + return db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%" + host + "%", boardRemoteId); } public List<Account> readAccountsForHostWithReadAccessToBoardDirectly(String host, long boardRemoteId) { - return db.getAccountDao().readAccountsForHostWithReadAccessToBoardDirectly("%"+host+"%", boardRemoteId); + return db.getAccountDao().readAccountsForHostWithReadAccessToBoardDirectly("%" + host + "%", boardRemoteId); + } + + public Board getBoardForAccountByNameDirectly(long account, String title) { + return db.getBoardDao().getBoardForAccountByNameDirectly(account, title); + } + + public OcsProject getProjectByRemoteIdDirectly(long accountId, Long remoteId) { + return db.getOcsProjectDao().getProjectByRemoteIdDirectly(accountId, remoteId); + } + + public Long createProjectDirectly(long accountId, OcsProject entity) { + entity.setAccountId(accountId); + return db.getOcsProjectDao().insert(entity); + } + + public void deleteProjectResourcesForProjectIdDirectly(Long localProjectId) { + db.getOcsProjectResourceDao().deleteByProjectId(localProjectId); + } + + public void updateProjectDirectly(long accountId, OcsProject entity) { + entity.setAccountId(accountId); + db.getOcsProjectDao().update(entity); + } + + public void deleteProjectDirectly(OcsProject ocsProject) { + db.getOcsProjectResourceDao().deleteByProjectId(ocsProject.getLocalId()); + db.getOcsProjectDao().delete(ocsProject); + } + + public Long createProjectResourceDirectly(Long accountId, OcsProjectResource resource) { + resource.setAccountId(accountId); + return db.getOcsProjectResourceDao().insert(resource); + } + + public int countProjectResourcesInProjectDirectly(Long projectLocalId) { + return db.getOcsProjectResourceDao().countProjectResourcesInProjectDirectly(projectLocalId); + } + + public LiveData<Integer> countProjectResourcesInProject(Long projectLocalId) { + return db.getOcsProjectResourceDao().countProjectResourcesInProject(projectLocalId); + } + + public LiveData<List<OcsProjectResource>> getResourcesByLocalProjectId(Long projectLocalId) { + return db.getOcsProjectResourceDao().getResourcesByLocalProjectId(projectLocalId); + } + + public void assignCardToProjectIfMissng(Long accountId, Long localProjectId, Long remoteCardId) { + Card card = db.getCardDao().getCardByRemoteIdDirectly(accountId, remoteCardId); + if (card != null) { + JoinCardWithProject existing = db.getJoinCardWithOcsProjectDao().getAssignmentByCardIdAndProjectIdDirectly(card.getLocalId(), localProjectId); + if (existing == null) { + JoinCardWithProject assignment = new JoinCardWithProject(); + assignment.setStatus(DBStatus.UP_TO_DATE.getId()); + assignment.setCardId(card.getLocalId()); + assignment.setProjectId(localProjectId); + db.getJoinCardWithOcsProjectDao().insert(assignment); + } + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java index 6b198a502..f6811e033 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java @@ -2,17 +2,17 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db; import androidx.room.TypeConverter; -import java.util.Date; +import java.time.Instant; public class DateTypeConverter { @TypeConverter - public static Date toDate(Long value) { - return value == null ? null : new Date(value); + public static Instant toInstant(Long value) { + return value == null ? null : Instant.ofEpochMilli(value); } @TypeConverter - public static Long toLong(Date value) { - return value == null ? null : value.getTime(); + public static Long fromInstant(Instant value) { + return value == null ? null : value.toEpochMilli(); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java index 0e86557fa..a491946ed 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java @@ -1,8 +1,11 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; +import android.graphics.Color; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import androidx.room.Database; @@ -12,6 +15,7 @@ import androidx.room.TypeConverters; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.LastSyncUtil; import it.niedermann.nextcloud.deck.model.AccessControl; @@ -28,10 +32,16 @@ import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.Permission; import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; +import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.model.relations.UserInBoard; +import it.niedermann.nextcloud.deck.model.relations.UserInGroup; import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel; import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.AccessControlDao; @@ -49,9 +59,15 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.JoinCardWit import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.LabelDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.MentionDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.PermissionDao; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.SingleCardWidgetModelDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.StackDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserInBoardDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserInGroupDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.JoinCardWithOcsProjectDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.OcsProjectDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.OcsProjectResourceDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.SingleCardWidgetModelDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.StackWidgetModelDao; @Database( entities = { @@ -73,9 +89,15 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserDao; DeckComment.class, Mention.class, SingleCardWidgetModel.class, + StackWidgetModel.class, + OcsProject.class, + OcsProjectResource.class, + JoinCardWithProject.class, + UserInGroup.class, + UserInBoard.class, }, exportSchema = false, - version = 15 + version = 23 ) @TypeConverters({DateTypeConverter.class}) public abstract class DeckDatabase extends RoomDatabase { @@ -175,8 +197,204 @@ public abstract class DeckDatabase extends RoomDatabase { } }; - public static final RoomDatabase.Callback ON_CREATE_CALLBACK = new RoomDatabase.Callback() { + private static final Migration MIGRATION_15_16 = new Migration(15, 16) { + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `StackWidgetModel` (`appWidgetId` INTEGER PRIMARY KEY, `accountId` INTEGER, `stackId` INTEGER, `darkTheme` INTEGER CHECK (`darkTheme` IN (0,1)) NOT NULL, " + + "FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, " + + "FOREIGN KEY(`stackId`) REFERENCES `Stack`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE INDEX `index_StackWidgetModel_stackId` ON `StackWidgetModel` (`stackId`)"); + database.execSQL("CREATE INDEX `index_StackWidgetModel_accountId` ON `StackWidgetModel` (`accountId`)"); + } + }; + + private static final Migration MIGRATION_16_17 = new Migration(16, 17) { + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `OcsProject` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `name` TEXT NOT NULL, `status` INTEGER NOT NULL, `lastModified` INTEGER, `lastModifiedLocal` INTEGER)"); + database.execSQL("CREATE UNIQUE INDEX `index_OcsProject_accountId_id` ON `OcsProject` (`accountId`, `id`)"); + database.execSQL("CREATE INDEX `index_project_accID` ON `OcsProject` (`accountId`)"); + database.execSQL("CREATE INDEX `index_OcsProject_id` ON `OcsProject` (`id`)"); + database.execSQL("CREATE INDEX `index_OcsProject_lastModifiedLocal` ON `OcsProject` (`lastModifiedLocal`)"); + + database.execSQL("CREATE TABLE `OcsProjectResource` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `name` TEXT, `status` INTEGER NOT NULL, `lastModified` INTEGER, `lastModifiedLocal` INTEGER, `projectId` INTEGER NOT NULL, `type` TEXT , `link` TEXT , `path` TEXT, `iconUrl` TEXT , `previewAvailable` INTEGER, `mimetype` TEXT, FOREIGN KEY(`projectId`) REFERENCES `OcsProject`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE INDEX `index_projectResource_accID` ON `OcsProjectResource` (`accountId`)"); + database.execSQL("CREATE INDEX `index_projectResource_projectId` ON `OcsProjectResource` (`projectId`)"); + database.execSQL("CREATE UNIQUE INDEX `index_OcsProjectResource_accountId_id` ON `OcsProjectResource` (`accountId`, `id`, `projectId`)"); + database.execSQL("CREATE INDEX `index_OcsProjectResource_id` ON `OcsProjectResource` (`id`)"); + database.execSQL("CREATE INDEX `index_OcsProjectResource_lastModifiedLocal` ON `OcsProjectResource` (`lastModifiedLocal`)"); + + database.execSQL("CREATE TABLE `JoinCardWithProject` (`status` INTEGER NOT NULL, `projectId` INTEGER NOT NULL, `cardId` INTEGER NOT NULL, PRIMARY KEY (`projectId`, `cardId`), FOREIGN KEY(`cardId`) REFERENCES `Card`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`projectId`) REFERENCES `OcsProject`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE INDEX `index_JoinCardWithProject_projectId` ON `JoinCardWithProject` (`projectId`)"); + database.execSQL("CREATE INDEX `index_JoinCardWithProject_cardId` ON `JoinCardWithProject` (`cardId`)"); + } + }; + private static final Migration MIGRATION_17_18 = new Migration(17, 18) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/435 + database.execSQL("ALTER TABLE `Account` ADD `etag` TEXT"); + } + }; + private static final Migration MIGRATION_18_19 = new Migration(18, 19) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/619 + database.execSQL("DROP INDEX `index_OcsProjectResource_accountId_id`"); + database.execSQL("ALTER TABLE `OcsProjectResource` ADD `idString` TEXT"); + database.execSQL("CREATE UNIQUE INDEX `index_OcsProjectResource_accountId_id` ON `OcsProjectResource` (`accountId`, `id`, `idString`, `projectId`)"); + } + }; + private static final Migration MIGRATION_19_20 = new Migration(19, 20) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/492 + // https://github.com/stefan-niedermann/nextcloud-deck/issues/631 + database.execSQL("CREATE TABLE `UserInGroup` (`groupId` INTEGER NOT NULL, `memberId` INTEGER NOT NULL, " + + "primary KEY(`groupId`, `memberId`), " + + "FOREIGN KEY(`groupId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, " + + "FOREIGN KEY(`memberId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE UNIQUE INDEX `unique_idx_group_member` ON `UserInGroup` (`groupId`, `memberId`)"); + database.execSQL("CREATE INDEX `index_UserInGroup_groupId` ON `UserInGroup` (`groupId`)"); + database.execSQL("CREATE INDEX `index_UserInGroup_memberId` ON `UserInGroup` (`memberId`)"); + + database.execSQL("CREATE TABLE `UserInBoard` (`userId` INTEGER NOT NULL, `boardId` INTEGER NOT NULL, " + + "primary KEY(`userId`, `boardId`), " + + "FOREIGN KEY(`userId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, " + + "FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE UNIQUE INDEX `unique_idx_user_board` ON `UserInBoard` (`userId`, `boardId`)"); + database.execSQL("CREATE INDEX `index_UserInBoard_userId` ON `UserInBoard` (`userId`)"); + database.execSQL("CREATE INDEX `index_UserInBoard_boardId` ON `UserInBoard` (`boardId`)"); + } + }; + + private static final Migration MIGRATION_20_21 = new Migration(20, 21) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/556 + String suffix = "_new"; + { + String tableName = "Account"; + database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `userName` TEXT NOT NULL, `url` TEXT NOT NULL, " + + "`color` INTEGER NOT NULL DEFAULT 0, `textColor` INTEGER NOT NULL DEFAULT 0, `serverDeckVersion` TEXT NOT NULL DEFAULT '0.6.4', `maintenanceEnabled` INTEGER NOT NULL DEFAULT 0, `etag` TEXT)"); + Cursor cursor = database.query("select * from `" + tableName + "`"); + while (cursor.moveToNext()) { + String colorAsString1 = cursor.getString(4); // color + String colorAsString2 = cursor.getString(5); // textColor + + @ColorInt Integer color1 = null; + @ColorInt Integer color2 = null; + try { + color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1)); + color2 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString2)); + } catch (Exception e) { + color1 = Color.GRAY; + color2 = Color.GRAY; + } + database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{ + cursor.getLong(0), cursor.getString(1), cursor.getString(2), cursor.getString(3), + color1, color2, cursor.getString(6), cursor.getInt(7), cursor.getString(8)}); + + } + + + database.execSQL("DROP TABLE `" + tableName + "`"); + database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`"); + database.execSQL("CREATE UNIQUE INDEX `index_Account_name` ON `" + tableName + "` (`name`)"); + database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName}); + } + { + String tableName = "Board"; + database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `status` INTEGER NOT NULL, " + + "`lastModified` INTEGER, `lastModifiedLocal` INTEGER, `title` TEXT, `ownerId` INTEGER NOT NULL, `color` INTEGER, " + + "`archived` INTEGER NOT NULL, `shared` INTEGER NOT NULL, `deletedAt` INTEGER, `permissionRead` INTEGER NOT NULL, " + + "`permissionEdit` INTEGER NOT NULL, `permissionManage` INTEGER NOT NULL, `permissionShare` INTEGER NOT NULL, " + + "FOREIGN KEY(`ownerId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE SET NULL )"); + Cursor cursor = database.query("select * from `" + tableName + "`"); + while (cursor.moveToNext()) { + String colorAsString1 = cursor.getString(8); // color + + @ColorInt Integer color1 = null; + try { + color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1)); + } catch (Exception e) { + color1 = Color.GRAY; + } + database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{ + cursor.getLong(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3), + cursor.getLong(4), cursor.getLong(5), cursor.getString(6), cursor.getLong(7), color1, + cursor.getInt(9), cursor.getInt(10), cursor.getInt(11), cursor.getInt(12), + cursor.getInt(13), cursor.getInt(14), cursor.getInt(15) + }); + + } + + + database.execSQL("DROP TABLE `" + tableName + "`"); + database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`"); + database.execSQL("CREATE INDEX `index_Board_accountId` ON `" + tableName + "` (`accountId`)"); + database.execSQL("CREATE UNIQUE INDEX `index_Board_accountId_id` ON `" + tableName + "` (`accountId`, `id`)"); + database.execSQL("CREATE INDEX `index_Board_id` ON `" + tableName + "` (`id`)"); + database.execSQL("CREATE INDEX `index_Board_ownerId` ON `" + tableName + "` (`ownerId`)"); + database.execSQL("CREATE INDEX `index_Board_lastModifiedLocal` ON `" + tableName + "` (`lastModifiedLocal`)"); + database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName}); + } + { + String tableName = "Label"; + database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `status` INTEGER NOT NULL, " + + "`lastModified` INTEGER, `lastModifiedLocal` INTEGER, `title` TEXT, `color` INTEGER NOT NULL DEFAULT 0, `boardId` INTEGER NOT NULL, " + + "FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + Cursor cursor = database.query("select * from `" + tableName + "`"); + while (cursor.moveToNext()) { + String colorAsString1 = cursor.getString(7); // color + + @ColorInt Integer color1 = null; + try { + color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1)); + } catch (Exception e) { + color1 = Color.GRAY; + } + database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{ + cursor.getLong(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3), + cursor.getLong(4), cursor.getLong(5), cursor.getString(6), color1, cursor.getLong(8)}); + + } + + + database.execSQL("DROP TABLE `" + tableName + "`"); + database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`"); + database.execSQL("CREATE UNIQUE INDEX `index_Label_accountId_id` ON `" + tableName + "` (`accountId`, `id`)"); + database.execSQL("CREATE INDEX `index_Label_boardId` ON `" + tableName + "` (`boardId`)"); + database.execSQL("CREATE INDEX `index_Label_accountId` ON `" + tableName + "` (`accountId`)"); + database.execSQL("CREATE UNIQUE INDEX `idx_label_title_unique` ON `" + tableName + "` (`boardId`, `title`)"); + database.execSQL("CREATE INDEX `index_Label_id` ON `" + tableName + "` (`id`)"); + database.execSQL("CREATE INDEX `index_Label_lastModifiedLocal` ON `" + tableName + "` (`lastModifiedLocal`)"); + database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName}); + } + } + }; + + private static final Migration MIGRATION_22_23 = new Migration(22, 23) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/359 + database.execSQL("ALTER TABLE `Account` ADD `boardsEtag` TEXT"); + database.execSQL("ALTER TABLE `Board` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Stack` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Card` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Label` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `AccessControl` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Attachment` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `User` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `DeckComment` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `Activity` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `OcsProject` ADD `etag` TEXT"); + database.execSQL("ALTER TABLE `OcsProjectResource` ADD `etag` TEXT"); + } + }; + + public static final RoomDatabase.Callback ON_CREATE_CALLBACK = new RoomDatabase.Callback() { @Override public void onCreate(@NonNull SupportSQLiteDatabase db) { super.onCreate(db); @@ -216,6 +434,26 @@ public abstract class DeckDatabase extends RoomDatabase { .apply(); } }) + .addMigrations(MIGRATION_15_16) + .addMigrations(MIGRATION_16_17) + .addMigrations(MIGRATION_17_18) + .addMigrations(MIGRATION_18_19) + .addMigrations(MIGRATION_19_20) + .addMigrations(MIGRATION_20_21) + .addMigrations(new Migration(21, 22) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/715 + final SharedPreferences.Editor lastSyncPref = context.getApplicationContext().getSharedPreferences("it.niedermann.nextcloud.deck.last_sync", Context.MODE_PRIVATE).edit(); + Cursor cursor = database.query("select id from `Account`"); + while (cursor.moveToNext()) { + lastSyncPref.remove("lS_" + cursor.getLong(0)); + } + cursor.close(); + lastSyncPref.apply(); + } + }) + .addMigrations(MIGRATION_22_23) .fallbackToDestructiveMigration() .addCallback(ON_CREATE_CALLBACK) .build(); @@ -256,4 +494,16 @@ public abstract class DeckDatabase extends RoomDatabase { public abstract MentionDao getMentionDao(); public abstract SingleCardWidgetModelDao getSingleCardWidgetModelDao(); + + public abstract StackWidgetModelDao getStackWidgetModelDao(); + + public abstract OcsProjectDao getOcsProjectDao(); + + public abstract OcsProjectResourceDao getOcsProjectResourceDao(); + + public abstract JoinCardWithOcsProjectDao getJoinCardWithOcsProjectDao(); + + public abstract UserInGroupDao getUserInGroupDao(); + + public abstract UserInBoardDao getUserInBoardDao(); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java index 2d0887903..8f7ee1cba 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java @@ -25,6 +25,10 @@ public interface AttachmentDao extends GenericDao<Attachment> { @Query("SELECT * FROM attachment WHERE accountId = :accountId and (status<>1 or id is null or lastModified <> lastModifiedLocal)") List<Attachment> getLocallyChangedAttachmentsDirectly(long accountId); + @Query("SELECT a.* FROM attachment a inner join card c on c.localId = a.cardId " + + "WHERE c.stackId = :localStackId and (a.status<>1 or a.id is null or a.lastModified <> a.lastModifiedLocal)") + List<Attachment> getLocallyChangedAttachmentsForStackDirectly(long localStackId); + @Query("SELECT * FROM attachment WHERE accountId = :accountId and cardId = :localCardId") List<Attachment> getAttachmentsForLocalCardIdDirectly(long accountId, Long localCardId); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java index bd189e846..6cb322aa3 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java @@ -13,6 +13,9 @@ import it.niedermann.nextcloud.deck.model.full.FullBoard; @Dao public interface BoardDao extends GenericDao<Board> { + @Query("SELECT * FROM board WHERE accountId = :accountId and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") + LiveData<List<Board>> getBoardsForAccount(final long accountId); + @Query("SELECT * FROM board WHERE accountId = :accountId and archived = 1 and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") LiveData<List<Board>> getArchivedBoardsForAccount(final long accountId); @@ -52,7 +55,8 @@ public interface BoardDao extends GenericDao<Board> { @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId") Board getBoardByLocalCardIdDirectly(long localCardId); - @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId") + @Transaction + @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId and c.stackId = s.localId") FullBoard getFullBoardByLocalCardIdDirectly(long localCardId); @Transaction @@ -72,4 +76,7 @@ public interface BoardDao extends GenericDao<Board> { @Query("SELECT count(*) FROM board WHERE accountId = :accountId and archived = 1 and (deletedAt = 0 or deletedAt is null) and status <> 3") LiveData<Integer> countArchivedBoards(long accountId); + + @Query("SELECT * FROM board WHERE accountId = :accountId and title = :title") + Board getBoardForAccountByNameDirectly(long accountId, String title); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java index 57163112a..82bdf1b8d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java @@ -11,6 +11,7 @@ import java.util.List; import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; @Dao public interface CardDao extends GenericDao<Card> { @@ -36,17 +37,24 @@ public interface CardDao extends GenericDao<Card> { @Query("SELECT * FROM card WHERE accountId = :accountId AND archived = 0 AND stackId = :localStackId and status<>3 order by `order`, createdAt asc") LiveData<List<FullCard>> getFullCardsForStack(final long accountId, final long localStackId); - @Transaction // v not deleted! + @Transaction @RawQuery(observedEntities = Card.class) LiveData<List<FullCard>> getFilteredFullCardsForStack(SupportSQLiteQuery query); @Transaction + @RawQuery(observedEntities = Card.class) + List<FullCard> getFilteredFullCardsForStackDirectly(SupportSQLiteQuery query); + + @Transaction @Query("SELECT * FROM card WHERE accountId = :accountId AND stackId = :localStackId order by `order`, createdAt asc") List<FullCard> getFullCardsForStackDirectly(final long accountId, final long localStackId); @Transaction @Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localCardId") LiveData<FullCard> getFullCardByLocalId(final long accountId, final long localCardId); + @Transaction + @Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localCardId") + LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(final long accountId, final long localCardId); @Transaction @Query("SELECT * FROM card WHERE accountId = :accountId and id = :remoteId") @@ -66,9 +74,15 @@ public interface CardDao extends GenericDao<Card> { @Query("SELECT * FROM card c WHERE accountId = :accountId and exists ( select 1 from DeckComment dc where dc.objectId = c.localId and dc.status<>1)") List<Card> getCardsWithLocallyChangedCommentsDirectly(Long accountId); + @Query("SELECT * FROM card c WHERE stackId = :localStackId and exists ( select 1 from DeckComment dc where dc.objectId = c.localId and dc.status<>1)") + List<Card> getCardsWithLocallyChangedCommentsForStackDirectly(Long localStackId); + @Query("SELECT count(*) FROM card c WHERE accountId = :accountId and stackId = :localStackId and status <> 3") LiveData<Integer> countCardsInStack(long accountId, long localStackId); @Query("SELECT coalesce(MAX(`order`), -1) FROM card c WHERE stackId = :localStackId and status <> 3") Integer getHighestOrderInStack(Long localStackId); + + @Query("SELECT c.stackId FROM card c WHERE localId = :localCardId") + Long getLocalStackIdByLocalCardId(Long localCardId); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java index c96c64e7d..26e6c65a8 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java @@ -40,6 +40,9 @@ public interface JoinCardWithLabelDao extends GenericDao<JoinCardWithLabel> { @Query("select * from joincardwithlabel WHERE status <> 1") // not UP_TO_DATE List<JoinCardWithLabel> getAllChangedJoins(); + @Query("select j.* from joincardwithlabel j inner join card c on j.cardId = c.localId WHERE c.stackId = :localStackId and j.status <> 1") // not UP_TO_DATE + List<JoinCardWithLabel> getAllChangedJoinsForStack(Long localStackId); + @Query("delete from joincardwithlabel " + "where cardId = (select c.localId from card c where c.accountId = :accountId and c.id = :remoteCardId) " + "and labelId = (select l.localId from label l where l.accountId = :accountId and l.id = :remoteLabelId)") diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java index 46554685f..416d52eed 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java @@ -25,7 +25,14 @@ public interface JoinCardWithUserDao extends GenericDao<JoinCardWithUser> { "inner join card c on j.cardId = c.localId " + "inner join user u on j.userId = u.localId " + "WHERE j.status <> 1") // not UP_TO_DATE - List<JoinCardWithUser> getDeletedJoinsWithRemoteIDs(); + List<JoinCardWithUser> getChangedJoinsWithRemoteIDs(); + + @Query("select u.localId as userId, c.id as cardId, j.status from joincardwithuser j " + + "inner join card c on j.cardId = c.localId " + + "inner join user u on j.userId = u.localId " + + "WHERE c.stackId = :localStackId " + + "AND j.status <> 1") // not UP_TO_DATE + List<JoinCardWithUser> getChangedJoinsWithRemoteIDsForStack(Long localStackId); @Query("delete from joincardwithuser " + "where cardId = (select c.localId from card c where c.accountId = :accountId and c.id = :remoteCardId) " + diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java index 0fbccbe08..feb7e453b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java @@ -13,7 +13,7 @@ import it.niedermann.nextcloud.deck.model.full.FullStack; @Dao public interface StackDao extends GenericDao<Stack> { - @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId order by `order` asc") + @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId and status<>3 and (deletedAt is null or deletedAt = 0) order by `order` asc") LiveData<List<Stack>> getStacksForBoard(final long accountId, final long localBoardId); @Query("SELECT * FROM stack WHERE accountId = :accountId and boardId = :localBoardId and id = :remoteId") @@ -31,10 +31,6 @@ public interface StackDao extends GenericDao<Stack> { FullStack getFullStackByRemoteIdDirectly(final long accountId, final long localBoardId, final long remoteId); @Transaction - @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId and status<>3 and (deletedAt is null or deletedAt = 0) order by `order` asc") - LiveData<List<FullStack>> getFullStacksForBoard(final long accountId, final long localBoardId); - - @Transaction @Query("SELECT * FROM stack WHERE accountId = :accountId and boardId = :localBoardId and id = :remoteId") LiveData<FullStack> getFullStackByRemoteId(final long accountId, final long localBoardId, final long remoteId); @@ -56,4 +52,7 @@ public interface StackDao extends GenericDao<Stack> { @Query("SELECT localId FROM stack s WHERE accountId = :accountId and id = :stackId") Long getLocalStackIdByRemoteStackIdDirectly(long accountId, Long stackId); + + @Query("SELECT coalesce(MAX(`order`), -1) FROM stack s WHERE boardId = :localBoardId") + Integer getHighestStackOrderInBoard(long localBoardId); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java index 6f712d07f..6e42cdaae 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java @@ -26,12 +26,16 @@ public interface UserDao extends GenericDao<User> { " where ju.userId = u.localId" + " and ju.cardId = :notYetAssignedToLocalCardId AND status <> 3" + // not LOCAL_DELETED " )" + - " AND" + - " (" + - " EXISTS (" + - " select 1 from accesscontrol" + - " where userId = u.localId and boardId = :boardId" + - " )" + + " AND ( " + + " EXISTS (" + + " select 1 from userinboard where boardId = :boardId AND userId = u.localId" + + " )" + + " OR" + + " EXISTS (" + + " select 1 from accesscontrol" + // v GROUP! + " where (userId = u.localId OR (type = 1 and exists(select 1 from UserInGroup uig where uig.memberId = u.localId and uig.groupId = userId))) " + + " and boardId = :boardId and status <> 3" + + " )" + " OR" + " EXISTS (" + " select 1 from board where localId = :boardId AND ownerId = u.localId" + @@ -48,7 +52,7 @@ public interface UserDao extends GenericDao<User> { "and ( uid LIKE :searchTerm or displayname LIKE :searchTerm or primaryKey LIKE :searchTerm ) " + "and u.localId <> (select b.ownerId from board b where localId = :boardId)" + "ORDER BY u.displayname") - LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long boardId, final String searchTerm); + List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long boardId, final String searchTerm); @Query("SELECT * FROM user WHERE accountId = :accountId and uid = :uid") User getUserByUidDirectly(final long accountId, final String uid); @@ -66,12 +70,16 @@ public interface UserDao extends GenericDao<User> { " where ju.userId = u.localId" + " and ju.cardId = :notAssignedToLocalCardId AND status <> 3" + // not LOCAL_DELETED " )" + - " AND" + - " (" + - " EXISTS (" + - " select 1 from accesscontrol" + - " where userId = u.localId and boardId = :boardId" + - " )" + + " AND ( " + + " EXISTS (" + + " select 1 from userinboard where boardId = :boardId AND userId = u.localId" + + " )" + + " OR" + + " EXISTS (" + + " select 1 from accesscontrol" + // v GROUP! + " where (userId = u.localId OR (type = 1 and exists(select 1 from UserInGroup uig where uig.memberId = u.localId and uig.groupId = userId))) " + + " and boardId = :boardId and status <> 3" + + " )" + " OR" + " EXISTS (" + " select 1 from board where localId = :boardId AND ownerId = u.localId" + diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java new file mode 100644 index 000000000..7a0476c53 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java @@ -0,0 +1,12 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao; + +import androidx.room.Dao; +import androidx.room.Query; + +import it.niedermann.nextcloud.deck.model.relations.UserInBoard; + +@Dao +public interface UserInBoardDao extends GenericDao<UserInBoard> { + @Query("DELETE FROM userinboard WHERE boardId = :localId") + void deleteByBoardId(long localId); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java new file mode 100644 index 000000000..fb3f5b26e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java @@ -0,0 +1,12 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao; + +import androidx.room.Dao; +import androidx.room.Query; + +import it.niedermann.nextcloud.deck.model.relations.UserInGroup; + +@Dao +public interface UserInGroupDao extends GenericDao<UserInGroup> { + @Query("DELETE FROM useringroup WHERE groupId = :localId") + void deleteByGroupId(long localId); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java new file mode 100644 index 000000000..d132796e4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java @@ -0,0 +1,13 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects; + +import androidx.room.Dao; +import androidx.room.Query; + +import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface JoinCardWithOcsProjectDao extends GenericDao<JoinCardWithProject> { + @Query("select * from JoinCardWithProject where projectId = :localProjectId and cardId = :localCardId") + JoinCardWithProject getAssignmentByCardIdAndProjectIdDirectly(Long localCardId, Long localProjectId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java new file mode 100644 index 000000000..fb0e3f836 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java @@ -0,0 +1,13 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects; + +import androidx.room.Dao; +import androidx.room.Query; + +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface OcsProjectDao extends GenericDao<OcsProject> { + @Query("select * from OcsProject where accountId = :accountId and id = :remoteId") + OcsProject getProjectByRemoteIdDirectly(long accountId, Long remoteId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java new file mode 100644 index 000000000..8a544667a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java @@ -0,0 +1,25 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface OcsProjectResourceDao extends GenericDao<OcsProjectResource> { + @Query("delete from OcsProjectResource where projectId = :localProjectId") + void deleteByProjectId(Long localProjectId); + + @Query("select * from OcsProjectResource where projectId = :localProjectId") + LiveData<List<OcsProjectResource>> getResourcesByLocalProjectId(Long localProjectId); + + @Query("select count(id) from OcsProjectResource where projectId = :localProjectId") + int countProjectResourcesInProjectDirectly(Long localProjectId); + + @Query("select count(id) from OcsProjectResource where projectId = :localProjectId") + LiveData<Integer> countProjectResourcesInProject(Long localProjectId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/SingleCardWidgetModelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/SingleCardWidgetModelDao.java index 0c2a485c1..a26269c30 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/SingleCardWidgetModelDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/SingleCardWidgetModelDao.java @@ -1,4 +1,4 @@ -package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao; +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets; import androidx.room.Dao; import androidx.room.Query; @@ -6,6 +6,7 @@ import androidx.room.Transaction; import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; @Dao public interface SingleCardWidgetModelDao extends GenericDao<SingleCardWidgetModel> { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java new file mode 100644 index 000000000..9b370e32b --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java @@ -0,0 +1,19 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets; + +import androidx.room.Dao; +import androidx.room.Query; +import androidx.room.Transaction; + +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface StackWidgetModelDao extends GenericDao<StackWidgetModel> { + + @Query("SELECT * FROM stackwidgetmodel WHERE appwidgetid = :appWidgetId") + StackWidgetModel getStackWidgetByAppWidgetIdDirectly(final int appWidgetId); + + @Transaction + @Query("SELECT EXISTS (SELECT 1 FROM stackwidgetmodel WHERE stackId in (:stackLocalIds))") + boolean containsStackLocalId(final long... stackLocalIds); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java index 038fcbad8..003bceb26 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java @@ -1,5 +1,6 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.MutableLiveData; @@ -28,10 +29,13 @@ public class WrappedLiveData<T> extends MutableLiveData<T> { } public void postError(@Nullable Throwable error) { + postError(error, null); + } + public void postError(@Nullable Throwable error, @NonNull T locallyCreatedEntity) { if (error == null) { DeckLog.warn("Given error is null"); } setError(error); - postValue(null); + postValue(locallyCreatedEntity); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java new file mode 100644 index 000000000..fca4ca369 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java @@ -0,0 +1,75 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class Debouncer <T> { + private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1); + private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>(); + private final Callback<T> callback; + private final int interval; + + public Debouncer(Callback<T> c, int interval) { + this.callback = c; + this.interval = interval; + } + + public void call(T key) { + TimerTask task = new TimerTask(key); + + TimerTask prev; + do { + prev = delayedMap.putIfAbsent(key, task); + if (prev == null) + sched.schedule(task, interval, TimeUnit.MILLISECONDS); + // Exit only if new task was added to map, or existing task was extended successfully + } while (prev != null && !prev.extend()); + } + + public void terminate() { + sched.shutdownNow(); + } + + public interface Callback<T> { + void call(T key); + } + + // The task that wakes up when the wait time elapses + private class TimerTask implements Runnable { + private final T key; + private long dueTime; + private final Object lock = new Object(); + + public TimerTask(T key) { + this.key = key; + extend(); + } + + public boolean extend() { + synchronized (lock) { + if (dueTime < 0) // Task has been shutdown + return false; + dueTime = System.currentTimeMillis() + interval; + return true; + } + } + + public void run() { + synchronized (lock) { + long remaining = dueTime - System.currentTimeMillis(); + if (remaining > 0) { // Re-schedule task + sched.schedule(this, remaining, TimeUnit.MILLISECONDS); + } else { // Mark as terminated and invoke callback + dueTime = -1; + try { + callback.call(key); + } finally { + delayedMap.remove(key); + } + } + } + } + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java new file mode 100644 index 000000000..179d816eb --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java @@ -0,0 +1,93 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst; + +import androidx.lifecycle.MediatorLiveData; + +import java.util.List; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.exceptions.OfflineException; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.enums.DBStatus; +import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; +import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; + +public class UserSearchLiveData extends MediatorLiveData<List<User>> implements Debouncer.Callback<Long> { + + private static final int DEBOUNCE_TIME = 300; // ms + private DataBaseAdapter db; + private ServerAdapter server; + long accountId; + String searchTerm; + long notYetAssignedInACL; + private Debouncer<Long> debouncer = new Debouncer<>(this, DEBOUNCE_TIME); + + public UserSearchLiveData(DataBaseAdapter db, ServerAdapter server) { + this.db = db; + this.server = server; + } + + public UserSearchLiveData search(long accountId, long notYetAssignedInACL, String searchTerm) { + this.accountId = accountId; + this.searchTerm = searchTerm; + this.notYetAssignedInACL = notYetAssignedInACL; + new Thread(() -> debouncer.call(notYetAssignedInACL)).start(); + return this; + } + + + @Override + public void call(Long key) { + if (key!=notYetAssignedInACL){ + return; + } + + final String term = String.copyValueOf(searchTerm.toCharArray()); + + postCurrentFromDB(term); + + if (server.hasInternetConnection()) { + try { + Account account = db.getAccountByIdDirectly(accountId); + server.searchUser(term, new IResponseCallback<OcsUserList>(account) { + @Override + public void onResponse(OcsUserList response) { + if (response == null || response.getUsers().isEmpty()){ + return; + } + for (OcsUser user : response.getUsers()) { + User existingUser = db.getUserByUidDirectly(accountId, user.getId()); + if (existingUser == null) { + User newUser = new User(); + newUser.setStatus(DBStatus.UP_TO_DATE.getId()); + newUser.setPrimaryKey(user.getId()); + newUser.setUid(user.getId()); + newUser.setDisplayname(user.getDisplayName()); + db.createUser(accountId, newUser); + } + } + if (!term.equals(searchTerm)) { + return; + } + postCurrentFromDB(term); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + } + }); + } catch (OfflineException e) { + DeckLog.logError(e); + } + } + } + + private void postCurrentFromDB(String term) { + List<User> foundInDB = db.searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedInACL, term); + postValue(foundInDB); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/DataPropagationHelper.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/DataPropagationHelper.java index 9590f5abd..782b6d951 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/DataPropagationHelper.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/DataPropagationHelper.java @@ -34,27 +34,31 @@ public class DataPropagationHelper { entity.setLocalId(newID); boolean connected = serverAdapter.hasInternetConnection(); if (connected) { - provider.createOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) { - @Override - public void onResponse(T response) { - new Thread(() -> { - response.setAccountId(accountId); - response.setLocalId(newID); - if (actionOnResponse!= null) { - actionOnResponse.onResponse(entity, response); - } - response.setStatus(DBStatus.UP_TO_DATE.getId()); - provider.updateInDB(dataBaseAdapter, accountId, response, false); - callback.onResponse(response); - }).start(); - } + try { + provider.createOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) { + @Override + public void onResponse(T response) { + new Thread(() -> { + response.setAccountId(accountId); + response.setLocalId(newID); + if (actionOnResponse != null) { + actionOnResponse.onResponse(entity, response); + } + response.setStatus(DBStatus.UP_TO_DATE.getId()); + provider.updateInDB(dataBaseAdapter, accountId, response, false); + callback.onResponse(response); + }).start(); + } - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - new Thread(() -> callback.onError(throwable)).start(); - } - }, entity); + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + new Thread(() -> callback.onError(throwable, entity)).start(); + } + }, entity); + } catch (Throwable t) { + callback.onError(t, entity); + } } else { callback.onResponse(entity); } @@ -71,24 +75,28 @@ public class DataPropagationHelper { } boolean connected = serverAdapter.hasInternetConnection(); if (entity.getId() != null && connected) { - provider.updateOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) { - @Override - public void onResponse(T response) { - new Thread(() -> { - entity.setStatus(DBStatus.UP_TO_DATE.getId()); - provider.updateInDB(dataBaseAdapter, accountId, entity, false); - callback.onResponse(entity); - }).start(); - } + try { + provider.updateOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) { + @Override + public void onResponse(T response) { + new Thread(() -> { + entity.setStatus(DBStatus.UP_TO_DATE.getId()); + provider.updateInDB(dataBaseAdapter, accountId, entity, false); + callback.onResponse(entity); + }).start(); + } - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - new Thread(() -> { - callback.onError(throwable); - }).start(); - } - }, entity); + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + new Thread(() -> { + callback.onError(throwable, entity); + }).start(); + } + }, entity); + } catch (Throwable t) { + callback.onError(t, entity); + } } else { callback.onResponse(entity); } @@ -98,23 +106,28 @@ public class DataPropagationHelper { provider.deleteInDB(dataBaseAdapter, accountId, entity); boolean connected = serverAdapter.hasInternetConnection(); if (entity.getId() != null && connected) { - provider.deleteOnServer(serverAdapter, accountId, new IResponseCallback<Void>(new Account(accountId)) { - @Override - public void onResponse(Void response) { - new Thread(() -> { - provider.deletePhysicallyInDB(dataBaseAdapter, accountId, entity); - callback.onResponse(null); - }).start(); - } + try { + provider.deleteOnServer(serverAdapter, accountId, new IResponseCallback<Void>(new Account(accountId)) { + @Override + public void onResponse(Void response) { + new Thread(() -> { + provider.deletePhysicallyInDB(dataBaseAdapter, accountId, entity); + callback.onResponse(null); + }).start(); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + new Thread(() -> { + callback.onError(throwable); + }).start(); + } + }, entity, dataBaseAdapter); + } catch (Throwable t) { + callback.onError(t); + } - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - new Thread(() -> { - callback.onError(throwable); - }).start(); - } - }, entity, dataBaseAdapter); } else { callback.onResponse(null); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java index 18d8c3672..17c0de2cf 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java @@ -1,6 +1,12 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers; -import java.util.Date; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; + +import java.net.HttpURLConnection; +import java.time.Instant; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -15,28 +21,33 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AbstractS import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.IRelationshipProvider; public class SyncHelper { - private ServerAdapter serverAdapter; - private DataBaseAdapter dataBaseAdapter; + private final ServerAdapter serverAdapter; + private final DataBaseAdapter dataBaseAdapter; private Account account; private long accountId; private IResponseCallback<Boolean> responseCallback; - private Date lastSync; + private final Instant lastSync; - public SyncHelper(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, Date lastSync) { + public SyncHelper(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, Instant lastSync) { this.serverAdapter = serverAdapter; this.dataBaseAdapter = dataBaseAdapter; this.lastSync = lastSync; } // Sync Server -> App - public <T extends IRemoteEntity> void doSyncFor(final AbstractSyncDataProvider<T> provider){ + public <T extends IRemoteEntity> void doSyncFor(@NonNull final AbstractSyncDataProvider<T> provider) { provider.registerChildInParent(provider); - provider.getAllFromServer(serverAdapter, accountId, new IResponseCallback<List<T>>(account) { + provider.getAllFromServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<List<T>>(account) { @Override public void onResponse(List<T> response) { if (response != null) { provider.goingDeeper(); for (T entityFromServer : response) { + if (entityFromServer == null) { + // see https://github.com/stefan-niedermann/nextcloud-deck/issues/574 + DeckLog.error("Skipped null value from server for DataProvider: " + provider.getClass().getSimpleName()); + continue; + } entityFromServer.setAccountId(accountId); T existingEntity = provider.getSingleFromDB(dataBaseAdapter, accountId, entityFromServer); @@ -44,13 +55,14 @@ public class SyncHelper { provider.createInDB(dataBaseAdapter, accountId, entityFromServer); } else { //TODO: how to handle deletes? what about archived? - if (existingEntity.getStatus() != DBStatus.UP_TO_DATE.getId()){ - DeckLog.log("Conflicting changes on entity: "+existingEntity); + if (existingEntity.getStatus() != DBStatus.UP_TO_DATE.getId()) { + DeckLog.warn("Conflicting changes on entity: " + existingEntity); // TODO: what to do? } else { -// if (existingEntity.getLastModified().getTime() == entityFromServer.getLastModified().getTime()) { -// continue; // TODO: is this is ok for sure? -> isn`t! NPE -// } + if (entityFromServer.getEtag() != null && entityFromServer.getEtag().equals(existingEntity.getEtag())) { + DeckLog.log("[" + provider.getClass().getSimpleName() + "] ETags do match! skipping " + existingEntity.getClass().getSimpleName() + " with localId: " + existingEntity.getLocalId()); + continue; + } provider.updateInDB(dataBaseAdapter, accountId, applyUpdatesFromRemote(provider, existingEntity, entityFromServer, accountId), false); } } @@ -58,7 +70,7 @@ public class SyncHelper { provider.goDeeper(SyncHelper.this, existingEntity, entityFromServer, responseCallback); } - provider.handleDeletes(serverAdapter, dataBaseAdapter, accountId, response); + provider.handleDeletes(serverAdapter, dataBaseAdapter, accountId, response); provider.doneGoingDeeper(responseCallback, true); } else { @@ -68,25 +80,35 @@ public class SyncHelper { @Override public void onError(Throwable throwable) { + super.onError(throwable); + if (throwable.getClass() == NextcloudHttpRequestFailedException.class) { + NextcloudHttpRequestFailedException requestFailedException = (NextcloudHttpRequestFailedException) throwable; + if (HttpURLConnection.HTTP_NOT_MODIFIED == requestFailedException.getStatusCode()){ + DeckLog.log("[" + provider.getClass().getSimpleName() + "] ETags do match! skipping this one."); + // well, etags say we're fine here. no need to go deeper. + provider.childDone(provider, responseCallback, false); + return; + } + } provider.onError(throwable, responseCallback); - DeckLog.logError(throwable); responseCallback.onError(throwable); } }, lastSync); } // Sync App -> Server - public <T extends IRemoteEntity> void doUpSyncFor(AbstractSyncDataProvider<T> provider){ + public <T extends IRemoteEntity> void doUpSyncFor(@NonNull AbstractSyncDataProvider<T> provider) { doUpSyncFor(provider, null); } - public <T extends IRemoteEntity> void doUpSyncFor(AbstractSyncDataProvider<T> provider, CountDownLatch countDownLatch){ - List<T> allFromDB = provider.getAllChangedFromDB(dataBaseAdapter, accountId, lastSync); + + public <T extends IRemoteEntity> void doUpSyncFor(@NonNull AbstractSyncDataProvider<T> provider, @Nullable CountDownLatch countDownLatch) { + final List<T> allFromDB = provider.getAllChangedFromDB(dataBaseAdapter, accountId, lastSync); if (allFromDB != null && !allFromDB.isEmpty()) { for (T entity : allFromDB) { - if (entity.getId()!=null) { + if (entity.getId() != null) { if (entity.getStatusEnum() == DBStatus.LOCAL_DELETED) { provider.deleteOnServer(serverAdapter, accountId, getDeleteCallback(provider, entity), entity, dataBaseAdapter); - if (countDownLatch != null){ + if (countDownLatch != null) { countDownLatch.countDown(); } } else { @@ -98,13 +120,13 @@ public class SyncHelper { } } else { provider.goDeeperForUpSync(this, serverAdapter, dataBaseAdapter, responseCallback); - if (countDownLatch != null){ + if (countDownLatch != null) { countDownLatch.countDown(); } } } - private <T extends IRemoteEntity> IResponseCallback<Void> getDeleteCallback(AbstractSyncDataProvider<T> provider, T entity) { + private <T extends IRemoteEntity> IResponseCallback<Void> getDeleteCallback(@NonNull AbstractSyncDataProvider<T> provider, T entity) { return new IResponseCallback<Void>(account) { @Override public void onResponse(Void response) { @@ -120,16 +142,17 @@ public class SyncHelper { }; } - private <T extends IRemoteEntity> IResponseCallback<T> getUpdateCallback(AbstractSyncDataProvider<T> provider, T entity, CountDownLatch countDownLatch) { + private <T extends IRemoteEntity> IResponseCallback<T> getUpdateCallback(@NonNull AbstractSyncDataProvider<T> provider, @NonNull T entity, @Nullable CountDownLatch countDownLatch) { return new IResponseCallback<T>(account) { @Override public void onResponse(T response) { response.setAccountId(this.account.getId()); T update = applyUpdatesFromRemote(provider, entity, response, accountId); + update.setId(response.getId()); update.setStatus(DBStatus.UP_TO_DATE.getId()); provider.updateInDB(dataBaseAdapter, accountId, update, false); provider.goDeeperForUpSync(SyncHelper.this, serverAdapter, dataBaseAdapter, responseCallback); - if (countDownLatch != null){ + if (countDownLatch != null) { countDownLatch.countDown(); } } @@ -138,20 +161,20 @@ public class SyncHelper { public void onError(Throwable throwable) { super.onError(throwable); responseCallback.onError(throwable); - if (countDownLatch != null){ + if (countDownLatch != null) { countDownLatch.countDown(); } } }; } - public void fixRelations(IRelationshipProvider relationshipProvider) { + public void fixRelations(@NonNull IRelationshipProvider relationshipProvider) { // this is OK, since the delete only affects records with status UP_TO_DATE relationshipProvider.deleteAllExisting(dataBaseAdapter, accountId); relationshipProvider.insertAllNecessary(dataBaseAdapter, accountId); } - private <T extends IRemoteEntity> T applyUpdatesFromRemote(AbstractSyncDataProvider<T> provider, T localEntity, T remoteEntity, Long accountId) { + private <T extends IRemoteEntity> T applyUpdatesFromRemote(@NonNull AbstractSyncDataProvider<T> provider, @NonNull T localEntity, @NonNull T remoteEntity, @NonNull Long accountId) { if (!accountId.equals(localEntity.getAccountId())) { throw new IllegalArgumentException("IDs of Accounts are not matching! WTF are you doin?!"); } @@ -160,7 +183,7 @@ public class SyncHelper { return remoteEntity; } - public SyncHelper setResponseCallback(IResponseCallback<Boolean> callback) { + public SyncHelper setResponseCallback(@NonNull IResponseCallback<Boolean> callback) { this.responseCallback = callback; this.account = responseCallback.getAccount(); accountId = account.getId(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java index 166d6c519..11fb38a66 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java @@ -1,9 +1,10 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.List; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity; import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; @@ -26,28 +27,37 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> { } } - public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<T> entitiesFromServer){ + public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<T> entitiesFromServer) { // do nothing as a default. } /** * Searches each entry of <code>listB</code> in list <code>listA</code> and returns the missing ones + * * @param listA List * @param listB List * @return all entries of <code>listB</code> missing in <code>listA</code> */ - public static <T extends IRemoteEntity> List<T> findDelta(List<T> listA, List<T> listB){ + public static <T extends IRemoteEntity> List<T> findDelta(List<T> listA, List<T> listB) { List<T> delta = new ArrayList<>(); for (T b : listB) { + if (b == null) { + DeckLog.error("Entry in listB is null! skipping..."); + continue; + } boolean found = false; for (T a : listA) { - if ((a.getLocalId()!= null && b.getLocalId()!= null ? (a.getLocalId().equals(b.getLocalId())) + if (a == null) { + DeckLog.error("Entry in listA is null! skipping..."); + continue; + } + if ((a.getLocalId() != null && b.getLocalId() != null ? (a.getLocalId().equals(b.getLocalId())) : a.getId().equals(b.getId())) && b.getAccountId() == a.getAccountId()) { found = true; break; } } - if (!found){ + if (!found) { delta.add(b); } } @@ -58,7 +68,14 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> { children.add(child); } - public abstract void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<T>> responder, Date lastSync); + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<T>> responder, Instant lastSync) { + return; + } + + public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<List<T>> responder, Instant lastSync) { + // Overridden, because we also need the DB-Adapter at some points here (see ACL data provider) + getAllFromServer(serverAdapter, accountId, responder, lastSync); + } public abstract T getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, T entity); @@ -72,7 +89,7 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> { public abstract void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, T t); - public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, T t){ + public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, T t) { deleteInDB(dataBaseAdapter, accountId, t); } @@ -106,7 +123,7 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> { stillGoingDeeper = true; } - public abstract List<T> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync); + public abstract List<T> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync); public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) { //do nothing diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java index 3242ab7c1..6dde6b45c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java @@ -1,17 +1,24 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; -import java.util.Date; +import java.time.Instant; import java.util.List; +import java.util.concurrent.CountDownLatch; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.AccessControl; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; +import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.AsyncUtil; public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessControl> { + private static final Long TYPE_GROUP = 1L; private List<AccessControl> acl; private FullBoard board; @@ -22,10 +29,66 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<AccessControl>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<List<AccessControl>> responder, Instant lastSync) { + AsyncUtil.awaitAsyncWork(acl.size(), latch -> { + for (AccessControl accessControl : acl) { + if (accessControl.getType() == TYPE_GROUP) { + serverAdapter.searchGroupMembers(accessControl.getUser().getUid(), new IResponseCallback<GroupMemberUIDs>(responder.getAccount()) { + @Override + public void onResponse(GroupMemberUIDs response) { + accessControl.setGroupMemberUIDs(response); + if (response.getUids().size() > 0) { + ensureGroupMembersInDB(getAccount(), dataBaseAdapter, serverAdapter, response); + } + latch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + latch.countDown(); + } + }); + } else latch.countDown(); + } + }); + responder.onResponse(acl); } + private void ensureGroupMembersInDB(Account account, DataBaseAdapter dataBaseAdapter, ServerAdapter serverAdapter, GroupMemberUIDs response) { + CountDownLatch memberLatch = new CountDownLatch(response.getUids().size()); + for (String uid : response.getUids()) { + User user = dataBaseAdapter.getUserByUidDirectly(account.getId(), uid); + if (user == null) { + // unknown user. fetch! + serverAdapter.getSingleUserData(uid, new IResponseCallback<OcsUser>(account) { + @Override + public void onResponse(OcsUser response) { + DeckLog.log(response.toString()); + User user = new User(); + user.setUid(response.getId()); + user.setPrimaryKey(response.getId()); + user.setDisplayname(response.getDisplayName()); + dataBaseAdapter.createUser(account.getId(), user); + memberLatch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + memberLatch.countDown(); + } + }); + } else memberLatch.countDown(); + } + try { + memberLatch.await(); + } catch (InterruptedException e) { + DeckLog.logError(e); + } + } + @Override public AccessControl getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) { return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, entity.getEntity().getId()); @@ -34,7 +97,26 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo @Override public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) { prepareUser(dataBaseAdapter, accountId, entity); - return dataBaseAdapter.createAccessControl(accountId, entity); + long newId = dataBaseAdapter.createAccessControl(accountId, entity); + entity.setLocalId(newId); + handleGroupMemberships(dataBaseAdapter, entity); + return newId; + } + + private void handleGroupMemberships(DataBaseAdapter dataBaseAdapter, AccessControl entity) { + if (entity.getType() != TYPE_GROUP) { + return; + } + dataBaseAdapter.deleteGroupMembershipsOfGroup(entity.getUser().getLocalId()); + if (entity.getGroupMemberUIDs() == null) { + return; + } + for (String groupMemberUID : entity.getGroupMemberUIDs().getUids()) { + User member = dataBaseAdapter.getUserByUidDirectly(entity.getAccountId(), groupMemberUID); + if (member != null) { + dataBaseAdapter.addUserToGroup(entity.getUserId(), member.getLocalId()); + } + } } private void prepareUser(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) { @@ -42,6 +124,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo if (user == null) { long userId = dataBaseAdapter.createUser(accountId, entity.getUser()); entity.setUserId(userId); + entity.getUser().setLocalId(userId); } else { entity.setUserId(user.getLocalId()); entity.getUser().setLocalId(user.getLocalId()); @@ -52,7 +135,9 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo @Override public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity, boolean setStatus) { prepareUser(dataBaseAdapter, accountId, entity); + entity.setBoardId(board.getLocalId()); dataBaseAdapter.updateAccessControl(entity, setStatus); + handleGroupMemberships(dataBaseAdapter, entity); } @Override @@ -64,6 +149,9 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<AccessControl> responder, AccessControl entity) { AccessControl acl = new AccessControl(entity); acl.setBoardId(board.getBoard().getId()); + if (acl.getUser() == null && acl.getUserId() != null) { + acl.setUser(dataBaseAdapter.getUserByLocalIdDirectly(acl.getUserId())); + } serverAdapter.createAccessControl(board.getBoard().getId(), acl, responder); } @@ -79,6 +167,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo @Override public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl accessControl) { + dataBaseAdapter.deleteGroupMembershipsOfGroup(accessControl.getUser().getLocalId()); dataBaseAdapter.deleteAccessControl(accessControl, false); } @@ -88,7 +177,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo } @Override - public List<AccessControl> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + public List<AccessControl> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { return dataBaseAdapter.getLocallyChangedAccessControl(accountId, board.getLocalId()); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java index a3785ca78..5a7abf732 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java @@ -1,7 +1,9 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; -import java.util.ArrayList; -import java.util.Date; +import androidx.annotation.NonNull; + +import java.time.Instant; +import java.util.Collections; import java.util.List; import it.niedermann.nextcloud.deck.api.IResponseCallback; @@ -12,15 +14,16 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter public class ActivityDataProvider extends AbstractSyncDataProvider<Activity> { - protected Card card; + @NonNull + private final Card card; - public ActivityDataProvider(AbstractSyncDataProvider<?> parent, Card card) { + public ActivityDataProvider(AbstractSyncDataProvider<?> parent, @NonNull Card card) { super(parent); this.card = card; } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Activity>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Activity>> responder, Instant lastSync) { serverAdapter.getActivitiesForCard(card.getId(), responder); } @@ -65,7 +68,7 @@ public class ActivityDataProvider extends AbstractSyncDataProvider<Activity> { } @Override - public List<Activity> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { - return new ArrayList<>(); + public List<Activity> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { + return Collections.emptyList(); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java index 37e00dada..781f7b1f4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java @@ -4,7 +4,7 @@ import android.net.Uri; import java.io.File; import java.io.IOException; -import java.util.Date; +import java.time.Instant; import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; @@ -32,7 +32,7 @@ public class AttachmentDataProvider extends AbstractSyncDataProvider<Attachment> } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Attachment>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Attachment>> responder, Instant lastSync) { responder.onResponse(attachments); } @@ -104,7 +104,7 @@ public class AttachmentDataProvider extends AbstractSyncDataProvider<Attachment> } @Override - public List<Attachment> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + public List<Attachment> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { return dataBaseAdapter.getLocallyChangedAttachmentsByLocalCardIdDirectly(accountId, card.getLocalId()); } @@ -120,7 +120,7 @@ public class AttachmentDataProvider extends AbstractSyncDataProvider<Attachment> dataBaseAdapter.deleteAttachment(accountId, attachment, false); } for (Attachment attachment : entitiesFromServer) { - if (attachment.getDeletedAt() != null && attachment.getDeletedAt().getTime() != 0) { + if (attachment.getDeletedAt() != null && attachment.getDeletedAt().toEpochMilli() != 0) { Attachment toDelete = dataBaseAdapter.getAttachmentByRemoteIdDirectly(accountId, attachment.getId()); if (toDelete != null) { dataBaseAdapter.deleteAttachment(accountId, toDelete, false); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java index 98163029f..f5e071adb 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java @@ -1,14 +1,16 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import android.annotation.SuppressLint; + +import com.nextcloud.android.sso.api.ParsedResponse; + +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.CountDownLatch; -import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.AccessControl; import it.niedermann.nextcloud.deck.model.Board; @@ -19,16 +21,33 @@ import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.AsyncUtil; public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { - public BoardDataProvider(){ + public BoardDataProvider() { super(null); } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullBoard>> responder, Date lastSync) { - serverAdapter.getBoards(responder); + public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<List<FullBoard>> responder, Instant lastSync) { + serverAdapter.getBoards(new IResponseCallback<ParsedResponse<List<FullBoard>>>(responder.getAccount()) { + @Override + public void onResponse(ParsedResponse<List<FullBoard>> response) { + String etag = response.getHeaders().get("ETag"); + if (etag != null && !etag.equals(account.getBoardsEtag())) { + account.setBoardsEtag(etag); + dataBaseAdapter.updateAccount(account); + } + responder.onResponse(response.getResponse()); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + responder.onError(throwable); + } + }); } @Override @@ -39,27 +58,65 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { @Override public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) { handleOwner(dataBaseAdapter, accountId, entity); - return dataBaseAdapter.createBoardDirectly(accountId, entity.getBoard()); + Long localId = dataBaseAdapter.createBoardDirectly(accountId, entity.getBoard()); + entity.getBoard().setLocalId(localId); + handleUsers(dataBaseAdapter, accountId, entity); + return localId; } private void handleOwner(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) { - if (entity.getOwner()!=null) { - User remoteOwner = entity.getOwner(); - User owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteOwner.getUid()); - if (owner == null){ - dataBaseAdapter.createUser(accountId, remoteOwner); - } else { - dataBaseAdapter.updateUser(accountId, remoteOwner, false); - } - owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteOwner.getUid()); + if (entity.getOwner() != null) { + User owner = createOrUpdateUser(dataBaseAdapter, accountId, entity.getOwner()); entity.getBoard().setOwnerId(owner.getLocalId()); } } + private void handleUsers(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) { + dataBaseAdapter.deleteBoardMembershipsOfBoard(entity.getLocalId()); + if (entity.getUsers() != null && !entity.getUsers().isEmpty()) { + for (User user : entity.getUsers()) { + if (user == null) { + continue; + } + User existing = createOrUpdateUser(dataBaseAdapter, accountId, user); + dataBaseAdapter.addUserToBoard(existing.getLocalId(), entity.getLocalId()); + } + } + } + + private User createOrUpdateUser(DataBaseAdapter dataBaseAdapter, long accountId, User remoteUser) { + User owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteUser.getUid()); + if (owner == null) { + dataBaseAdapter.createUser(accountId, remoteUser); + } else { + dataBaseAdapter.updateUser(accountId, remoteUser, false); + } + return dataBaseAdapter.getUserByUidDirectly(accountId, remoteUser.getUid()); + } + @Override public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity, boolean setStatus) { + handleDefaultLabels(dataBaseAdapter, entity); handleOwner(dataBaseAdapter, accountId, entity); dataBaseAdapter.updateBoard(entity.getBoard(), setStatus); + handleUsers(dataBaseAdapter, accountId, entity); + } + + private void handleDefaultLabels(DataBaseAdapter dataBaseAdapter, FullBoard entity) { + // ## merge labels (created at board creation): + // the server creates four default labels. if a board is copied, they will also be copied. At sync, after creating the board, the labels are already there. + // this merges the created default ones with the ones i already have. + if (entity != null && entity.getLabels() != null) { + for (Label label : entity.getLabels()) { + // does this label exist and unknown to server yet? + Label existing = dataBaseAdapter.getLabelByBoardIdAndTitleDirectly(entity.getLocalId(), label.getTitle()); + if (existing != null && existing.getId() == null) { + // take our label and lets say it IS the same as on server (but use the local color, no matter what the server says) + existing.setId(label.getId()); + dataBaseAdapter.updateLabel(existing, false); + } + } + } } @Override @@ -71,19 +128,19 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { @Override public void goDeeper(SyncHelper syncHelper, FullBoard existingEntity, FullBoard entityFromServer, IResponseCallback<Boolean> callback) { List<Label> labels = entityFromServer.getLabels(); - if (labels != null && !labels.isEmpty()){ + if (labels != null && !labels.isEmpty()) { syncHelper.doSyncFor(new LabelDataProvider(this, existingEntity.getBoard(), labels)); } List<AccessControl> acl = entityFromServer.getParticipants(); - if (acl != null && !acl.isEmpty()){ - for (AccessControl ac : acl){ + if (acl != null && !acl.isEmpty()) { + for (AccessControl ac : acl) { ac.setBoardId(existingEntity.getLocalId()); } syncHelper.doSyncFor(new AccessControlDataProvider(this, existingEntity, acl)); } - if (entityFromServer.getStacks() != null && !entityFromServer.getStacks().isEmpty()){ + if (entityFromServer.getStacks() != null && !entityFromServer.getStacks().isEmpty()) { syncHelper.doSyncFor(new StackDataProvider(this, existingEntity)); } } @@ -94,7 +151,7 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { } @Override - public List<FullBoard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + public List<FullBoard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { return dataBaseAdapter.getLocallyChangedBoards(accountId); } @@ -102,21 +159,17 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) { Long accountId = callback.getAccount().getId(); List<Label> locallyChangedLabels = dataBaseAdapter.getLocallyChangedLabels(accountId); - CountDownLatch countDownLatch = new CountDownLatch(locallyChangedLabels.size()); - for (Label label : locallyChangedLabels) { - Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId()); - label.setBoardId(board.getId()); - syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), countDownLatch); - } - try { - countDownLatch.await(); - } catch (InterruptedException e) { - DeckLog.logError(e); - } + AsyncUtil.awaitAsyncWork(locallyChangedLabels.size(), (countDownLatch) -> { + for (Label label : locallyChangedLabels) { + Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId()); + label.setBoardId(board.getId()); + syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), countDownLatch); + } + }); List<Long> localBoardIDsWithChangedACL = dataBaseAdapter.getBoardIDsOfLocallyChangedAccessControl(accountId); for (Long boardId : localBoardIDsWithChangedACL) { - syncHelper.doUpSyncFor(new AccessControlDataProvider(this, dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, boardId) ,new ArrayList<>())); + syncHelper.doUpSyncFor(new AccessControlDataProvider(this, dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, boardId), new ArrayList<>())); } Set<Long> syncedBoards = new HashSet<>(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java index 03e02bd70..0ceac2cd8 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java @@ -1,12 +1,15 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import android.annotation.SuppressLint; + +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.exceptions.DeckException; import it.niedermann.nextcloud.deck.exceptions.OfflineException; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; @@ -37,7 +40,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullCard>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullCard>> responder, Instant lastSync) { List<FullCard> result = new ArrayList<>(); if (stack.getCards() == null || stack.getCards().isEmpty()) { @@ -54,6 +57,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { responder.onError(throwable); @@ -70,7 +74,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { @Override public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullCard entity) { fixRelations(dataBaseAdapter, accountId, entity); - return dataBaseAdapter.createCard(accountId, entity.getCard()); + return dataBaseAdapter.createCardDirectly(accountId, entity.getCard()); } protected CardUpdate toCardUpdate(FullCard card) { @@ -144,10 +148,17 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } else { DeckLog.verbose("Comments - Version is too low, DONT SYNC"); } + syncHelper.doSyncFor(new OcsProjectDataProvider(this, existingEntity.getCard())); } @Override public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<FullCard> responder, FullCard entity) { + if (stack.getId() == null) { + responder.onError(new DeckException(DeckException.Hint.DEPENDENCY_NOT_SYNCED_YET, "Stack \"" + + stack.getStack().getTitle() + "\" for Card \"" + entity.getCard().getTitle() + + "\" is not synced yet. Perform a full sync (pull to refresh) as soon as you are online again.")); + return; + } entity.getCard().setStackId(stack.getId()); serverAdapter.createCard(board.getId(), stack.getId(), entity.getCard(), responder); } @@ -170,7 +181,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } @Override - public List<FullCard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + public List<FullCard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { if (board == null || stack == null) { // no cards changed! // (see call from StackDataProvider: goDeeperForUpSync called with null for board.) @@ -184,8 +195,13 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) { FullStack stack; Board board; + List<JoinCardWithLabel> changedLabels; + if (this.stack == null) { + changedLabels = dataBaseAdapter.getAllChangedLabelJoins(); + } else { + changedLabels = dataBaseAdapter.getAllChangedLabelJoinsForStack(this.stack.getLocalId()); + } - List<JoinCardWithLabel> changedLabels = dataBaseAdapter.getAllChangedJoins(); Account account = callback.getAccount(); for (JoinCardWithLabel changedLabelLocal : changedLabels) { Card card = dataBaseAdapter.getCardByLocalIdDirectly(account.getId(), changedLabelLocal.getCardId()); @@ -201,7 +217,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { board = this.board; } - JoinCardWithLabel changedLabel = dataBaseAdapter.getRemoteIdsForJoin(changedLabelLocal.getCardId(), changedLabelLocal.getLabelId()); + JoinCardWithLabel changedLabel = dataBaseAdapter.getAllChangedLabelJoinsWithRemoteIDs(changedLabelLocal.getCardId(), changedLabelLocal.getLabelId()); if (changedLabel.getStatusEnum() == DBStatus.LOCAL_DELETED) { if (changedLabel.getLabelId() == null || changedLabel.getCardId() == null) { dataBaseAdapter.deleteJoinedLabelForCardPhysicallyByRemoteIDs(account.getId(), changedLabel.getCardId(), changedLabel.getLabelId()); @@ -229,11 +245,22 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } } - List<JoinCardWithUser> deletedUsers = dataBaseAdapter.getAllDeletedUserJoinsWithRemoteIDs(); - for (JoinCardWithUser deletedUser : deletedUsers) { - Card card = dataBaseAdapter.getCardByRemoteIdDirectly(account.getId(), deletedUser.getCardId()); + + List<JoinCardWithUser> changedUsers; + if (this.stack == null) { + changedUsers = dataBaseAdapter.getAllChangedUserJoinsWithRemoteIDs(); + } else { + changedUsers = dataBaseAdapter.getAllChangedUserJoinsWithRemoteIDsForStack(this.stack.getLocalId()); + } + for (JoinCardWithUser changedUser : changedUsers) { + // not already known to server? + if (changedUser.getCardId() == null) { + //skip for now + continue; + } + Card card = dataBaseAdapter.getCardByRemoteIdDirectly(account.getId(), changedUser.getCardId()); if (this.stack == null) { - stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getLocalId()); + stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); } else { stack = this.stack; } @@ -243,16 +270,16 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } else { board = this.board; } - User user = dataBaseAdapter.getUserByLocalIdDirectly(deletedUser.getUserId()); - if (deletedUser.getStatusEnum() == DBStatus.LOCAL_DELETED) { - serverAdapter.unassignUserFromCard(board.getId(), stack.getId(), deletedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) { + User user = dataBaseAdapter.getUserByLocalIdDirectly(changedUser.getUserId()); + if (changedUser.getStatusEnum() == DBStatus.LOCAL_DELETED) { + serverAdapter.unassignUserFromCard(board.getId(), stack.getId(), changedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) { @Override public void onResponse(Void response) { - dataBaseAdapter.deleteJoinedUserForCardPhysicallyByRemoteIDs(account.getId(), deletedUser.getCardId(), user.getUid()); + dataBaseAdapter.deleteJoinedUserForCardPhysicallyByRemoteIDs(account.getId(), changedUser.getCardId(), user.getUid()); } }); - } else if (deletedUser.getStatusEnum() == DBStatus.LOCAL_EDITED) { - serverAdapter.assignUserToCard(board.getId(), stack.getId(), deletedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) { + } else if (changedUser.getStatusEnum() == DBStatus.LOCAL_EDITED) { + serverAdapter.assignUserToCard(board.getId(), stack.getId(), changedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) { @Override public void onResponse(Void response) { dataBaseAdapter.setStatusForJoinCardWithUser(card.getLocalId(), user.getLocalId(), DBStatus.UP_TO_DATE.getId()); @@ -261,7 +288,12 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } } - List<Attachment> attachments = dataBaseAdapter.getLocallyChangedAttachmentsDirectly(account.getId()); + List<Attachment> attachments; + if (this.stack == null) { + attachments = dataBaseAdapter.getLocallyChangedAttachmentsDirectly(account.getId()); + } else { + attachments = dataBaseAdapter.getLocallyChangedAttachmentsForStackDirectly(this.stack.getLocalId()); + } for (Attachment attachment : attachments) { FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(account.getId(), attachment.getCardId()); stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId()); @@ -269,7 +301,12 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { syncHelper.doUpSyncFor(new AttachmentDataProvider(this, board, stack.getStack(), card, Collections.singletonList(attachment))); } - List<Card> cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsDirectly(account.getId()); + List<Card> cardsWithChangedComments; + if (this.stack == null) { + cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsDirectly(account.getId()); + } else { + cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsForStackDirectly(this.stack.getLocalId()); + } for (Card card : cardsWithChangedComments) { syncHelper.doUpSyncFor(new DeckCommentsDataProvider(this, card)); } @@ -279,7 +316,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { @Override public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<FullCard> entitiesFromServer) { - List<FullCard> localCards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stack.getLocalId()); + List<FullCard> localCards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stack.getLocalId(), null); List<FullCard> delta = findDelta(entitiesFromServer, localCards); for (FullCard cardToDelete : delta) { if (cardToDelete.getId() == null) { @@ -294,6 +331,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { // do not delete, it's still there and was just moved! } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { if (!(throwable instanceof OfflineException)) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java index 5b9be120f..06ef3030d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java @@ -1,8 +1,8 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; @@ -24,10 +24,13 @@ public class DeckCommentsDataProvider extends AbstractSyncDataProvider<OcsCommen } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<OcsComment>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<OcsComment>> responder, Instant lastSync) { serverAdapter.getCommentsForRemoteCardId(card.getId(), new IResponseCallback<OcsComment>(responder.getAccount()) { @Override public void onResponse(OcsComment response) { + if (response == null) { + response = new OcsComment(); + } List<OcsComment> comments = response.split(); Collections.sort(comments, (o1, o2) -> o1.getSingle().getCreationDateTime().compareTo(o2.getSingle().getCreationDateTime())); verifyCommentListIntegrity(comments); @@ -131,7 +134,7 @@ public class DeckCommentsDataProvider extends AbstractSyncDataProvider<OcsCommen } @Override - public List<OcsComment> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + public List<OcsComment> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { return new OcsComment(dataBaseAdapter.getLocallyChangedCommentsByLocalCardIdDirectly(accountId, card.getLocalId())).split(); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java index 403d71f87..caa7c68e6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java @@ -1,8 +1,9 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; -import java.util.Date; +import java.time.Instant; import java.util.List; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.exceptions.HandledServerErrors; import it.niedermann.nextcloud.deck.model.Board; @@ -12,14 +13,14 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter public class LabelDataProvider extends AbstractSyncDataProvider<Label> { - private List<Label> labels; - private Board board; + private final List<Label> labels; + private final Board board; public LabelDataProvider(AbstractSyncDataProvider<?> parent, Board board, List<Label> labels) { super(parent); this.board = board; this.labels = labels; - if (this.labels!= null && board != null){ + if (this.labels != null && board != null) { for (Label label : labels) { label.setBoardId(board.getLocalId()); } @@ -27,7 +28,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Label>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Label>> responder, Instant lastSync) { responder.onResponse(labels); } @@ -44,7 +45,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { updateInDB(dataBaseAdapter, accountId, entity, false); return entity.getLocalId(); } else { - return dataBaseAdapter.createLabel(accountId, entity); + return dataBaseAdapter.createLabelDirectly(accountId, entity); } } @@ -53,7 +54,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { dataBaseAdapter.updateLabel(entity, setStatus); } - private IResponseCallback<Label> getLabelUniqueHandler(DataBaseAdapter dataBaseAdapter, Label entitiy, IResponseCallback<Label> responder){ + private IResponseCallback<Label> getLabelUniqueHandler(DataBaseAdapter dataBaseAdapter, Label entitiy, IResponseCallback<Label> responder) { return new IResponseCallback<Label>(responder.getAccount()) { @Override public void onResponse(Label response) { @@ -62,10 +63,13 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { @Override public void onError(Throwable throwable) { - if (HandledServerErrors.LABELS_TITLE_MUST_BE_UNIQUE == HandledServerErrors.fromThrowable(throwable)){ + if (HandledServerErrors.LABELS_TITLE_MUST_BE_UNIQUE == HandledServerErrors.fromThrowable(throwable)) { + DeckLog.log(throwable.getCause().getMessage() + ": " + entitiy.toString()); dataBaseAdapter.deleteLabelPhysically(entitiy); + responder.onResponse(entitiy); + } else { + responder.onError(throwable); } - responder.onError(throwable); } }; } @@ -92,7 +96,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { } @Override - public List<Label> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + public List<Label> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { return labels; } @@ -105,7 +109,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<Label> entitiesFromServer) { List<Label> deletedLabels = findDelta(labels, dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, board.getLocalId()).getLabels()); for (Label deletedLabel : deletedLabels) { - if (deletedLabel.getId()!=null){ + if (deletedLabel.getId() != null) { // preserve new, unsynced card. dataBaseAdapter.deleteLabelPhysically(deletedLabel); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java new file mode 100644 index 000000000..b9bfb8b31 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java @@ -0,0 +1,100 @@ +package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectList; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; + +public class OcsProjectDataProvider extends AbstractSyncDataProvider<OcsProject> { + private Card card; + + public OcsProjectDataProvider(AbstractSyncDataProvider<?> parent, Card card) { + super(parent); + this.card = card; + } + + @Override + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<OcsProject>> responder, Instant lastSync) { + serverAdapter.getProjectsForCard(card.getId(), new IResponseCallback<OcsProjectList>(responder.getAccount()) { + @Override + public void onResponse(OcsProjectList response) { + responder.onResponse(response.getProjects()); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + // dont break the sync! + DeckLog.logError(throwable); + responder.onResponse(Collections.emptyList()); + } + }); + } + + @Override + public OcsProject getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity) { + return dataBaseAdapter.getProjectByRemoteIdDirectly(accountId, entity.getId()); + } + + @Override + public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity) { + Long newId = dataBaseAdapter.createProjectDirectly(accountId, entity); + entity.setLocalId(newId); + updateResources(dataBaseAdapter, accountId, entity); + return newId; + } + + @Override + public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity, boolean setStatus) { + dataBaseAdapter.updateProjectDirectly(accountId, entity); + dataBaseAdapter.deleteProjectResourcesForProjectIdDirectly(entity.getLocalId()); + updateResources(dataBaseAdapter, accountId, entity); + } + + @Override + public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject ocsProject) { + if (ocsProject != null && ocsProject.getLocalId() != null) { + dataBaseAdapter.deleteProjectDirectly(ocsProject); + } + } + + private void updateResources(DataBaseAdapter dataBaseAdapter, Long accountId, OcsProject entity) { + if (entity.getResources() != null) { + for (OcsProjectResource resource : entity.getResources()) { + resource.setProjectId(entity.getLocalId()); + resource.setLocalId(dataBaseAdapter.createProjectResourceDirectly(accountId, resource)); + if ("deck-card".equals(resource.getType())) { + dataBaseAdapter.assignCardToProjectIfMissng(accountId, entity.getLocalId(), resource.getId()); + } + } + } + } + + @Override + public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<OcsProject> responder, OcsProject entity) { + // Do Nothing + } + + @Override + public void updateOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<OcsProject> callback, OcsProject entity) { + // Do Nothing + } + + @Override + public void deleteOnServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<Void> callback, OcsProject entity, DataBaseAdapter dataBaseAdapter) { + // Do Nothing + } + + @Override + public List<OcsProject> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { + return Collections.emptyList(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java index a3c123afd..93761d616 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java @@ -1,12 +1,13 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import java.time.Instant; import java.util.Collections; -import java.util.Date; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.exceptions.DeckException; import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.full.FullBoard; @@ -19,13 +20,15 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { private FullBoard board; + private Set<Long> syncedStacks = new ConcurrentSkipListSet<>(); + public StackDataProvider(AbstractSyncDataProvider<?> parent, FullBoard board) { super(parent); this.board = board; } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullStack>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullStack>> responder, Instant lastSync) { serverAdapter.getStacks(board.getId(), responder); } @@ -54,12 +57,12 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { @Override public void goDeeper(SyncHelper syncHelper, FullStack existingEntity, FullStack entityFromServer, IResponseCallback<Boolean> callback) { - boolean serverHasCards = entityFromServer.getCards() != null && !entityFromServer.getCards().isEmpty(); - boolean weHaveCards = existingEntity.getCards() != null && !existingEntity.getCards().isEmpty(); - if (serverHasCards || weHaveCards){ + boolean serverHasCards = entityFromServer.getCards() != null && !entityFromServer.getCards().isEmpty(); + boolean weHaveCards = existingEntity.getCards() != null && !existingEntity.getCards().isEmpty(); + if (serverHasCards || weHaveCards) { existingEntity.setCards(entityFromServer.getCards()); List<Card> cards = existingEntity.getCards(); - if (cards != null ){ + if (cards != null) { for (Card card : cards) { card.setStackId(existingEntity.getLocalId()); } @@ -72,6 +75,9 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { @Override public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<FullStack> responder, FullStack entity) { + if (board.getId() == null) { + throw new DeckException(DeckException.Hint.DEPENDENCY_NOT_SYNCED_YET, "Board for this stack is not synced yet. Perform a full sync (pull to referesh) as soon as you are online again."); + } entity.getStack().setBoardId(board.getId()); serverAdapter.createStack(board.getBoard(), entity.getStack(), responder); } @@ -89,8 +95,8 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { } @Override - public List<FullStack> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { - if (board == null){ + public List<FullStack> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { + if (board == null) { // no stacks changed! // (see call from BoardDataProvider: goDeeperForUpSync called with null for board.) // so we can just skip this one and proceed with cards. @@ -103,16 +109,19 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { @Override public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) { List<FullCard> changedCards = dataBaseAdapter.getLocallyChangedCardsDirectly(callback.getAccount().getId()); - Set<Long> syncedStacks = new HashSet<>(); - if (changedCards != null && changedCards.size() > 0){ + if (changedCards != null && !changedCards.isEmpty()) { for (FullCard changedCard : changedCards) { long stackId = changedCard.getCard().getStackId(); - boolean added = syncedStacks.add(stackId); - if (added) { + boolean alreadySynced = syncedStacks.contains(stackId); + if (!alreadySynced) { FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackId); - Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - changedCard.getCard().setStackId(stack.getId()); - syncHelper.doUpSyncFor(new CardDataProvider(this, board, stack)); + // already synced and known to server? + if (stack.getStack().getId() != null) { + syncedStacks.add(stackId); + Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); + changedCard.getCard().setStackId(stack.getId()); + syncHelper.doUpSyncFor(new CardDataProvider(this, board, stack)); + } } } } else { @@ -132,7 +141,7 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { List<FullStack> localStacks = dataBaseAdapter.getFullStacksForBoardDirectly(accountId, board.getLocalId()); List<FullStack> delta = findDelta(entitiesFromServer, localStacks); for (FullStack stackToDelete : delta) { - if (stackToDelete.getId() == null){ + if (stackToDelete.getId() == null) { // not pushed up yet so: continue; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java index 60c906bda..279ce9e55 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java @@ -1,6 +1,6 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; -import java.util.Date; +import java.time.Instant; import java.util.List; import it.niedermann.nextcloud.deck.api.IResponseCallback; @@ -27,7 +27,7 @@ public class UserDataProvider extends AbstractSyncDataProvider<User> { } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<User>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<User>> responder, Instant lastSync) { responder.onResponse(users); } @@ -67,7 +67,7 @@ public class UserDataProvider extends AbstractSyncDataProvider<User> { } @Override - public List<User> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + public List<User> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { return null; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWitAclDownSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithAclDownSyncDataProvider.java index 326d257ab..8516f0fc0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWitAclDownSyncDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithAclDownSyncDataProvider.java @@ -11,7 +11,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AccessControlDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.BoardDataProvider; -public class BoardWitAclDownSyncDataProvider extends BoardDataProvider { +public class BoardWithAclDownSyncDataProvider extends BoardDataProvider { @Override public void goDeeper(SyncHelper syncHelper, FullBoard existingEntity, FullBoard entityFromServer, IResponseCallback<Boolean> callback) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java new file mode 100644 index 000000000..5f2c79ad3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java @@ -0,0 +1,37 @@ +package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; + +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.BoardDataProvider; + +public class BoardWithStacksAndLabelsUpSyncDataProvider extends BoardDataProvider { + + private FullBoard board; + + public BoardWithStacksAndLabelsUpSyncDataProvider(FullBoard boardToSync) { + board = boardToSync; + } + + @Override + public List<FullBoard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) { + return Collections.singletonList(board); + } + + @Override + public void goDeeper(SyncHelper syncHelper, FullBoard existingEntity, FullBoard entityFromServer, IResponseCallback<Boolean> callback) { + // do nothing! + + } + + @Override + public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<FullBoard> entitiesFromServer) { + // do nothing! + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java new file mode 100644 index 000000000..faabda163 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.persistence.sync.helpers.util; + +import java.util.concurrent.CountDownLatch; + +import it.niedermann.nextcloud.deck.DeckLog; + +public class AsyncUtil { + public interface LatchCallback { + void doWork(CountDownLatch latch); + } + + public static void awaitAsyncWork(int count, LatchCallback worker){ + CountDownLatch countDownLatch = new CountDownLatch(count); + worker.doWork(countDownLatch); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + DeckLog.logError(e); + } + } +} |