diff options
author | Stefan Niedermann <info@niedermann.it> | 2020-11-25 21:49:12 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2020-11-25 21:49:12 +0300 |
commit | 8110ebb0724c7e414223be651e9dcf6a95bcde4b (patch) | |
tree | d7bee0e0538f309aaebcd3a77bafbca8e98157a1 /app/src/main/java/it/niedermann/nextcloud/deck | |
parent | 5411d277ee02a00d8c393967880ad1d1bbe70fc4 (diff) | |
parent | d4f8c50e0b8609481aa43614a2715477724087b1 (diff) |
Merge branch 'master' into webview-markdown
Diffstat (limited to 'app/src/main/java/it/niedermann/nextcloud/deck')
30 files changed, 378 insertions, 198 deletions
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java index a303a8a33..1f0148b91 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java @@ -1,6 +1,8 @@ package it.niedermann.nextcloud.deck.api; +import com.nextcloud.android.sso.api.ParsedResponse; + import java.util.List; import io.reactivex.Observable; @@ -33,6 +35,7 @@ import retrofit2.http.Query; public interface DeckAPI { String MODIFIED_SINCE_HEADER = "If-Modified-Since"; + String IF_NONE_MATCH = "If-None-Match"; // ### BOARDS @POST("boards") @@ -51,7 +54,7 @@ public interface DeckAPI { Observable<FullBoard> restoreBoard(@Path("id") long id); @GET("boards") - Observable<List<FullBoard>> getBoards(@Query ("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync ); + Observable<ParsedResponse<List<FullBoard>>> getBoards(@Query ("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync, @Header(IF_NONE_MATCH) String eTag); // ### Stacks diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java index 0e523c318..be3eaeb22 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java @@ -24,12 +24,18 @@ public abstract class IResponseCallback<T> { DeckLog.logError(throwable); } + @CallSuper + public void onError(Throwable throwable, T locallyCreatedEntity) { + onError(throwable); + } + public static <T> IResponseCallback<T> getDefaultResponseCallback(Account account) { return new IResponseCallback<T>(account) { @Override public void onResponse(T response) { // Do Nothing } + }; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java index 8601cab41..e759f27d4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java @@ -284,6 +284,7 @@ public class JsonToEntityParser { makeTraceableIfFails(() -> { board.setTitle(getNullAsEmptyString(e.get("title"))); board.setColor(getNullAsEmptyString(e.get("color"))); + board.setEtag(getNullAsNull(e.get("ETag"))); board.setArchived(e.get("archived").getAsBoolean()); board.setLastModified(getTimestampFromLong(e.get("lastModified"))); @@ -396,6 +397,7 @@ public class JsonToEntityParser { card.setDescription(getNullAsEmptyString(e.get("description"))); card.setStackId(e.get("stackId").getAsLong()); card.setType(getNullAsEmptyString(e.get("type"))); + card.setEtag(getNullAsNull(e.get("ETag"))); card.setLastModified(getTimestampFromLong(e.get("lastModified"))); card.setCreatedAt(getTimestampFromLong(e.get("createdAt"))); card.setDeletedAt(getTimestampFromLong(e.get("deletedAt"))); @@ -456,6 +458,7 @@ public class JsonToEntityParser { a.setId(e.get("id").getAsLong()); a.setCardId(e.get("cardId").getAsLong()); a.setType(e.get("type").getAsString()); + a.setEtag(getNullAsNull(e.get("ETag"))); a.setData(e.get("data").getAsString()); a.setLastModified(getTimestampFromLong(e.get("lastModified"))); a.setCreatedAt(getTimestampFromLong(e.get("createdAt"))); @@ -571,33 +574,6 @@ public class JsonToEntityParser { return Color.GRAY; } - protected static List<Activity> parseActivity(JsonObject e) { - DeckLog.verbose(e.toString()); - List<Activity> activityList = new ArrayList<>(); - - makeTraceableIfFails(() -> { - if (e.has("ocs")) { - JsonObject ocs = e.getAsJsonObject("ocs"); - if (ocs.has("data")) { - JsonArray data = ocs.getAsJsonArray("data"); - for (JsonElement activityJson : data) { - Activity activity = new Activity(); - JsonObject activityObject = activityJson.getAsJsonObject(); - - activity.setId(activityObject.get("activity_id").getAsLong()); - activity.setType(ActivityType.findByPath(getNullAsEmptyString(activityObject.get("icon"))).getId()); - activity.setSubject(getNullAsEmptyString(activityObject.get("subject"))); - activity.setCardId(activityObject.get("object_id").getAsLong()); - activity.setLastModified(getTimestampFromString(activityObject.get("datetime"))); - - activityList.add(activity); - } - } - } - }, e); - return activityList; - } - protected static FullStack parseStack(JsonObject e) { DeckLog.verbose(e.toString()); FullStack fullStack = new FullStack(); @@ -607,6 +583,7 @@ public class JsonToEntityParser { stack.setTitle(getNullAsEmptyString(e.get("title"))); stack.setBoardId(e.get("boardId").getAsLong()); stack.setId(e.get("id").getAsLong()); + stack.setEtag(getNullAsNull(e.get("ETag"))); stack.setLastModified(getTimestampFromLong(e.get("lastModified"))); stack.setDeletedAt(getTimestampFromLong(e.get("deletedAt"))); if (e.has("order") && !e.get("order").isJsonNull()) { @@ -627,6 +604,34 @@ public class JsonToEntityParser { return fullStack; } + protected static List<Activity> parseActivity(JsonObject e) { + DeckLog.verbose(e.toString()); + List<Activity> activityList = new ArrayList<>(); + + makeTraceableIfFails(() -> { + if (e.has("ocs")) { + JsonObject ocs = e.getAsJsonObject("ocs"); + if (ocs.has("data")) { + JsonArray data = ocs.getAsJsonArray("data"); + for (JsonElement activityJson : data) { + Activity activity = new Activity(); + JsonObject activityObject = activityJson.getAsJsonObject(); + + activity.setId(activityObject.get("activity_id").getAsLong()); + activity.setType(ActivityType.findByPath(getNullAsEmptyString(activityObject.get("icon"))).getId()); + activity.setSubject(getNullAsEmptyString(activityObject.get("subject"))); + activity.setCardId(activityObject.get("object_id").getAsLong()); + activity.setEtag(getNullAsNull(e.get("ETag"))); + activity.setLastModified(getTimestampFromString(activityObject.get("datetime"))); + + activityList.add(activity); + } + } + } + }, e); + return activityList; + } + protected static Label parseLabel(JsonObject e) { DeckLog.verbose(e.toString()); Label label = new Label(); @@ -635,13 +640,18 @@ public class JsonToEntityParser { //todo: last modified! // label.setLastModified(get); label.setTitle(getNullAsEmptyString(e.get("title"))); + label.setEtag(getNullAsNull(e.get("ETag"))); label.setColor(getColorAsInt(e, "color")); }, e); return label; } private static String getNullAsEmptyString(JsonElement jsonElement) { - return jsonElement.isJsonNull() ? "" : jsonElement.getAsString(); + return jsonElement == null || jsonElement.isJsonNull() ? "" : jsonElement.getAsString(); + } + + private static String getNullAsNull(JsonElement jsonElement) { + return jsonElement == null || jsonElement.isJsonNull() ? null : jsonElement.getAsString(); } private static Instant getTimestampFromString(JsonElement jsonElement) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java index d9d8c5dde..0512f8a60 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java @@ -5,7 +5,8 @@ public class DeckException extends IllegalArgumentException { public enum Hint { CAPABILITIES_NOT_PARSABLE, CAPABILITIES_VERSION_NOT_PARSABLE, - UNKNOWN_ACCOUNT_USER_ID + UNKNOWN_ACCOUNT_USER_ID, + DEPENDENCY_NOT_SYNCED_YET } private Hint hint; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java index 7acfb60bd..8d436477d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java @@ -11,6 +11,8 @@ public class TraceableException extends RuntimeException { public static void makeTraceableIfFails(Runnable runnable, Object... args) { try { runnable.run(); + } catch (TraceableException t) { + throw t; } catch (Throwable t) { final StringBuilder message = new StringBuilder("Sorry, a wild error appeared!\n\n" + "⚠️ If you want to tell us about the following issue, " + diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java index 1c189d162..8abcbced7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java @@ -54,6 +54,7 @@ public class Account implements Serializable { private boolean maintenanceEnabled = false; private String etag; + private String boardsEtag; @Ignore public Account(Long id, @NonNull String name, @NonNull String userName, @NonNull String url) { @@ -190,6 +191,14 @@ public class Account implements Serializable { this.etag = etag; } + public String getBoardsEtag() { + return boardsEtag; + } + + public void setBoardsEtag(String boardsEtag) { + this.boardsEtag = boardsEtag; + } + /** * A cache buster parameter is added for duplicate account names on different hosts which shall be fetched from the same {@link SingleSignOnAccount} (e. g. {@link AccountSwitcherDialog}) * diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java index f88594e7f..27dcd1df8 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java @@ -37,8 +37,8 @@ public class Board extends AbstractRemoteEntity implements Serializable { } @Ignore - public Board(String title, String color) { - this.title = title; + public Board(String title, @ColorInt int color) { + setTitle(title); setColor(color); } @@ -98,7 +98,7 @@ public class Board extends AbstractRemoteEntity implements Serializable { } } - public void setColor(Integer color) { + public void setColor(@ColorInt Integer color) { this.color = color; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java index 54cd0fc31..2f7771383 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java @@ -39,6 +39,8 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity { protected Instant lastModified; protected Instant lastModifiedLocal; + protected String etag; + public AbstractRemoteEntity() { } @@ -137,6 +139,15 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity { this.status = status.getId(); } + @Override + public String getEtag() { + return etag; + } + + @Override + public void setEtag(String etag) { + this.etag = etag; + } @Override public boolean equals(Object o) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java index f50bf0b90..f197c0d30 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java @@ -68,6 +68,14 @@ public interface IRemoteEntity { getEntity().setStatusEnum(status); } + default String getEtag() { + return getEntity().getEtag(); + } + + default void setEtag(String etag) { + getEntity().setEtag(etag); + } + default <T> List<T> copyList(List<T> listToCopy) { if (listToCopy == null) { return null; 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 0a7f213ef..c86f65340 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 @@ -527,8 +527,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()); @@ -547,6 +547,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); + } }); } }); @@ -907,7 +913,18 @@ public class SyncManager { 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; } @@ -1036,8 +1053,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()); @@ -1074,11 +1091,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; 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 a0b9fd8aa..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 @@ -125,9 +125,9 @@ public class ServerAdapter { // return lastSyncHeader; } - public void getBoards(IResponseCallback<List<FullBoard>> responseCallback) { + public void getBoards(IResponseCallback<ParsedResponse<List<FullBoard>>> responseCallback) { RequestHelper.request(provider, () -> - provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId())), + provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId()), responseCallback.getAccount().getBoardsEtag()), responseCallback); } 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 0ac0f786b..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 @@ -97,7 +97,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.Sta UserInBoard.class, }, exportSchema = false, - version = 22 + version = 23 ) @TypeConverters({DateTypeConverter.class}) public abstract class DeckDatabase extends RoomDatabase { @@ -375,6 +375,25 @@ public abstract class DeckDatabase extends RoomDatabase { } }; + 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) { @@ -434,6 +453,7 @@ public abstract class DeckDatabase extends RoomDatabase { lastSyncPref.apply(); } }) + .addMigrations(MIGRATION_22_23) .fallbackToDestructiveMigration() .addCallback(ON_CREATE_CALLBACK) .build(); 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/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 032c3594a..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,5 +1,11 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers; +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; @@ -29,7 +35,7 @@ public class SyncHelper { } // 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, dataBaseAdapter, accountId, new IResponseCallback<List<T>>(account) { @Override @@ -50,12 +56,13 @@ public class SyncHelper { } else { //TODO: how to handle deletes? what about archived? if (existingEntity.getStatus() != DBStatus.UP_TO_DATE.getId()) { - DeckLog.log("Conflicting changes on entity: " + existingEntity); + 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); } } @@ -73,20 +80,29 @@ 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) { @@ -110,7 +126,7 @@ public class SyncHelper { } } - 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) { @@ -126,7 +142,7 @@ 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) { @@ -152,13 +168,13 @@ public class SyncHelper { }; } - 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?!"); } @@ -167,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/BoardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java index 5b458471f..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,5 +1,9 @@ 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; @@ -26,8 +30,24 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullBoard>> responder, Instant 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 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 15b8b2b74..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,5 +1,7 @@ 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; @@ -7,6 +9,7 @@ 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; @@ -54,6 +57,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { responder.onError(throwable); @@ -149,10 +153,14 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { @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()); -// if (board != null && stack != null && board.getId() != null && stack.getId() != null) { serverAdapter.createCard(board.getId(), stack.getId(), entity.getCard(), responder); -// } else DeckLog.error("Skipped card creation due to missing remote ID"); } @Override @@ -323,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/StackDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java index 6a8e1f227..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 @@ -7,6 +7,7 @@ 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; @@ -74,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); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java index 91c08ffad..5056985fa 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java @@ -470,21 +470,23 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener } @Override - public void onCreateBoard(String title, String color) { + public void onCreateBoard(String title, @ColorInt int color) { if (boardsLiveData == null || boardsLiveDataObserver == null) { throw new IllegalStateException("Cannot create board when noone observe boards yet. boardsLiveData or observer is null."); } boardsLiveData.removeObserver(boardsLiveDataObserver); - final Board boardToCreate = new Board(title, color.startsWith("#") ? color.substring(1) : color); + final Board boardToCreate = new Board(title, color); boardToCreate.setPermissionEdit(true); boardToCreate.setPermissionManage(true); - observeOnce(syncManager.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate), this, createdBoard -> { - if (createdBoard == null) { - BrandedSnackbar.make(binding.coordinatorLayout, "Open Deck in web interface first!", Snackbar.LENGTH_LONG) - // TODO implement action! - // .setAction(R.string.simple_open, v -> ExceptionDialogFragment.newInstance(throwable).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) + + final WrappedLiveData<FullBoard> createLiveData = syncManager.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate); + observeOnce(createLiveData, this, (createdBoard) -> { + if (createLiveData.hasError()) { + BrandedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG) + .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(createLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) .show(); - } else { + } + if (createdBoard != null && !createLiveData.hasError()) { boardsList.add(createdBoard.getBoard()); setCurrentBoard(createdBoard.getBoard()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java index 67b2c00be..30f1e5d49 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java @@ -6,6 +6,7 @@ import android.view.MenuItem; import android.view.View; import androidx.appcompat.widget.PopupMenu; +import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; @@ -30,14 +31,13 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder { void bind(boolean isSupportedVersion, Board board, FragmentManager fragmentManager, Consumer<Board> dearchiveBoardListener) { final Context context = itemView.getContext(); - binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, String.format("#%06X", (0xFFFFFF & board.getColor())))); + binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, board.getColor())); binding.boardMenu.setVisibility(View.GONE); binding.boardTitle.setText(board.getTitle()); if (isSupportedVersion) { if (board.isPermissionManage()) { binding.boardMenu.setVisibility(View.VISIBLE); - binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, R.color.grey600)); - + binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600))); binding.boardMenu.setOnClickListener((v) -> { PopupMenu popup = new PopupMenu(context, binding.boardMenu); popup.getMenuInflater().inflate(R.menu.archived_board_menu, popup.getMenu()); @@ -47,28 +47,27 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder { } popup.setOnMenuItemClickListener((MenuItem item) -> { final String editBoard = context.getString(R.string.edit_board); - switch (item.getItemId()) { - case SHARE_BOARD_ID: - AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName()); - return true; - case R.id.edit_board: - EditBoardDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, editBoard); - return true; - case R.id.dearchive_board: - dearchiveBoardListener.accept(board); - return true; - case R.id.delete_board: - DeleteBoardDialogFragment.newInstance(board).show(fragmentManager, DeleteBoardDialogFragment.class.getSimpleName()); - return true; - default: - return false; + int itemId = item.getItemId(); + if (itemId == SHARE_BOARD_ID) { + AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName()); + return true; + } else if (itemId == R.id.edit_board) { + EditBoardDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, editBoard); + return true; + } else if (itemId == R.id.dearchive_board) { + dearchiveBoardListener.accept(board); + return true; + } else if (itemId == R.id.delete_board) { + DeleteBoardDialogFragment.newInstance(board).show(fragmentManager, DeleteBoardDialogFragment.class.getSimpleName()); + return true; } + return false; }); popup.show(); }); } else if (board.isPermissionShare()) { binding.boardMenu.setVisibility(View.VISIBLE); - binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, R.color.grey600)); + binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, ContextCompat.getColor(context, R.color.grey600))); binding.boardMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName())); } binding.boardMenu.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java index 5975e0306..c9501ffa9 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java @@ -37,7 +37,7 @@ public class BoardAdapter extends ArrayAdapter<Board> { TextView boardName = convertView.findViewById(R.id.boardName); if (board != null) { boardName.setText(board.getTitle()); - boardName.setCompoundDrawables(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, String.format("#%06X", (0xFFFFFF & board.getColor()))), null, null, null); + boardName.setCompoundDrawables(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, board.getColor()), null, null, null); } else { DeckLog.logError(new IllegalArgumentException("board at position " + position + "is null")); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java index e197727d5..1ee23abfa 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java @@ -56,7 +56,7 @@ public class EditBoardDialogFragment extends BrandedDialogFragment { dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> { this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor()); this.fullBoard.board.setTitle(binding.input.getText().toString()); - editBoardListener.onUpdateBoard(fullBoard); + this.editBoardListener.onUpdateBoard(fullBoard); }); final MainViewModel viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); new SyncManager(requireActivity()).getFullBoardById(viewModel.getCurrentAccount().getId(), args.getLong(KEY_BOARD_ID)).observe(EditBoardDialogFragment.this, (FullBoard fb) -> { @@ -65,13 +65,13 @@ public class EditBoardDialogFragment extends BrandedDialogFragment { String title = this.fullBoard.getBoard().getTitle(); binding.input.setText(title); binding.input.setSelection(title.length()); - binding.colorChooser.selectColor(String.format("#%06X", (0xFFFFFF & fullBoard.getBoard().getColor()))); + binding.colorChooser.selectColor(fullBoard.getBoard().getColor()); } }); } else { dialogBuilder.setTitle(R.string.add_board); dialogBuilder.setPositiveButton(R.string.simple_add, (dialog, which) -> editBoardListener.onCreateBoard(binding.input.getText().toString(), binding.colorChooser.getSelectedColor())); - binding.colorChooser.selectColor(String.format("#%06X", 0xFFFFFF & ContextCompat.getColor(requireContext(), R.color.board_default_color))); + binding.colorChooser.selectColor(ContextCompat.getColor(requireContext(), R.color.board_default_color)); } return dialogBuilder diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java index ee9ba9b9d..9d8fcdbde 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java @@ -1,11 +1,13 @@ package it.niedermann.nextcloud.deck.ui.board; +import androidx.annotation.ColorInt; + import it.niedermann.nextcloud.deck.model.full.FullBoard; public interface EditBoardListener { void onUpdateBoard(FullBoard fullBoard); - default void onCreateBoard(String title, String color) { + default void onCreateBoard(String title, @ColorInt int color) { // Creating board is not necessary } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java index a0fb4cadd..2dacfe6ac 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java @@ -65,7 +65,7 @@ public class EditLabelDialogFragment extends BrandedDialogFragment { String title = this.label.getTitle(); binding.input.setText(title); binding.input.setSelection(title.length()); - binding.colorChooser.selectColor(String.format("#%06X", (0xFFFFFF & this.label.getColor()))); + binding.colorChooser.selectColor(this.label.getColor()); return dialogBuilder .setView(binding.getRoot()) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java index 558a22db4..c27fa04ee 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java @@ -37,7 +37,7 @@ public class BoardAdapter extends AbstractAdapter<Board> { final Board item = getItem(position); if (item != null) { binding.boardTitle.setText(item.getTitle()); - binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, String.format("#%06X", (0xFFFFFF & item.getColor())))); + binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, item.getColor())); } else { DeckLog.logError(new IllegalArgumentException("No item for position " + position)); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java index c82f47c9c..0dd431ff9 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java @@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.ui.view; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Color; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -9,11 +10,15 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; +import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import com.google.android.flexbox.FlexboxLayout; import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener; +import java.util.Arrays; + import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.WidgetColorChooserBinding; @@ -24,10 +29,12 @@ public class ColorChooser extends LinearLayout { private final WidgetColorChooserBinding binding; private final Context context; - private final String[] colors; + private final int[] colors; - private String selectedColor; - private String previouslySelectedColor; + @ColorInt + private int selectedColor; + @ColorInt + private int previouslySelectedColor; @Nullable private ImageView previouslySelectedImageView; @@ -42,14 +49,15 @@ public class ColorChooser extends LinearLayout { params.setMargins(0, DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1x), 0, 0); params.setFlexBasisPercent(.15f); - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.ColorChooser, 0, 0); - colors = getResources().getStringArray(a.getResourceId(R.styleable.ColorChooser_colors, 0)); + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorChooser, 0, 0); + colors = Arrays.stream(getResources().getStringArray(a.getResourceId(R.styleable.ColorChooser_colors, 0))) + .mapToInt(Color::parseColor) + .toArray(); a.recycle(); binding = WidgetColorChooserBinding.inflate(LayoutInflater.from(context), this, true); - for (final String color : colors) { - ImageView image = new ImageView(getContext()); + for (final int color : colors) { + final ImageView image = new ImageView(getContext()); image.setLayoutParams(params); image.setOnClickListener((imageView) -> { if (previouslySelectedImageView != null) { // null when first selection @@ -59,7 +67,7 @@ public class ColorChooser extends LinearLayout { selectedColor = color; this.previouslySelectedColor = color; this.previouslySelectedImageView = image; - binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, R.color.board_default_custom_color)); + binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color))); binding.customColorPicker.setVisibility(View.GONE); binding.brightnessSlide.setVisibility(View.GONE); }); @@ -82,19 +90,20 @@ public class ColorChooser extends LinearLayout { previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor)); previouslySelectedImageView = null; } - String customColor = "#" + envelope.getHexCode().substring(2); + @ColorInt + final int customColor = envelope.getColor(); selectedColor = customColor; previouslySelectedColor = customColor; binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.circle_alpha_colorize_36dp, selectedColor)); }); } - public void selectColor(String newColor) { + public void selectColor(@ColorInt int newColor) { boolean newColorIsCustomColor = true; selectedColor = newColor; for (int i = 0; i < colors.length; i++) { - if (colors[i].equals(newColor)) { - binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, R.color.board_default_custom_color)); + if (colors[i] == newColor) { + binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color))); binding.colorPicker.getChildAt(i).performClick(); newColorIsCustomColor = false; break; @@ -105,7 +114,8 @@ public class ColorChooser extends LinearLayout { } } - public String getSelectedColor() { + @ColorInt + public int getSelectedColor() { return this.selectedColor; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java index 6b86b50ba..771feabc2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java @@ -7,6 +7,7 @@ import android.os.Bundle; import androidx.appcompat.app.ActionBar; import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.ui.PickStackActivity; @@ -18,7 +19,7 @@ public class StackWidgetConfigurationActivity extends PickStackActivity { super.onCreate(savedInstanceState); final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { - actionBar.setTitle("Add stack widget"); + actionBar.setTitle(R.string.add_stack_widget); } setResult(RESULT_CANCELED); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java index 1f6490bf2..1a88bec97 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java @@ -7,6 +7,7 @@ import android.view.SubMenu; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatImageButton; import androidx.appcompat.widget.PopupMenu; +import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import java.util.List; @@ -38,12 +39,12 @@ public class DrawerMenuUtil { SubMenu boardsMenu = menu.addSubMenu(R.string.simple_boards); int index = 0; for (Board board : boards) { - MenuItem m = boardsMenu.add(Menu.NONE, index++, Menu.NONE, board.getTitle()).setIcon(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, String.format("#%06X", (0xFFFFFF & board.getColor())))); + MenuItem m = boardsMenu.add(Menu.NONE, index++, Menu.NONE, board.getTitle()).setIcon(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, board.getColor())); if (currentServerVersionIsSupported) { if (board.isPermissionManage()) { AppCompatImageButton contextMenu = new AppCompatImageButton(context); contextMenu.setBackgroundDrawable(null); - contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, R.color.grey600)); + contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600))); contextMenu.setOnClickListener((v) -> { PopupMenu popup = new PopupMenu(context, contextMenu); popup.getMenuInflater().inflate(R.menu.navigation_context_menu, popup.getMenu()); @@ -53,28 +54,27 @@ public class DrawerMenuUtil { } popup.setOnMenuItemClickListener((MenuItem item) -> { final String editBoard = context.getString(R.string.edit_board); - switch (item.getItemId()) { - case SHARE_BOARD_ID: - AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName()); - return true; - case R.id.edit_board: - EditBoardDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard); - return true; - case R.id.manage_labels: - ManageLabelsDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard); - return true; - case R.id.clone_board: - context.onClone(board); - return true; - case R.id.archive_board: - context.onArchive(board); - return true; - case R.id.delete_board: - DeleteBoardDialogFragment.newInstance(board).show(context.getSupportFragmentManager(), DeleteBoardDialogFragment.class.getCanonicalName()); - return true; - default: - return false; + int itemId = item.getItemId(); + if (itemId == SHARE_BOARD_ID) { + AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName()); + return true; + } else if (itemId == R.id.edit_board) { + EditBoardDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard); + return true; + } else if (itemId == R.id.manage_labels) { + ManageLabelsDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard); + return true; + } else if (itemId == R.id.clone_board) { + context.onClone(board); + return true; + } else if (itemId == R.id.archive_board) { + context.onArchive(board); + return true; + } else if (itemId == R.id.delete_board) { + DeleteBoardDialogFragment.newInstance(board).show(context.getSupportFragmentManager(), DeleteBoardDialogFragment.class.getCanonicalName()); + return true; } + return false; }); popup.show(); }); @@ -82,7 +82,7 @@ public class DrawerMenuUtil { } else if (board.isPermissionShare()) { AppCompatImageButton contextMenu = new AppCompatImageButton(context); contextMenu.setBackgroundDrawable(null); - contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, R.color.grey600)); + contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, ContextCompat.getColor(context, R.color.grey600))); contextMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName())); m.setActionView(contextMenu); } @@ -90,7 +90,7 @@ public class DrawerMenuUtil { } if (hasArchivedBoards) { - boardsMenu.add(Menu.NONE, MENU_ID_ARCHIVED_BOARDS, Menu.NONE, R.string.archived_boards).setIcon(ViewUtil.getTintedImageView(context, R.drawable.ic_archive_white_24dp, R.color.grey600)); + boardsMenu.add(Menu.NONE, MENU_ID_ARCHIVED_BOARDS, Menu.NONE, R.string.archived_boards).setIcon(ViewUtil.getTintedImageView(context, R.drawable.ic_archive_white_24dp, ContextCompat.getColor(context, R.color.grey600))); } if (currentServerVersionIsSupported) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java index f43f7d824..58c465290 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java @@ -33,7 +33,7 @@ public class ProjectUtil { /** * extracts the values of board- and card-ID from url. - * Depending on what kind of url it gets, it will return a long[] of lenght 1 or 2: + * Depending on what kind of url it gets, it will return a long[] of length 1 or 2: * If the url contains both values, you'll get 2, if it contains only the board, you'll get 1. * <p> * The order is fixed here: [boardId, cardId] @@ -50,7 +50,7 @@ public class ProjectUtil { throw new IllegalArgumentException("trimmed url is empty"); } // extract important part - String[] splitByPrefix = url.split(".*index\\.php/apps/deck(/#)?/board/"); + String[] splitByPrefix = url.split(".*(index\\.php/)?apps/deck(/#)?/board/"); // split into board- and card part if (splitByPrefix.length < 2) { throw new IllegalArgumentException("This URL doesn't seem to be an URL containing the boardId: \"" + url + "\""); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java index ffc06e33d..7319f7793 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java @@ -3,7 +3,6 @@ package it.niedermann.nextcloud.deck.util; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Bitmap; -import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.Spannable; @@ -12,6 +11,7 @@ import android.text.style.ImageSpan; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; @@ -81,18 +81,14 @@ public final class ViewUtil { TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(ContextCompat.getColor(context, textColor))); } - public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @NonNull String color) { + public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @ColorInt int color) { final Drawable drawable = ContextCompat.getDrawable(context, imageId); assert drawable != null; final Drawable wrapped = DrawableCompat.wrap(drawable).mutate(); - DrawableCompat.setTint(wrapped, Color.parseColor(color)); + DrawableCompat.setTint(wrapped, color); return drawable; } - public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, int colorId) { - return getTintedImageView(context, imageId, context.getResources().getString(colorId)); - } - /** * Replaces all mentions in the textView with an avatar and the display name * |