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 | |
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')
222 files changed, 9426 insertions, 2898 deletions
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java index ff492c226..cf30a0971 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java @@ -1,20 +1,17 @@ package it.niedermann.nextcloud.deck; -import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.multidex.MultiDexApplication; import androidx.preference.PreferenceManager; -import com.jakewharton.threetenabp.AndroidThreeTen; - import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode; -import static androidx.multidex.MultiDex.install; -public class DeckApplication extends Application { +public class DeckApplication extends MultiDexApplication { public static final long NO_ACCOUNT_ID = -1L; public static final long NO_BOARD_ID = -1L; @@ -24,17 +21,6 @@ public class DeckApplication extends Application { public void onCreate() { setAppTheme(isDarkTheme(getApplicationContext())); super.onCreate(); - AndroidThreeTen.init(this); - } - - // -------- - // Multidex - // -------- - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - install(this); } // ----------------- 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 9883608c2..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 @@ -83,11 +86,11 @@ public interface DeckAPI { @FormUrlEncoded @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel") - Observable assignLabelToCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId); + Observable<Void> assignLabelToCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId); @FormUrlEncoded @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/removeLabel") - Observable unassignLabelFromCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId); + Observable<Void> unassignLabelFromCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId); @FormUrlEncoded @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignUser") diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java index 29b3706fa..4e61abfb3 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java @@ -5,7 +5,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; -import java.util.Date; +import java.time.Instant; import java.util.List; import it.niedermann.nextcloud.deck.model.Attachment; @@ -16,6 +16,8 @@ import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; 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; @@ -42,14 +44,17 @@ public class GsonConfig { Type ocsUserList = new TypeToken<OcsUserList>() {}.getType(); Type ocsUser = new TypeToken<OcsUser>() {}.getType(); Type activity = new TypeToken<Activity>() {}.getType(); + Type activityList = new TypeToken<List<Activity>>() {}.getType(); Type attachment = new TypeToken<Attachment>() {}.getType(); Type attachmentList = new TypeToken<List<Attachment>>() {}.getType(); Type comment = new TypeToken<OcsComment>() {}.getType(); + Type projectList = new TypeToken<OcsProjectList>() {}.getType(); + Type groupMembers = new TypeToken<GroupMemberUIDs>() {}.getType(); INSTANCE = new GsonBuilder() .setDateFormat(DATE_PATTERN) .setLenient() - .registerTypeAdapter(Date.class, new GsonUTCDateAdapter()) + .registerTypeAdapter(Instant.class, new GsonUTCInstantAdapter()) .registerTypeAdapter(boardList, new NextcloudArrayDeserializer<>("boards", FullBoard.class)) .registerTypeAdapter(board, new NextcloudDeserializer<>("board", FullBoard.class)) .registerTypeAdapter(cardList, new NextcloudArrayDeserializer<>("cards", FullCard.class)) @@ -62,9 +67,12 @@ public class GsonConfig { .registerTypeAdapter(ocsUserList, new NextcloudDeserializer<>("ocsUserList", OcsUserList.class)) .registerTypeAdapter(ocsUser, new NextcloudDeserializer<>("ocsUser", OcsUser.class)) .registerTypeAdapter(activity, new NextcloudDeserializer<>("activity", Activity.class)) + .registerTypeAdapter(activityList, new NextcloudDeserializer<>("activityList", Activity.class)) .registerTypeAdapter(attachmentList, new NextcloudArrayDeserializer<>("attachments", Attachment.class)) .registerTypeAdapter(attachment, new NextcloudDeserializer<>("attachment", Attachment.class)) .registerTypeAdapter(comment, new NextcloudDeserializer<>("comment", OcsComment.class)) + .registerTypeAdapter(projectList, new NextcloudDeserializer<>("projectList", OcsProjectList.class)) + .registerTypeAdapter(groupMembers, new NextcloudDeserializer<>("groupMembers", GroupMemberUIDs.class)) .create(); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCDateAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCDateAdapter.java deleted file mode 100644 index 4d30b3e81..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCDateAdapter.java +++ /dev/null @@ -1,48 +0,0 @@ -package it.niedermann.nextcloud.deck.api; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -import java.lang.reflect.Type; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; -import java.util.regex.Pattern; - -public class GsonUTCDateAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> { - - private final DateFormat dateFormat; - private final Pattern UNIX_TIMESTAMP = Pattern.compile("^[0-9]+$"); - - public GsonUTCDateAdapter() { - //This is the format I need - dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - //This is the key line which converts the date to UTC which cannot be accessed with the default serializer - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - @Override public synchronized JsonElement serialize(Date date, Type type, JsonSerializationContext jsonSerializationContext) { - return new JsonPrimitive(dateFormat.format(date)); - } - - @Override public synchronized Date deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) { - String dateValue = jsonElement.getAsString(); - try { - return dateFormat.parse(dateValue); - } catch (ParseException e) { - // fallback to unix timestamp? - if (UNIX_TIMESTAMP.matcher(dateValue).matches()){ - return new Date(Long.valueOf(dateValue)); - } - throw new JsonParseException(e); - } - } -}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCInstantAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCInstantAdapter.java new file mode 100644 index 000000000..5a6b5b0c0 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCInstantAdapter.java @@ -0,0 +1,43 @@ +package it.niedermann.nextcloud.deck.api; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; +import java.text.ParseException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.regex.Pattern; + +public class GsonUTCInstantAdapter implements JsonSerializer<Instant>, JsonDeserializer<Instant> { + + private static final Pattern UNIX_TIMESTAMP = Pattern.compile("^[0-9]+$"); + + @Override + public synchronized JsonElement serialize(Instant date, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(DateTimeFormatter.ISO_INSTANT.format(date)); + } + + @Override + public synchronized Instant deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) { + String dateValue = jsonElement.getAsString(); + try { + final Instant parsedDate = Instant.parse(dateValue); + if (parsedDate == null) { + throw new ParseException("Parsed date is null", 0); + } + return parsedDate; + } catch (ParseException e) { + // fallback to unix timestamp? + if (UNIX_TIMESTAMP.matcher(dateValue).matches()) { + return Instant.ofEpochMilli(Long.parseLong(dateValue)); + } + throw new JsonParseException(e); + } + } +}
\ No newline at end of file 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 6dedefa01..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 @@ -1,20 +1,20 @@ package it.niedermann.nextcloud.deck.api; +import android.graphics.Color; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import org.threeten.bp.DateTimeUtils; -import org.threeten.bp.ZonedDateTime; -import org.threeten.bp.format.DateTimeFormatter; - +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Date; import java.util.List; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.exceptions.DeckException; -import it.niedermann.nextcloud.deck.exceptions.TraceableException; import it.niedermann.nextcloud.deck.model.AccessControl; import it.niedermann.nextcloud.deck.model.Attachment; import it.niedermann.nextcloud.deck.model.Board; @@ -32,13 +32,19 @@ import it.niedermann.nextcloud.deck.model.ocs.Version; 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.OcsComment; +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.model.ocs.user.GroupMemberUIDs; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; import static it.niedermann.nextcloud.deck.exceptions.DeckException.Hint.CAPABILITIES_VERSION_NOT_PARSABLE; +import static it.niedermann.nextcloud.deck.exceptions.TraceableException.makeTraceableIfFails; public class JsonToEntityParser { + @SuppressWarnings("unchecked") protected static <T> T parseJsonObject(JsonObject obj, Class<T> mType) { if (mType == FullBoard.class) { return (T) parseBoard(obj); @@ -55,44 +61,54 @@ public class JsonToEntityParser { } else if (mType == OcsUserList.class) { return (T) parseOcsUserList(obj); } else if (mType == OcsUser.class) { - return (T) parseOcsUser(obj); + return (T) parseSingleOcsUser(obj); } else if (mType == Attachment.class) { return (T) parseAttachment(obj); } else if (mType == OcsComment.class) { return (T) parseOcsComment(obj); + } else if (mType == GroupMemberUIDs.class) { + return (T) parseGroupMemberUIDs(obj); + } else if (mType == OcsProjectList.class) { + return (T) parseOcsProjectList(obj); } throw new IllegalArgumentException("unregistered type: " + mType.getCanonicalName()); } - private static OcsUser parseOcsUser(JsonObject obj) { + private static GroupMemberUIDs parseGroupMemberUIDs(JsonObject obj) { DeckLog.verbose(obj.toString()); - OcsUser ocsUser = new OcsUser(); - TraceableException.makeTraceableIfFails(() -> { + GroupMemberUIDs uids = new GroupMemberUIDs(); + makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); - if (!data.isJsonNull()) { - JsonObject jsonObject = data.getAsJsonObject(); - if (jsonObject.has("id")) { - ocsUser.setId(getNullAsEmptyString(jsonObject.get("id"))); - } - if (jsonObject.has("displayname")) { - ocsUser.setDisplayName(getNullAsEmptyString(jsonObject.get("displayname"))); + if (!data.isJsonNull() && data.getAsJsonObject().has("users")) { + JsonElement users = data.getAsJsonObject().get("users"); + if (!users.isJsonNull() && users.isJsonArray()) { + for (JsonElement userElement : users.getAsJsonArray()) { + uids.add(userElement.getAsString()); + } } } }, obj); - return ocsUser; + return uids; } private static OcsUserList parseOcsUserList(JsonObject obj) { DeckLog.verbose(obj.toString()); OcsUserList ocsUserList = new OcsUserList(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); if (!data.isJsonNull() && data.getAsJsonObject().has("users")) { JsonElement users = data.getAsJsonObject().get("users"); if (!users.isJsonNull() && users.isJsonArray()) { for (JsonElement userElement : users.getAsJsonArray()) { - ocsUserList.add(userElement.getAsString()); + JsonObject singleUserElement = userElement.getAsJsonObject(); + OcsUser user = new OcsUser(); + user.setDisplayName(singleUserElement.get("label").getAsString()); + user.setId( + singleUserElement.get("value").getAsJsonObject() + .get("shareWith").getAsString() + ); + ocsUserList.addUser(user); } } } @@ -101,10 +117,105 @@ public class JsonToEntityParser { return ocsUserList; } + private static OcsUser parseSingleOcsUser(JsonObject obj) { + DeckLog.verbose(obj.toString()); + OcsUser ocsUser = new OcsUser(); + makeTraceableIfFails(() -> { + JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); + if (!data.isJsonNull()) { + JsonObject user = data.getAsJsonObject(); + if (user.has("id")) { + ocsUser.setId(user.get("id").getAsString()); + } + if (user.has("displayname")) { + ocsUser.setDisplayName(user.get("displayname").getAsString()); + } + } + + }, obj); + return ocsUser; + } + + private static OcsProjectList parseOcsProjectList(JsonObject obj) { + DeckLog.verbose(obj.toString()); + OcsProjectList projectList = new OcsProjectList(); + makeTraceableIfFails(() -> { + JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); + if (!data.isJsonNull() && data.isJsonArray()) { + JsonArray projectJsonArray = data.getAsJsonArray(); + for (JsonElement jsonArrayElement : projectJsonArray) { + if (jsonArrayElement.isJsonObject()) { + JsonObject jsonObject = jsonArrayElement.getAsJsonObject(); + OcsProject project = new OcsProject(); + project.setId(jsonObject.get("id").getAsLong()); + project.setName(getNullAsEmptyString(jsonObject.get("name"))); + project.setResources(new ArrayList<>()); + JsonElement jsonResources = jsonObject.get("resources"); + if (jsonResources != null && jsonResources.isJsonArray()) { + JsonArray resourcesArray = jsonResources.getAsJsonArray(); + for (JsonElement resourceElement : resourcesArray) { + if (resourceElement.isJsonObject()) { + OcsProjectResource resource = parseOcsProjectResource(resourceElement.getAsJsonObject()); + resource.setProjectId(project.getId()); + project.getResources().add(resource); + } + } + } + projectList.add(project); + } + } + } + + }, obj); + return projectList; + } + + private static OcsProjectResource parseOcsProjectResource(JsonObject obj) { + DeckLog.verbose(obj.toString()); + OcsProjectResource resource = new OcsProjectResource(); + makeTraceableIfFails(() -> { + if (obj.has("id")) { + String idString = obj.get("id").getAsString(); + if (idString != null && idString.trim().length() > 0) { + if (idString.matches("[0-9]+")) { + resource.setId(Long.parseLong(idString.trim())); + } else { + resource.setIdString(idString); + } + } + } + if (obj.has("type")) { + resource.setType(getNullAsEmptyString(obj.get("type"))); + } + if (obj.has("name")) { + resource.setName(getNullAsEmptyString(obj.get("name"))); + } + if (obj.has("link")) { + resource.setLink(getNullAsEmptyString(obj.get("link"))); + } + if (obj.has("iconUrl")) { + resource.setIconUrl(getNullAsEmptyString(obj.get("iconUrl"))); + } + if (obj.has("path")) { + resource.setPath(obj.get("path").getAsString()); + } + if (obj.has("mimetype")) { + resource.setMimetype(obj.get("mimetype").getAsString()); + } + if (obj.has("preview-available")) { + resource.setPreviewAvailable(obj.get("preview-available").getAsBoolean()); + } else { + resource.setPreviewAvailable(false); + } + + }, obj); + return resource; + } + private static OcsComment parseOcsComment(JsonObject obj) { DeckLog.verbose(obj.toString()); OcsComment comment = new OcsComment(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); if (data.isJsonArray()) { for (JsonElement deckComment : data.getAsJsonArray()) { @@ -121,7 +232,7 @@ public class JsonToEntityParser { DeckLog.verbose(data.toString()); DeckComment deckComment = new DeckComment(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { JsonObject commentJson = data.getAsJsonObject(); deckComment.setId(commentJson.get("id").getAsLong()); @@ -132,17 +243,18 @@ public class JsonToEntityParser { deckComment.setActorType(commentJson.get("actorType").getAsString()); deckComment.setCreationDateTime(getTimestampFromString(commentJson.get("creationDateTime"))); - if (commentJson.has("replyTo")){ - JsonObject replyTo = commentJson.get("replyTo").getAsJsonObject(); - deckComment.setParentId(replyTo.get("id").getAsLong()); - } + if (commentJson.has("replyTo")) { + JsonObject replyTo = commentJson.get("replyTo").getAsJsonObject(); + deckComment.setParentId(replyTo.get("id").getAsLong()); + } - JsonElement mentions = commentJson.get("mentions"); - if (mentions != null && mentions.isJsonArray()) { - for (JsonElement mention : mentions.getAsJsonArray()) { - deckComment.addMention(parseMention(mention)); + JsonElement mentions = commentJson.get("mentions"); + if (mentions != null && mentions.isJsonArray()) { + for (JsonElement mention : mentions.getAsJsonArray()) { + deckComment.addMention(parseMention(mention)); + } } - }}, data); + }, data); return deckComment; } @@ -152,7 +264,7 @@ public class JsonToEntityParser { DeckLog.verbose(mentionJson.toString()); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { JsonObject mentionObject = mentionJson.getAsJsonObject(); mention.setMentionId(mentionObject.get("mentionId").getAsString()); mention.setMentionType(mentionObject.get("mentionType").getAsString()); @@ -169,9 +281,10 @@ public class JsonToEntityParser { DeckLog.verbose(e.toString()); Board board = new Board(); - TraceableException.makeTraceableIfFails(() -> { + 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"))); @@ -232,12 +345,19 @@ public class JsonToEntityParser { } } - JsonElement owner = e.get("owner"); - if (owner != null) { - if (owner.isJsonPrimitive()) {//TODO: remove if, let only else! - DeckLog.verbose("owner is Primitive, skipping"); - } else - fullBoard.setOwner(parseUser(owner.getAsJsonObject())); + if (e.has("owner")) { + fullBoard.setOwner(parseUser(e.get("owner"))); + } + if (e.has("users")) { + JsonElement users = e.get("users"); + if (users != null && !users.isJsonNull() && users.isJsonArray()) { + JsonArray usersArray = users.getAsJsonArray(); + List<User> usersList = new ArrayList<>(); + for (JsonElement userJson : usersArray) { + usersList.add(parseUser(userJson)); + } + fullBoard.setUsers(usersList); + } } }, e); return fullBoard; @@ -248,8 +368,8 @@ public class JsonToEntityParser { AccessControl acl = new AccessControl(); if (aclJson.has("participant") && !aclJson.get("participant").isJsonNull()) { - TraceableException.makeTraceableIfFails(() -> { - User participant = parseUser(aclJson.get("participant").getAsJsonObject()); + makeTraceableIfFails(() -> { + User participant = parseUser(aclJson.get("participant")); acl.setUser(participant); acl.setType(aclJson.get("type").getAsLong()); acl.setBoardId(aclJson.get("boardId").getAsLong()); @@ -271,12 +391,13 @@ public class JsonToEntityParser { FullCard fullCard = new FullCard(); Card card = new Card(); fullCard.setCard(card); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { card.setId(e.get("id").getAsLong()); card.setTitle(getNullAsEmptyString(e.get("title"))); 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"))); @@ -296,7 +417,7 @@ public class JsonToEntityParser { for (JsonElement assignedUser : assignedUsers) { JsonObject userJson = assignedUser.getAsJsonObject(); if (userJson.has("participant") && !userJson.get("participant").isJsonNull()) { - users.add(parseUser(userJson.get("participant").getAsJsonObject())); + users.add(parseUser(userJson.get("participant"))); } } fullCard.setAssignedUsers(users); @@ -322,10 +443,7 @@ public class JsonToEntityParser { card.setCommentsUnread(e.get("commentsUnread").getAsInt()); JsonElement owner = e.get("owner"); if (owner != null) { - if (owner.isJsonPrimitive()) {//TODO: remove if, let only else! - DeckLog.verbose("owner is Primitive, skipping"); - } else - fullCard.setOwner(parseUser(owner.getAsJsonObject())); + fullCard.setOwner(parseUser(owner)); } card.setArchived(e.get("archived").getAsBoolean()); }, e); @@ -336,16 +454,17 @@ public class JsonToEntityParser { protected static Attachment parseAttachment(JsonObject e) { DeckLog.verbose(e.toString()); Attachment a = new Attachment(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { 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"))); a.setCreatedBy(e.get("createdBy").getAsString()); a.setDeletedAt(getTimestampFromLong(e.get("deletedAt"))); - if (e.has("extendedData") && !e.get("extendedData").isJsonNull()) { + if (e.has("extendedData") && !e.get("extendedData").isJsonNull() && e.get("extendedData").isJsonObject()) { JsonObject extendedData = e.getAsJsonObject("extendedData").getAsJsonObject(); a.setFilesize(extendedData.get("filesize").getAsLong()); a.setMimetype(extendedData.get("mimetype").getAsString()); @@ -364,14 +483,27 @@ public class JsonToEntityParser { return a; } - protected static User parseUser(JsonObject e) { - DeckLog.verbose(e.toString()); + protected static User parseUser(JsonElement userElement) { + DeckLog.verbose(userElement.toString()); + if (userElement.isJsonNull()) { + return null; + } User user = new User(); - TraceableException.makeTraceableIfFails(() -> { - user.setDisplayname(getNullAsEmptyString(e.get("displayname"))); - user.setPrimaryKey(getNullAsEmptyString(e.get("primaryKey"))); - user.setUid(getNullAsEmptyString(e.get("uid"))); - }, e); + makeTraceableIfFails(() -> { + + if (userElement.isJsonPrimitive()) { + String uid = userElement.getAsString(); + user.setDisplayname(uid); + user.setPrimaryKey(uid); + user.setUid(uid); + } else { + JsonObject userJson = userElement.getAsJsonObject(); + user.setDisplayname(getNullAsEmptyString(userJson.get("displayname"))); + user.setPrimaryKey(getNullAsEmptyString(userJson.get("primaryKey"))); + user.setUid(getNullAsEmptyString(userJson.get("uid"))); + } + + }, userElement); return user; } @@ -419,8 +551,8 @@ public class JsonToEntityParser { } if (caps.has("theming")) { JsonObject theming = caps.getAsJsonObject("theming"); - capabilities.setColor(theming.get("color").getAsString()); - capabilities.setTextColor(theming.get("color-text").getAsString()); + capabilities.setColor(getColorAsInt(theming, "color")); + capabilities.setTextColor(getColorAsInt(theming, "color-text")); } } capabilities.setDeckVersion(Version.of(version)); @@ -429,31 +561,17 @@ public class JsonToEntityParser { return capabilities; } - protected static List<Activity> parseActivity(JsonObject e) { - DeckLog.verbose(e.toString()); - List<Activity> activityList = new ArrayList<>(); - - TraceableException.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); - } - } + private static int getColorAsInt(JsonObject element, String field) { + String rawString = getNullAsEmptyString(element.get(field)); + try { + if (!rawString.trim().isEmpty()) { + String colorAsString = ColorUtil.INSTANCE.formatColorToParsableHexString(rawString); + return Color.parseColor(colorAsString); } - }, e); - return activityList; + } catch (Exception e) { + // Do mostly nothing, return default value + } + return Color.GRAY; } protected static FullStack parseStack(JsonObject e) { @@ -461,10 +579,11 @@ public class JsonToEntityParser { FullStack fullStack = new FullStack(); Stack stack = new Stack(); fullStack.setStack(stack); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { 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()) { @@ -485,37 +604,70 @@ 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(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { label.setId(e.get("id").getAsLong()); //todo: last modified! // label.setLastModified(get); label.setTitle(getNullAsEmptyString(e.get("title"))); - label.setColor(getNullAsEmptyString(e.get("color"))); + 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 Date getTimestampFromString(JsonElement jsonElement) { + private static Instant getTimestampFromString(JsonElement jsonElement) { if (jsonElement.isJsonNull()) { return null; } else { String dateAsString = jsonElement.getAsString(); - return DateTimeUtils.toDate(ZonedDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(dateAsString)).toInstant()); + return ZonedDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(dateAsString)).toInstant(); } } - private static Date getTimestampFromLong(JsonElement jsonElement) { + private static Instant getTimestampFromLong(JsonElement jsonElement) { if (jsonElement.isJsonNull()) { return null; } else { - return new Date(jsonElement.getAsLong() * 1000); + return Instant.ofEpochMilli(jsonElement.getAsLong() * 1000); } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java index 1d44bfe13..1492d78ac 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java @@ -3,7 +3,7 @@ package it.niedermann.nextcloud.deck.api; import android.content.Context; import android.content.SharedPreferences; -import java.util.Date; +import java.time.Instant; import it.niedermann.nextcloud.deck.R; @@ -22,19 +22,19 @@ public class LastSyncUtil { } - public static long getLastSync(long accountId){ + public static long getLastSync(long accountId) { return INSTANCE.lastSyncPref.getLong(getSyncKeyForAccount(accountId), 0L); } - public static Date getLastSyncDate(long accountId){ - return new Date(getLastSync(accountId)); + public static Instant getLastSyncDate(long accountId) { + return Instant.ofEpochMilli(getLastSync(accountId)); } - public static void setLastSyncDate(long accountId, Date value){ - INSTANCE.lastSyncPref.edit().putLong(getSyncKeyForAccount(accountId), value.getTime()).apply(); + public static void setLastSyncDate(long accountId, Instant value) { + INSTANCE.lastSyncPref.edit().putLong(getSyncKeyForAccount(accountId), value.toEpochMilli()).apply(); } - public static void resetLastSyncDate(long accountId){ + public static void resetLastSyncDate(long accountId) { INSTANCE.lastSyncPref.edit().remove(getSyncKeyForAccount(accountId)).apply(); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java index d69ee8513..7b93ace49 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.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; @@ -8,11 +10,14 @@ import it.niedermann.nextcloud.deck.model.ocs.Activity; 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 retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.Header; import retrofit2.http.Headers; import retrofit2.http.POST; import retrofit2.http.PUT; @@ -22,13 +27,19 @@ import retrofit2.http.Query; public interface NextcloudServerAPI { @GET("cloud/capabilities?format=json") - Observable<Capabilities> getCapabilities(); + Observable<ParsedResponse<Capabilities>> getCapabilities(@Header("If-None-Match") String eTag); + + @GET("collaboration/resources/deck-card/{cardId}?format=json") + Observable<OcsProjectList> getProjectsForCard(@Path("cardId") long cardId); + + @GET("apps/files_sharing/api/v1/sharees?format=json&perPage=20&itemType=0%2C1%2C7") + Observable<OcsUserList> searchUser(@Query("search") String searchTerm); - @GET("cloud/users?format=json") - Observable<OcsUserList> getAllUsers(); + @GET("cloud/groups/{search}?format=json") + Observable<GroupMemberUIDs> searchGroupMembers(@Path("search") String groupUid); - @GET("cloud/users/{uid}?format=json") - Observable<OcsUser> getUserDetails(@Path("uid") String uid); + @GET("cloud/users/{search}?format=json") + Observable<OcsUser> getSingleUserData(@Path("search") String userUid); @GET("apps/activity/api/v2/activity/filter?format=json&object_type=deck_card&limit=50&since=-1&sort=asc") Observable<List<Activity>> getActivitiesForCard(@Query("object_id") long cardId); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java new file mode 100644 index 000000000..5169e7f5e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java @@ -0,0 +1,26 @@ +package it.niedermann.nextcloud.deck.api.json; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +import it.niedermann.android.util.ColorUtil; + +public class JsonColorSerializer extends TypeAdapter<Integer> { + @Override + public void write(JsonWriter out, Integer value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(ColorUtil.INSTANCE.intColorToHexString(value)); + } + } + + @Override + public Integer read(JsonReader in) throws IOException { + // currently not needed + return null; + } +} 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 8c2904d2b..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,6 +5,8 @@ public class DeckException extends IllegalArgumentException { public enum Hint { CAPABILITIES_NOT_PARSABLE, CAPABILITIES_VERSION_NOT_PARSABLE, + 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 a53ce8c1f..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,22 +11,24 @@ public class TraceableException extends RuntimeException { public static void makeTraceableIfFails(Runnable runnable, Object... args) { try { runnable.run(); + } catch (TraceableException t) { + throw t; } catch (Throwable t) { - String message = "Sorry, a wild error appeared!\n" + - "### If you want to tell us about the following issue, " + - "please make sure to censor sensitive data beforehand! ###\n" + - "Failed to run traceable code"; + final StringBuilder message = new StringBuilder("Sorry, a wild error appeared!\n\n" + + "⚠️ If you want to tell us about the following issue, " + + "please make sure to censor sensitive data beforehand! ⚠️\n\n" + + "Failed to run traceable code"); if (args != null && args.length > 0) { - message += " with arguments:\n"; + message.append(" with arguments:\n"); for (Object arg : args) { - message += (arg == null ? "null" : arg.toString())+"\n"; + message.append(arg == null ? "null" : arg.toString()).append("\n"); } } else { - message += ":\n"; + message.append(":\n"); } - message += "Cause: " + t.getLocalizedMessage(); - TraceableException ex = new TraceableException(message, t); + message.append("Cause: ").append(t.getLocalizedMessage()); + TraceableException ex = new TraceableException(message.toString(), t); DeckLog.logError(ex); throw ex; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java index 299dd7ca0..e2a3ccc3b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java @@ -8,6 +8,7 @@ import androidx.room.Index; import com.google.gson.annotations.SerializedName; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; @Entity(inheritSuperIndices = true, indices = { @@ -35,6 +36,8 @@ public class AccessControl extends AbstractRemoteEntity { @Ignore @SerializedName("participant") private User user; + @Ignore + private GroupMemberUIDs groupMemberUIDs; public AccessControl() { super(); @@ -115,6 +118,14 @@ public class AccessControl extends AbstractRemoteEntity { this.user = user; } + public GroupMemberUIDs getGroupMemberUIDs() { + return groupMemberUIDs; + } + + public void setGroupMemberUIDs(GroupMemberUIDs groupMemberUIDs) { + this.groupMemberUIDs = groupMemberUIDs; + } + @Override public boolean equals(Object o) { if (this == o) return true; 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 ba8fb72b7..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 @@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.model; import android.net.Uri; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Px; import androidx.room.ColumnInfo; @@ -18,7 +19,6 @@ import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.Version; import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog; -import it.niedermann.nextcloud.deck.util.ColorUtil; @Entity(indices = {@Index(value = "name", unique = true)}) public class Account implements Serializable { @@ -38,12 +38,12 @@ public class Account implements Serializable { private String url; @NonNull - @ColumnInfo(defaultValue = "#0082c9") - private String color = "#0082c9"; + @ColumnInfo(defaultValue = "0") + private Integer color = 0; @NonNull - @ColumnInfo(defaultValue = "#ffffff") - private String textColor = "#ffffff"; + @ColumnInfo(defaultValue = "0") + private Integer textColor = 0; @NonNull @ColumnInfo(defaultValue = "0.6.4") @@ -53,6 +53,9 @@ public class Account implements Serializable { @ColumnInfo(defaultValue = "0") private boolean maintenanceEnabled = false; + private String etag; + private String boardsEtag; + @Ignore public Account(Long id, @NonNull String name, @NonNull String userName, @NonNull String url) { this(name, userName, url); @@ -60,7 +63,7 @@ public class Account implements Serializable { } @Ignore - public Account(String name, String userName, String url) { + public Account(@NonNull String name, @NonNull String userName, @NonNull String url) { this.name = name; this.userName = userName; this.url = url; @@ -74,22 +77,29 @@ public class Account implements Serializable { public Account() { } - public void applyCapabilities(Capabilities capabilities) { + public void applyCapabilities(Capabilities capabilities, String eTag) { + if (capabilities == null) { + maintenanceEnabled = true; + return; + } maintenanceEnabled = capabilities.isMaintenanceEnabled(); if (!isMaintenanceEnabled()) { try { // Nextcloud might return color format #000 which cannot be parsed by Color.parseColor() // https://github.com/stefan-niedermann/nextcloud-deck/issues/466 - color = ColorUtil.formatColorToParsableHexString(capabilities.getColor()); - textColor = ColorUtil.formatColorToParsableHexString(capabilities.getTextColor()); + color = capabilities.getColor(); + textColor = capabilities.getTextColor(); } catch (Exception e) { DeckLog.logError(e); - color = "#0082c9"; - color = "#ffffff"; + color = 0; + color = 0; } if (capabilities.getDeckVersion() != null) { serverDeckVersion = capabilities.getDeckVersion().getOriginalVersion(); } + if (eTag != null) { + this.etag = eTag; + } } } @@ -132,21 +142,23 @@ public class Account implements Serializable { return serialVersionUID; } + @ColorInt @NonNull - public String getColor() { + public Integer getColor() { return color; } - public void setColor(@NonNull String color) { + public void setColor(@NonNull Integer color) { this.color = color; } @NonNull - public String getTextColor() { + public Integer getTextColor() { return textColor; } - public void setTextColor(@NonNull String textColor) { + @Deprecated + public void setTextColor(@NonNull Integer textColor) { this.textColor = textColor; } @@ -171,6 +183,31 @@ public class Account implements Serializable { this.maintenanceEnabled = maintenanceEnabled; } + public String getEtag() { + return etag; + } + + public void setEtag(String etag) { + 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}) + * + * @return an {@link String} to fetch the avatar for this account. + */ + public String getAvatarUrl(@Px int size) { + return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -185,16 +222,8 @@ public class Account implements Serializable { if (!url.equals(account.url)) return false; if (!color.equals(account.color)) return false; if (!textColor.equals(account.textColor)) return false; - return serverDeckVersion.equals(account.serverDeckVersion); - } - - /** - * 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}) - * - * @return an {@link String} to fetch the avatar for this account. - */ - public String getAvatarUrl(@Px int size) { - return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size; + if (!serverDeckVersion.equals(account.serverDeckVersion)) return false; + return etag != null ? etag.equals(account.etag) : account.etag == null; } @Override @@ -207,6 +236,7 @@ public class Account implements Serializable { result = 31 * result + textColor.hashCode(); result = 31 * result + serverDeckVersion.hashCode(); result = 31 * result + (maintenanceEnabled ? 1 : 0); + result = 31 * result + (etag != null ? etag.hashCode() : 0); return result; } @@ -221,6 +251,7 @@ public class Account implements Serializable { ", textColor='" + textColor + '\'' + ", serverDeckVersion='" + serverDeckVersion + '\'' + ", maintenanceEnabled=" + maintenanceEnabled + + ", eTag='" + etag + '\'' + '}'; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java index 91192f597..7eabcbe70 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java @@ -2,32 +2,33 @@ package it.niedermann.nextcloud.deck.model; import androidx.room.Entity; import androidx.room.ForeignKey; +import androidx.room.Ignore; import androidx.room.Index; import java.io.Serializable; -import java.util.Date; +import java.time.Instant; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @Entity(inheritSuperIndices = true, indices = {@Index("cardId")}, foreignKeys = { - @ForeignKey( - entity = Card.class, - parentColumns = "localId", - childColumns = "cardId", - onDelete = ForeignKey.CASCADE - ) - } + @ForeignKey( + entity = Card.class, + parentColumns = "localId", + childColumns = "cardId", + onDelete = ForeignKey.CASCADE + ) + } ) public class Attachment extends AbstractRemoteEntity implements Comparable<Attachment>, Serializable { private long cardId; private String type = "deck_file"; private String data; - private Date createdAt; + private Instant createdAt; private String createdBy; - private Date deletedAt; + private Instant deletedAt; private long filesize; private String mimetype; private String dirname; @@ -35,6 +36,9 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac private String extension; private String filename; private String localPath; + // TODO should probably be a Long... depends on https://github.com/nextcloud/deck/pull/2638 + @Ignore + private String fileId; public long getCardId() { return cardId; @@ -60,11 +64,11 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac this.data = data; } - public Date getCreatedAt() { + public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Date createdAt) { + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } @@ -76,11 +80,11 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac this.createdBy = createdBy; } - public Date getDeletedAt() { + public Instant getDeletedAt() { return deletedAt; } - public void setDeletedAt(Date deletedAt) { + public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } @@ -140,6 +144,22 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac this.localPath = localPath; } + /** + * TODO depends on https://github.com/nextcloud/deck/pull/2638 + */ + @Ignore + public String getFileId() { + return this.fileId; + } + + /** + * TODO depends on https://github.com/nextcloud/deck/pull/2638 + */ + @Ignore + public void setFileId(String fileId) { + this.fileId = fileId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -202,7 +222,7 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac private static int longToComparsionResult(long diff) { if (diff > 0) { return 1; - } else if(diff < 0) { + } else if (diff < 0) { return -1; } return 0; @@ -210,18 +230,18 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac public long getModificationTimeForComparsion() { if (lastModifiedLocal != null) { - return lastModifiedLocal.getTime(); + return lastModifiedLocal.toEpochMilli(); } if (lastModified != null) { - return lastModified.getTime(); + return lastModified.toEpochMilli(); } - return new Date().getTime(); + return Instant.now().toEpochMilli(); } public long getCreationTimeForComparsion() { if (createdAt != null) { - return createdAt.getTime(); + return createdAt.toEpochMilli(); } - return new Date().getTime(); + return Instant.now().toEpochMilli(); } }
\ No newline at end of file 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 29afe844b..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 @@ -1,17 +1,23 @@ package it.niedermann.nextcloud.deck.model; +import android.graphics.Color; + +import androidx.annotation.ColorInt; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Ignore; import androidx.room.Index; +import com.google.gson.annotations.JsonAdapter; + import java.io.Serializable; -import java.util.Date; +import java.time.Instant; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.json.JsonColorSerializer; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; -import it.niedermann.nextcloud.deck.util.ColorUtil; @Entity( inheritSuperIndices = true, @@ -31,20 +37,18 @@ 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); } private String title; private long ownerId; - /** - * Deck App sends color strings without leading # character - */ - private String color; + @JsonAdapter(JsonColorSerializer.class) + private Integer color; private boolean archived; private int shared; - private Date deletedAt; + private Instant deletedAt; private boolean permissionRead = false; private boolean permissionEdit = false; private boolean permissionManage = false; @@ -52,22 +56,22 @@ public class Board extends AbstractRemoteEntity implements Serializable { @Override - public Date getLastModified() { + public Instant getLastModified() { return lastModified; } @Override - public void setLastModified(Date lastModified) { + public void setLastModified(Instant lastModified) { this.lastModified = lastModified; } @Override - public Date getLastModifiedLocal() { + public Instant getLastModifiedLocal() { return lastModifiedLocal; } @Override - public void setLastModifiedLocal(Date lastModifiedLocal) { + public void setLastModifiedLocal(Instant lastModifiedLocal) { this.lastModifiedLocal = lastModifiedLocal; } @@ -79,21 +83,25 @@ public class Board extends AbstractRemoteEntity implements Serializable { this.id = id; } - public String getColor() { + @ColorInt + public Integer getColor() { return color; } + public void setColor(String color) { try { - // Nextcloud might return color format #000 which cannot be parsed by Color.parseColor() - // https://github.com/stefan-niedermann/nextcloud-deck/issues/466 - this.color = ColorUtil.formatColorToParsableHexString(color).substring(1); + setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(color))); } catch (Exception e) { DeckLog.logError(e); - this.color = "757575"; + setColor(Color.GRAY); } } + public void setColor(@ColorInt Integer color) { + this.color = color; + } + public boolean isArchived() { return archived; } @@ -114,11 +122,11 @@ public class Board extends AbstractRemoteEntity implements Serializable { this.shared = shared; } - public Date getDeletedAt() { + public Instant getDeletedAt() { return deletedAt; } - public void setDeletedAt(Date deletedAt) { + public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java index 732ccfc50..6f2b70020 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java @@ -8,7 +8,7 @@ import androidx.room.Index; import com.google.gson.annotations.SerializedName; -import java.util.Date; +import java.time.Instant; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -16,22 +16,23 @@ import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @Entity(inheritSuperIndices = true, - indices = { - @Index(value = "accountId", name = "card_accID"), - @Index("stackId") - }, - foreignKeys = { - @ForeignKey( - entity = Stack.class, - parentColumns = "localId", - childColumns = "stackId", onDelete = ForeignKey.CASCADE - ) - } + indices = { + @Index(value = "accountId", name = "card_accID"), + @Index("stackId") + }, + foreignKeys = { + @ForeignKey( + entity = Stack.class, + parentColumns = "localId", + childColumns = "stackId", onDelete = ForeignKey.CASCADE + ) + } ) public class Card extends AbstractRemoteEntity { private static Pattern PATTERN_MD_TASK = Pattern.compile("\\[([xX ])]"); - public class TaskStatus { + + public static class TaskStatus { public int taskCount; public int doneCount; @@ -49,20 +50,21 @@ public class Card extends AbstractRemoteEntity { @NonNull private Long stackId; private String type; - private Date createdAt; - private Date deletedAt; + private Instant createdAt; + private Instant deletedAt; private int attachmentCount; private Long userId; private int order; private boolean archived; @SerializedName("duedate") - private Date dueDate; + private Instant dueDate; private boolean notified; private int overdue; private int commentsUnread; - public Card() {} + public Card() { + } @Ignore public Card(String title, String description, long stackId) { @@ -89,15 +91,15 @@ public class Card extends AbstractRemoteEntity { this.commentsUnread = card.getCommentsUnread(); } - public TaskStatus getTaskStatus(){ - if (taskStatus == null){ + public TaskStatus getTaskStatus() { + if (taskStatus == null) { int count = 0, done = 0; if (description != null) { Matcher matcher = PATTERN_MD_TASK.matcher(description); - while (matcher.find()){ + while (matcher.find()) { count++; char c = matcher.group().charAt(1); - if (c == 'x' || c == 'X'){ + if (c == 'x' || c == 'X') { done++; } } @@ -164,19 +166,19 @@ public class Card extends AbstractRemoteEntity { this.type = type; } - public Date getCreatedAt() { + public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Date createdAt) { + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } - public Date getDeletedAt() { + public Instant getDeletedAt() { return deletedAt; } - public void setDeletedAt(Date deletedAt) { + public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } @@ -200,12 +202,12 @@ public class Card extends AbstractRemoteEntity { this.archived = archived; } - public Date getDueDate() { + public Instant getDueDate() { return dueDate; } - public void setDueDate(Date dueDate) { - this.dueDate = dueDate; + public void setDueDate(Instant dateTime) { + this.dueDate = dateTime; } public int getOverdue() { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java index 0d97ce5a0..da24edb5a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java @@ -1,27 +1,41 @@ package it.niedermann.nextcloud.deck.model; +import android.graphics.Color; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; +import com.google.gson.annotations.JsonAdapter; + import java.io.Serializable; +import it.niedermann.android.util.ColorUtil; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.json.JsonColorSerializer; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @Entity(inheritSuperIndices = true, indices = {@Index("boardId"), @Index(value = {"boardId", "title"}, unique = true, name = "idx_label_title_unique")}, foreignKeys = { - @ForeignKey( - entity = Board.class, - parentColumns = "localId", - childColumns = "boardId", - onDelete = ForeignKey.CASCADE - ) - } + @ForeignKey( + entity = Board.class, + parentColumns = "localId", + childColumns = "boardId", + onDelete = ForeignKey.CASCADE + ) + } ) public class Label extends AbstractRemoteEntity implements Serializable { private String title; - private String color; + + @JsonAdapter(JsonColorSerializer.class) + @NonNull + @ColumnInfo(defaultValue = "0") + private Integer color; private long boardId; public Label() { @@ -42,14 +56,25 @@ public class Label extends AbstractRemoteEntity implements Serializable { this.title = title; } - public String getColor() { + @NonNull + @ColorInt + public Integer getColor() { return color; } - public void setColor(String color) { + public void setColor(@NonNull @ColorInt Integer color) { this.color = color; } + public void setColor(String color) { + try { + setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(color))); + } catch (Exception e) { + DeckLog.logError(e); + setColor(Color.GRAY); + } + } + public long getBoardId() { return boardId; } @@ -62,18 +87,20 @@ public class Label extends AbstractRemoteEntity implements Serializable { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; Label label = (Label) o; if (boardId != label.boardId) return false; if (title != null ? !title.equals(label.title) : label.title != null) return false; - return color != null ? color.equals(label.color) : label.color == null; + return color.equals(label.color); } @Override public int hashCode() { - int result = title != null ? title.hashCode() : 0; - result = 31 * result + (color != null ? color.hashCode() : 0); + int result = super.hashCode(); + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + color.hashCode(); result = 31 * result + (int) (boardId ^ (boardId >>> 32)); return result; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java index 330ec16d7..00f054605 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java @@ -1,12 +1,11 @@ package it.niedermann.nextcloud.deck.model; -import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Ignore; import androidx.room.Index; -import java.util.Date; +import java.time.Instant; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @@ -35,12 +34,10 @@ public class Stack extends AbstractRemoteEntity { private String title; - @NonNull private long boardId; - private Date deletedAt; + private Instant deletedAt; - @NonNull private int order; // // @ToMany @@ -64,11 +61,11 @@ public class Stack extends AbstractRemoteEntity { this.boardId = boardId; } - public Date getDeletedAt() { + public Instant getDeletedAt() { return deletedAt; } - public void setDeletedAt(Date deletedAt) { + public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java index cfb48d543..fd318ce7a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java @@ -4,10 +4,12 @@ import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.Index; +import java.io.Serializable; + import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @Entity(inheritSuperIndices = true, indices = {@Index(value = "uid", name = "user_uid")}) -public class User extends AbstractRemoteEntity { +public class User extends AbstractRemoteEntity implements Serializable { private String primaryKey; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java new file mode 100644 index 000000000..fb7a74076 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java @@ -0,0 +1,68 @@ +package it.niedermann.nextcloud.deck.model.appwidgets; + +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Stack; + +@Entity( + indices = { + @Index("stackId"), + @Index("accountId") + }, + foreignKeys = { + @ForeignKey( + entity = Account.class, + parentColumns = "id", + childColumns = "accountId", onDelete = ForeignKey.CASCADE + ), + @ForeignKey( + entity = Stack.class, + parentColumns = "localId", + childColumns = "stackId", onDelete = ForeignKey.CASCADE + ) + } +) +public class StackWidgetModel { + + @PrimaryKey() + private Integer appWidgetId; + private Long accountId; + private Long stackId; + private boolean darkTheme; + + public Integer getAppWidgetId() { + return appWidgetId; + } + + public void setAppWidgetId(Integer appWidgetId) { + this.appWidgetId = appWidgetId; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getStackId() { + return stackId; + } + + public void setStackId(Long stackId) { + this.stackId = stackId; + } + + public boolean getDarkTheme() { + return darkTheme; + } + + public void setDarkTheme(boolean darkTheme) { + this.darkTheme = darkTheme; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java index 31ad21f16..f9f1643ff 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java @@ -1,5 +1,6 @@ package it.niedermann.nextcloud.deck.model.full; +import androidx.annotation.NonNull; import androidx.room.Embedded; import androidx.room.Ignore; import androidx.room.Relation; @@ -29,6 +30,8 @@ public class FullBoard implements IRemoteEntity { @Relation(entity = Stack.class, parentColumn = "localId", entityColumn = "boardId") public List<Stack> stacks; + @Ignore + public List<User> users; public User getOwner() { return owner; @@ -54,9 +57,17 @@ public class FullBoard implements IRemoteEntity { this.labels = labels; } + public List<User> getUsers() { + return users; + } + + public void setUsers(List<User> users) { + this.users = users; + } + @Ignore @Override - public IRemoteEntity getEntity() { + public Board getEntity() { return board; } @@ -102,6 +113,7 @@ public class FullBoard implements IRemoteEntity { return result; } + @NonNull @Override public String toString() { return "FullBoard{" + diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java index 41303cac6..611baf3df 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java @@ -6,8 +6,6 @@ import androidx.room.Ignore; import androidx.room.Junction; import androidx.room.Relation; -import org.jetbrains.annotations.NotNull; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -25,7 +23,7 @@ import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; public class FullCard implements IRemoteEntity, DragAndDropModel { @Ignore - private transient boolean isAttachmentsSorted = false; + protected transient boolean isAttachmentsSorted = false; @Embedded public Card card; @@ -48,7 +46,6 @@ public class FullCard implements IRemoteEntity, DragAndDropModel { @Relation(entity = DeckComment.class, parentColumn = "localId", entityColumn = "objectId", projection = "localId") public List<Long> commentIDs; - public FullCard() { super(); } @@ -126,11 +123,11 @@ public class FullCard implements IRemoteEntity, DragAndDropModel { @Ignore @Override - public IRemoteEntity getEntity() { + public Card getEntity() { return card; } - @NotNull + @NonNull @Override public String toString() { return "FullCard{" + diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java new file mode 100644 index 000000000..cf43fc86b --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java @@ -0,0 +1,82 @@ +package it.niedermann.nextcloud.deck.model.full; + +import androidx.annotation.NonNull; +import androidx.room.Junction; +import androidx.room.Relation; + +import java.util.ArrayList; +import java.util.List; + +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.full.OcsProjectWithResources; + +public class FullCardWithProjects extends FullCard { + + + @NonNull + @Relation(entity = OcsProject.class, parentColumn = "localId", entityColumn = "localId", + associateBy = @Junction(value = JoinCardWithProject.class, parentColumn = "cardId", entityColumn = "projectId")) + + private List<OcsProjectWithResources> projects = new ArrayList<>(); + + public FullCardWithProjects() { + super(); + } + + public FullCardWithProjects(FullCardWithProjects fullCard) { + super(fullCard); + this.projects = copyList(fullCard.getProjects()); + } + + @NonNull + public List<OcsProjectWithResources> getProjects() { + return projects; + } + + public void setProjects(@NonNull List<OcsProjectWithResources> projects) { + this.projects = projects; + } + + @NonNull + @Override + public String toString() { + return "FullCard{" + + "card=" + card + + ", labels=" + labels + + ", assignedUsers=" + assignedUsers + + ", owner=" + owner + + ", attachments=" + attachments + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FullCardWithProjects fullCard = (FullCardWithProjects) o; + + if (card != null ? !card.equals(fullCard.card) : fullCard.card != null) return false; + if (labels != null ? !labels.equals(fullCard.labels) : fullCard.labels != null) + return false; + if (assignedUsers != null ? !assignedUsers.equals(fullCard.assignedUsers) : fullCard.assignedUsers != null) + return false; + if (owner != null ? !owner.equals(fullCard.owner) : fullCard.owner != null) return false; + if (attachments != null ? !attachments.equals(fullCard.attachments) : fullCard.attachments != null) + return false; + return commentIDs != null ? commentIDs.equals(fullCard.commentIDs) : fullCard.commentIDs == null; + } + + @Override + public int hashCode() { + int result = (isAttachmentsSorted ? 1 : 0); + result = 31 * result + (card != null ? card.hashCode() : 0); + result = 31 * result + (labels != null ? labels.hashCode() : 0); + result = 31 * result + (assignedUsers != null ? assignedUsers.hashCode() : 0); + result = 31 * result + (owner != null ? owner.hashCode() : 0); + result = 31 * result + (attachments != null ? attachments.hashCode() : 0); + result = 31 * result + (commentIDs != null ? commentIDs.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java index eddbafce4..1c9e94899 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java @@ -14,7 +14,7 @@ public class FullStack implements IRemoteEntity { @Embedded public Stack stack; - @Relation(entity = Card.class, parentColumn = "localId", entityColumn = "stackId") + @Relation(entity = Card.class, parentColumn = "localId", entityColumn = "stackId") public List<Card> cards; @@ -36,7 +36,7 @@ public class FullStack implements IRemoteEntity { @Ignore @Override - public IRemoteEntity getEntity() { + public Stack getEntity() { return stack; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java index 50057dfe4..ce14ac6be 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java @@ -1,21 +1,20 @@ package it.niedermann.nextcloud.deck.model.interfaces; -import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.Ignore; + import it.niedermann.nextcloud.deck.model.enums.DBStatus; @Entity() public abstract class AbstractJoinEntity { - @NonNull protected int status = DBStatus.UP_TO_DATE.getId(); public int getStatus() { return status; } - public void setStatus(@NonNull int status) { + public void setStatus(int status) { this.status = status; } 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 328c642ae..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 @@ -1,31 +1,30 @@ package it.niedermann.nextcloud.deck.model.interfaces; -import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; -import java.util.Date; +import java.time.Instant; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.enums.DBStatus; @Entity( - indices = { - @Index("accountId"), - @Index("id"), - @Index("lastModifiedLocal"), - @Index(value = {"accountId", "id"}, unique = true) - }, - foreignKeys = { - @ForeignKey( - entity = Account.class, - parentColumns = "id", - childColumns = "accountId", onDelete = ForeignKey.CASCADE - ) - } + indices = { + @Index("accountId"), + @Index("id"), + @Index("lastModifiedLocal"), + @Index(value = {"accountId", "id"}, unique = true) + }, + foreignKeys = { + @ForeignKey( + entity = Account.class, + parentColumns = "id", + childColumns = "accountId", onDelete = ForeignKey.CASCADE + ) + } ) public abstract class AbstractRemoteEntity implements IRemoteEntity { @PrimaryKey(autoGenerate = true) @@ -35,11 +34,12 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity { protected Long id; - @NonNull protected int status = DBStatus.UP_TO_DATE.getId(); - protected Date lastModified; - protected Date lastModifiedLocal; + protected Instant lastModified; + protected Instant lastModifiedLocal; + + protected String etag; public AbstractRemoteEntity() { } @@ -102,35 +102,30 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity { @Override - public void setStatus(@NonNull int status) { + public void setStatus(int status) { this.status = status; } - @Override - public Date getLastModified() { + public Instant getLastModified() { return this.lastModified; } - @Override - public void setLastModified(Date lastModified) { + public void setLastModified(Instant lastModified) { this.lastModified = lastModified; } - @Override - public Date getLastModifiedLocal() { + public Instant getLastModifiedLocal() { return this.lastModifiedLocal; } - @Override - public void setLastModifiedLocal(Date lastModifiedLocal) { + public void setLastModifiedLocal(Instant lastModifiedLocal) { this.lastModifiedLocal = lastModifiedLocal; } - @Ignore @Override public DBStatus getStatusEnum() { @@ -144,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 30a478808..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 @@ -1,94 +1,87 @@ package it.niedermann.nextcloud.deck.model.interfaces; -import androidx.annotation.NonNull; - +import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.List; import it.niedermann.nextcloud.deck.model.enums.DBStatus; public interface IRemoteEntity { - default IRemoteEntity getEntity() {return this;} + default IRemoteEntity getEntity() { + return this; + } default Long getLocalId() { return getEntity().getLocalId(); } - default void setLocalId(Long localId) { getEntity().setLocalId(localId); } - default long getAccountId() { return getEntity().getAccountId(); } - default void setAccountId(long accountId) { getEntity().setAccountId(accountId); } - default Long getId() { return getEntity().getId(); } - default void setId(Long id) { getEntity().setId(id); } - default int getStatus() { return getEntity().getStatus(); } - - default void setStatus(@NonNull int status) { + default void setStatus(int status) { getEntity().setStatus(status); } - - default Date getLastModified() { + default Instant getLastModified() { return getEntity().getLastModified(); } - - default void setLastModified(Date lastModified) { + default void setLastModified(Instant lastModified) { getEntity().setLastModified(lastModified); } - - default Date getLastModifiedLocal() { + default Instant getLastModifiedLocal() { return getEntity().getLastModifiedLocal(); } - - default void setLastModifiedLocal(Date lastModifiedLocal) { + default void setLastModifiedLocal(Instant lastModifiedLocal) { getEntity().setLastModifiedLocal(lastModifiedLocal); } - default DBStatus getStatusEnum() { return getEntity().getStatusEnum(); } - default void setStatusEnum(DBStatus status) { 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; } List<T> list = new ArrayList<>(listToCopy.size()); - for (T t : listToCopy) { - list.add(t); - } + list.addAll(listToCopy); return list; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java index f3892dfa5..1b5f0879d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java @@ -3,8 +3,6 @@ package it.niedermann.nextcloud.deck.model.internal; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.jetbrains.annotations.NotNull; - import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -14,12 +12,21 @@ import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.enums.EDueType; public class FilterInformation implements Serializable { + + public enum EArchiveStatus{ + ALL, ARCHIVED, NON_ARCHIVED + } + @NonNull private EDueType dueType = EDueType.NO_FILTER; + private boolean noAssignedLabel = false; + private boolean noAssignedUser = false; @NonNull private List<User> users = new ArrayList<>(); @NonNull private List<Label> labels = new ArrayList<>(); + @NonNull + private EArchiveStatus archiveStatus = EArchiveStatus.NON_ARCHIVED; public FilterInformation() { // Default constructor @@ -28,8 +35,12 @@ public class FilterInformation implements Serializable { public FilterInformation(@Nullable FilterInformation filterInformation) { if (filterInformation != null) { this.dueType = filterInformation.getDueType(); + this.archiveStatus = filterInformation.getArchiveStatus(); this.users.addAll(filterInformation.getUsers()); this.labels.addAll(filterInformation.getLabels()); + this.noAssignedUser = filterInformation.isNoAssignedUser(); + this.noAssignedLabel = filterInformation.isNoAssignedLabel(); + this.archiveStatus = filterInformation.getArchiveStatus(); } } @@ -47,7 +58,7 @@ public class FilterInformation implements Serializable { return users; } - @NotNull + @NonNull public List<Label> getLabels() { return labels; } @@ -68,23 +79,55 @@ public class FilterInformation implements Serializable { users.remove(user); } - @NotNull + public boolean isNoAssignedUser() { + return noAssignedUser; + } + + public void setNoAssignedUser(boolean noAssignedUser) { + this.noAssignedUser = noAssignedUser; + } + + public boolean isNoAssignedLabel() { + return noAssignedLabel; + } + + public void setNoAssignedLabel(boolean noAssignedLabel) { + this.noAssignedLabel = noAssignedLabel; + } + + public void setArchiveStatus(@NonNull EArchiveStatus archiveStatus) { + this.archiveStatus = archiveStatus; + } + + @NonNull + public EArchiveStatus getArchiveStatus() { + return archiveStatus; + } + + @NonNull @Override public String toString() { return "FilterInformation{" + "dueType=" + dueType + + ", noAssignedLabel=" + noAssignedLabel + + ", noAssignedUser=" + noAssignedUser + ", users=" + users + ", labels=" + labels + + ", archiveStatus=" + archiveStatus + '}'; } /** - * @return whether or not the given filterInformation has any actual filters set + * @return whether or not the given {@param filterInformation} has any actual filters set */ public static boolean hasActiveFilter(@Nullable FilterInformation filterInformation) { if (filterInformation == null) { return false; } - return filterInformation.getDueType() != EDueType.NO_FILTER || filterInformation.getUsers().size() > 0 || filterInformation.getLabels().size() > 0; + return filterInformation.getDueType() != EDueType.NO_FILTER + || filterInformation.getUsers().size() > 0 + || filterInformation.getLabels().size() > 0 + || filterInformation.noAssignedUser + || filterInformation.noAssignedLabel; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java index 9ff4d7945..12e8692d4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java @@ -5,8 +5,8 @@ public class Capabilities { private Version deckVersion; private Version nextcloudVersion; - private String color = "#0082c9"; - private String textColor = "#ffffff"; + private int color = 0; + private int textColor = 0; private boolean maintenanceEnabled = false; public Capabilities() { @@ -28,19 +28,19 @@ public class Capabilities { this.nextcloudVersion = nextcloudVersion; } - public String getColor() { + public int getColor() { return color; } - public void setColor(String color) { + public void setColor(int color) { this.color = color; } - public String getTextColor() { + public int getTextColor() { return textColor; } - public void setTextColor(String textColor) { + public void setTextColor(int textColor) { this.textColor = textColor; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java index 7ef97a294..cfd973831 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java @@ -6,18 +6,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; -import org.jetbrains.annotations.NotNull; - import java.util.regex.Matcher; import java.util.regex.Pattern; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Attachment; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; public class Version implements Comparable<Version> { private static final Pattern NUMBER_EXTRACTION_PATTERN = Pattern.compile("[0-9]+"); private static final Version VERSION_1_0_0 = new Version("1.0.0", 1, 0, 0); private static final Version VERSION_1_0_3 = new Version("1.0.3", 1, 0, 3); + private static final Version VERSION_1_3_0 = new Version("1.3.0", 1, 3, 0); @Nullable private static Version VERSION_MINIMUM_SUPPORTED; @@ -125,7 +125,7 @@ public class Version implements Comparable<Version> { return 0; } - @NotNull + @NonNull @Override public String toString() { return "Version{" + @@ -157,6 +157,22 @@ public class Version implements Comparable<Version> { } /** + * Before {@link #VERSION_1_3_0} all {@link Attachment}s have been stored in a special folder at the server. + * Starting with {@link #VERSION_1_3_0} {@link Attachment}s can be stored as regular files, allowing for example to make use of server side thumbnail generation. + * <p> + * Since the migration takes a long time, it does not happen on upgrading the server app but step by step via a cronjob. + * Therefore this method is just an indicator, that it is possible that {@link Attachment}s are stored as files, but it is no guarantee that all {@link Attachment}s already have been migrated to files. + * + * @return whether or not the server supports file attachments + * @see <a href="https://github.com/nextcloud/deck/pull/2638">documentation in PR</a> + */ + public boolean supportsFileAttachments() { + return false; +// TODO depends on https://github.com/nextcloud/deck/pull/2638 +// return isGreaterOrEqualTo(VERSION_1_3_0); + } + + /** * Title max length has been increased from 100 to 255 characters beginning with server {@link Version} 1.0.0 * * @return the number of characters that the title fields of cards allow @@ -167,6 +183,7 @@ public class Version implements Comparable<Version> { ? 255 : 100; } + /** * URL to view a card in the web interface has been changed in {@link Version} 1.0.0 * diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java index 707bae974..8bba9740b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java @@ -5,12 +5,13 @@ import androidx.room.ForeignKey; import androidx.room.Ignore; import androidx.room.Index; +import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.List; import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; + @Entity(inheritSuperIndices = true, indices = { @Index(value = "accountId", name = "comment_accID"), @@ -35,7 +36,7 @@ public class DeckComment extends AbstractRemoteEntity { private Long objectId; private String actorType; - private Date creationDateTime; + private Instant creationDateTime; private String actorId; private String actorDisplayName; private String message; @@ -47,7 +48,7 @@ public class DeckComment extends AbstractRemoteEntity { } @Ignore - public DeckComment(String message, String actorDisplayName, Date creationDateTime) { + public DeckComment(String message, String actorDisplayName, Instant creationDateTime) { setMessage(message); setActorDisplayName(actorDisplayName); setCreationDateTime(creationDateTime); @@ -76,11 +77,11 @@ public class DeckComment extends AbstractRemoteEntity { this.actorType = actorType; } - public Date getCreationDateTime() { + public Instant getCreationDateTime() { return creationDateTime; } - public void setCreationDateTime(Date creationDateTime) { + public void setCreationDateTime(Instant creationDateTime) { this.creationDateTime = creationDateTime; } @@ -121,8 +122,8 @@ public class DeckComment extends AbstractRemoteEntity { } public void setMessage(String message) { - if (message!= null && message.length() > MAX_MESSAGE_LENGTH) { - throw new IllegalArgumentException("The server won't accept messages longer than "+MAX_MESSAGE_LENGTH+" characters!"); + if (message != null && message.length() > MAX_MESSAGE_LENGTH) { + throw new IllegalArgumentException("The server won't accept messages longer than " + MAX_MESSAGE_LENGTH + " characters!"); } this.message = message; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java new file mode 100644 index 000000000..615c7a2a2 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java @@ -0,0 +1,66 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; + +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.interfaces.AbstractJoinEntity; + +@Entity( + primaryKeys = {"projectId", "cardId"}, + indices = {@Index("cardId"), @Index("projectId")}, + foreignKeys = { + @ForeignKey(entity = OcsProject.class, + parentColumns = "localId", + childColumns = "projectId", + onDelete = ForeignKey.CASCADE + ), + @ForeignKey(entity = Card.class, + parentColumns = "localId", + childColumns = "cardId", + onDelete = ForeignKey.CASCADE + ) + }) +public class JoinCardWithProject extends AbstractJoinEntity { + @NonNull + private Long projectId; + @NonNull + private Long cardId; + + @NonNull + public Long getProjectId() { + return projectId; + } + + public void setProjectId(@NonNull Long projectId) { + this.projectId = projectId; + } + + public Long getCardId() { + return cardId; + } + + public void setCardId(Long cardId) { + this.cardId = cardId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JoinCardWithProject that = (JoinCardWithProject) o; + + if (!projectId.equals(that.projectId)) return false; + return cardId.equals(that.cardId); + } + + @Override + public int hashCode() { + int result = projectId.hashCode(); + result = 31 * result + cardId.hashCode(); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java new file mode 100644 index 000000000..49bc7296d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java @@ -0,0 +1,45 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.Index; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; + +@Entity(inheritSuperIndices = true, + indices = { + @Index(value = "accountId", name = "index_project_accID"), + }, + foreignKeys = { + } +) +public class OcsProject extends AbstractRemoteEntity { + @NonNull + private String name; + + @Ignore + @NonNull + private ArrayList<OcsProjectResource> resources = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @NonNull + public ArrayList<OcsProjectResource> getResources() { + return resources; + } + + public void setResources(@NonNull List<OcsProjectResource> resources) { + this.resources.clear(); + this.resources.addAll(resources); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java new file mode 100644 index 000000000..af42b8432 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java @@ -0,0 +1,24 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects; + +import java.util.ArrayList; +import java.util.List; + +public class OcsProjectList { + List<OcsProject> projects; + + public OcsProjectList() { + projects = new ArrayList<>(); + } + + public OcsProjectList(List<OcsProject> projects) { + this.projects = projects; + } + + public List<OcsProject> getProjects() { + return projects; + } + + public void add(OcsProject project) { + projects.add(project); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java new file mode 100644 index 000000000..cd63ccde4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java @@ -0,0 +1,135 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.RoomWarnings; + +import java.io.Serializable; + +import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; + +@SuppressWarnings(RoomWarnings.INDEX_FROM_PARENT_IS_DROPPED) +@Entity( + indices = { + @Index(value = "id", name = "index_OcsProjectResource_id"), + @Index(value = "lastModifiedLocal", name = "index_OcsProjectResource_lastModifiedLocal"), + @Index(value = {"accountId", "id", "idString", "projectId"}, name = "index_OcsProjectResource_accountId_id", unique = true), + @Index(value = "accountId", name = "index_projectResource_accID"), + @Index(value = "projectId", name = "index_projectResource_projectId"), + }, + foreignKeys = { + @ForeignKey( + entity = OcsProject.class, + parentColumns = "localId", + childColumns = "projectId", onDelete = ForeignKey.CASCADE + ) + } +) +public class OcsProjectResource extends AbstractRemoteEntity implements Serializable { + @Nullable + private String type; + @Nullable + private String name; + @Nullable + private String link; + @Nullable + private String path; + @Nullable + private String iconUrl; + @Nullable + private String mimetype; + @Nullable + private Boolean previewAvailable; + @Nullable + private String idString; + + + @NonNull + private Long projectId; + + public Long getProjectId() { + return projectId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(@Nullable String type) { + this.type = type; + } + + @Nullable + public String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + /** + * Caution: the Link might be a full url or only the relative path! + * @return The link to the Resource + */ + @Nullable + public String getLink() { + return link; + } + + public void setLink(@Nullable String link) { + this.link = link; + } + + @Nullable + public String getIconUrl() { + return iconUrl; + } + + public void setIconUrl(@Nullable String iconUrl) { + this.iconUrl = iconUrl; + } + + @Nullable + public String getPath() { + return path; + } + + public void setPath(@Nullable String path) { + this.path = path; + } + + @Nullable + public String getMimetype() { + return mimetype; + } + + public void setMimetype(@Nullable String mimetype) { + this.mimetype = mimetype; + } + + public Boolean getPreviewAvailable() { + return Boolean.TRUE.equals(previewAvailable); + } + + public void setPreviewAvailable(@Nullable Boolean previewAvailable) { + this.previewAvailable = previewAvailable; + } + + @Nullable + public String getIdString() { + return idString; + } + + public void setIdString(@Nullable String idString) { + this.idString = idString; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java new file mode 100644 index 000000000..257cf6f60 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java @@ -0,0 +1,50 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects.full; + +import androidx.annotation.NonNull; +import androidx.room.Embedded; +import androidx.room.Relation; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; + +public class OcsProjectWithResources implements IRemoteEntity { + @Embedded + public OcsProject project; + + + @Relation(entity = OcsProjectResource.class, parentColumn = "localId", entityColumn = "projectId") + public List<OcsProjectResource> resources; + + public OcsProject getProject() { + return project; + } + + public void setProject(OcsProject project) { + this.project = project; + } + + @NonNull + public List<OcsProjectResource> getResources() { + return resources; + } + + public void setResources(List<OcsProjectResource> resources) { + this.resources = resources; + } + + public String getName() { + return project.getName(); + } + + public void setName(String name) { + project.setName(name); + } + + @Override + public IRemoteEntity getEntity() { + return project; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java new file mode 100644 index 000000000..085ca786e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java @@ -0,0 +1,27 @@ +package it.niedermann.nextcloud.deck.model.ocs.user; + +import java.util.ArrayList; +import java.util.List; + +public class GroupMemberUIDs { + private List<String> uids = new ArrayList<>(); + + public List<String> getUids() { + return uids; + } + + public void setUids(List<String> uids) { + this.uids = uids; + } + + @Override + public String toString() { + return "GroupMemberUIDs{" + + "uids=" + uids + + '}'; + } + + public void add(String uid) { + uids.add(uid); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java index a10ae005d..818d5b96e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java @@ -1,7 +1,16 @@ package it.niedermann.nextcloud.deck.model.ocs.user; import java.util.ArrayList; +import java.util.List; -public class OcsUserList extends ArrayList<String> { - // nothing. +public class OcsUserList { + private List<OcsUser> users = new ArrayList<>(); + + public List<OcsUser> getUsers() { + return users; + } + + public void addUser(OcsUser user) { + this.users.add(user); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java new file mode 100644 index 000000000..1f167bad1 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java @@ -0,0 +1,71 @@ +package it.niedermann.nextcloud.deck.model.relations; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; + +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.User; + +@Entity( + primaryKeys = {"userId", "boardId"}, + indices = {@Index("userId"), @Index("boardId"), @Index(name = "unique_idx_user_board", value = {"userId","boardId"}, unique = true)}, + foreignKeys = { + @ForeignKey(entity = User.class, + parentColumns = "localId", + childColumns = "userId", onDelete = ForeignKey.CASCADE), + @ForeignKey(entity = Board.class, + parentColumns = "localId", + childColumns = "boardId", onDelete = ForeignKey.CASCADE) + }) +public class UserInBoard { + @NonNull + private Long userId; + @NonNull + private Long boardId; + + @NonNull + public Long getUserId() { + return userId; + } + + public void setUserId(@NonNull Long userId) { + this.userId = userId; + } + + @NonNull + public Long getBoardId() { + return boardId; + } + + public void setBoardId(@NonNull Long boardId) { + this.boardId = boardId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserInBoard that = (UserInBoard) o; + + if (!userId.equals(that.userId)) return false; + return boardId.equals(that.boardId); + } + + @Override + public int hashCode() { + int result = userId.hashCode(); + result = 31 * result + boardId.hashCode(); + return result; + } + + @Override + public String toString() { + return "UserInGroup{" + + "userId=" + userId + + ", boardId=" + boardId + + '}'; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java new file mode 100644 index 000000000..48e16cce5 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java @@ -0,0 +1,70 @@ +package it.niedermann.nextcloud.deck.model.relations; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; + +import it.niedermann.nextcloud.deck.model.User; + +@Entity( + primaryKeys = {"groupId", "memberId"}, + indices = {@Index("groupId"), @Index("memberId"), @Index(name = "unique_idx_group_member", value = {"groupId","memberId"}, unique = true)}, + foreignKeys = { + @ForeignKey(entity = User.class, + parentColumns = "localId", + childColumns = "groupId", onDelete = ForeignKey.CASCADE), + @ForeignKey(entity = User.class, + parentColumns = "localId", + childColumns = "memberId", onDelete = ForeignKey.CASCADE) + }) +public class UserInGroup { + @NonNull + private Long groupId; + @NonNull + private Long memberId; + + @NonNull + public Long getGroupId() { + return groupId; + } + + public void setGroupId(@NonNull Long groupId) { + this.groupId = groupId; + } + + @NonNull + public Long getMemberId() { + return memberId; + } + + public void setMemberId(@NonNull Long memberId) { + this.memberId = memberId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserInGroup that = (UserInGroup) o; + + if (!groupId.equals(that.groupId)) return false; + return memberId.equals(that.memberId); + } + + @Override + public int hashCode() { + int result = groupId.hashCode(); + result = 31 * result + memberId.hashCode(); + return result; + } + + @Override + public String toString() { + return "UserInGroup{" + + "groupId=" + groupId + + ", memberId=" + memberId + + '}'; + } +} 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); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java index 854b99a6a..3ded31bb7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java @@ -23,6 +23,7 @@ import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGrant import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; +import com.nextcloud.android.sso.ui.UiExceptionManager; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; @@ -36,7 +37,6 @@ import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; -import it.niedermann.nextcloud.deck.util.ExceptionUtil; import static com.nextcloud.android.sso.AccountImporter.REQUEST_AUTH_TOKEN_SSO; @@ -79,7 +79,11 @@ public class ImportAccountActivity extends AppCompatActivity { try { AccountImporter.pickNewAccount(this); } catch (NextcloudFilesAppNotInstalledException e) { - ExceptionUtil.handleNextcloudFilesAppNotInstalledException(this, e); + UiExceptionManager.showDialogForException(this, e); + DeckLog.warn("============================================================="); + DeckLog.warn("Nextcloud app is not installed. Cannot choose account"); + DeckLog.logError(e); + binding.addButton.setEnabled(true); } catch (AndroidGetAccountsPermissionNotGranted e) { binding.addButton.setEnabled(true); AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this); 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 b5713909b..d7e726a7d 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 @@ -1,5 +1,6 @@ package it.niedermann.nextcloud.deck.ui; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -53,6 +54,7 @@ import java.util.Objects; import it.niedermann.android.crosstabdnd.CrossTabDragAndDrop; import it.niedermann.android.tablayouthelper.TabLayoutHelper; import it.niedermann.android.tablayouthelper.TabTitleGenerator; +import it.niedermann.nextcloud.deck.DeckApplication; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; @@ -65,6 +67,7 @@ import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.internal.FilterInformation; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.Version; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; @@ -86,6 +89,7 @@ import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; import it.niedermann.nextcloud.deck.ui.filter.FilterDialogFragment; import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel; import it.niedermann.nextcloud.deck.ui.settings.SettingsActivity; import it.niedermann.nextcloud.deck.ui.stack.DeleteStackDialogFragment; import it.niedermann.nextcloud.deck.ui.stack.DeleteStackListener; @@ -112,8 +116,8 @@ import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandTo import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.clearBrandColors; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.saveBrandColors; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficient; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficient; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ABOUT; import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ADD_BOARD; import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ARCHIVED_BOARDS; @@ -126,6 +130,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener protected MainViewModel mainViewModel; private FilterViewModel filterViewModel; + private PickStackViewModel pickStackViewModel; protected static final int ACTIVITY_ABOUT = 1; protected static final int ACTIVITY_SETTINGS = 2; @@ -133,7 +138,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @NonNull protected List<Account> accountsList = new ArrayList<>(); - protected SyncManager syncManager; protected SharedPreferences sharedPreferences; private StackAdapter stackAdapter; long lastBoardId; @@ -143,7 +147,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener private Observer<List<Board>> boardsLiveDataObserver; private Menu listMenu; - private LiveData<List<FullStack>> stacksLiveData; + private LiveData<List<Stack>> stacksLiveData; private LiveData<Boolean> hasArchivedBoardsLiveData; private Observer<Boolean> hasArchivedBoardsLiveDataObserver; @@ -178,6 +182,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener mainViewModel = new ViewModelProvider(this).get(MainViewModel.class); filterViewModel = new ViewModelProvider(this).get(FilterViewModel.class); + pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class); addList = getString(R.string.add_list); addBoard = getString(R.string.add_board); @@ -191,12 +196,11 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener toggle.syncState(); binding.navigationView.setNavigationItemSelectedListener(this); - syncManager = new SyncManager(this); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - switchMap(syncManager.hasAccounts(), hasAccounts -> { + switchMap(mainViewModel.hasAccounts(), hasAccounts -> { if (hasAccounts) { - return syncManager.readAccounts(); + return mainViewModel.readAccounts(); } else { startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); return null; @@ -220,7 +224,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { registerAutoSyncOnNetworkAvailable(); } else { - syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) { + mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) { @Override public void onResponse(Boolean response) { } @@ -240,7 +244,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener mainViewModel.getCurrentAccountLiveData().observe(this, (currentAccount) -> { SingleAccountHelper.setCurrentAccount(getApplicationContext(), mainViewModel.getCurrentAccount().getName()); - syncManager = new SyncManager(this); + mainViewModel.recreateSyncManager(); saveCurrentAccountId(this, mainViewModel.getCurrentAccount().getId()); if (mainViewModel.getCurrentAccount().isMaintenanceEnabled()) { @@ -253,7 +257,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener boardsLiveData.removeObserver(boardsLiveDataObserver); } - boardsLiveData = syncManager.getBoards(currentAccount.getId(), false); + boardsLiveData = mainViewModel.getBoards(currentAccount.getId(), false); boardsLiveDataObserver = (boards) -> { if (boards == null) { throw new IllegalStateException("List<Board> boards must not be null."); @@ -273,15 +277,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener if (!currentBoardIdWasInList) { setCurrentBoard(boardsList.get(0)); } + + binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); } else { clearBrandColors(this); clearCurrentBoard(); + + binding.filter.setOnClickListener(null); } if (hasArchivedBoardsLiveData != null && hasArchivedBoardsLiveDataObserver != null) { hasArchivedBoardsLiveData.removeObserver(hasArchivedBoardsLiveDataObserver); } - hasArchivedBoardsLiveData = syncManager.hasArchivedBoards(currentAccount.getId()); + hasArchivedBoardsLiveData = mainViewModel.hasArchivedBoards(currentAccount.getId()); hasArchivedBoardsLiveDataObserver = (hasArchivedBoards) -> { mainViewModel.setCurrentAccountHasArchivedBoards(Boolean.TRUE.equals(hasArchivedBoards)); inflateBoardMenu(); @@ -320,7 +328,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener CrossTabDragAndDrop<StackFragment, CardAdapter, FullCard> dragAndDrop = new CrossTabDragAndDrop<>(getResources(), ViewCompat.getLayoutDirection(binding.getRoot()) == ViewCompat.LAYOUT_DIRECTION_LTR); dragAndDrop.register(binding.viewPager, binding.stackTitles, getSupportFragmentManager()); dragAndDrop.addItemMovedByDragListener((movedCard, stackId, position) -> { - syncManager.reorder(mainViewModel.getCurrentAccount().getId(), movedCard, stackId, position); + mainViewModel.reorder(mainViewModel.getCurrentAccount().getId(), movedCard, stackId, position); DeckLog.info("Card \"" + movedCard.getCard().getTitle() + "\" was moved to Stack " + stackId + " on position " + position); }); @@ -374,8 +382,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener }); filterViewModel.getFilterInformation().observe(this, (info) -> binding.filterIndicator.setVisibility(filterViewModel.getFilterInformation().getValue() == null ? View.GONE : View.VISIBLE)); - - binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); binding.archivedCards.setOnClickListener((v) -> startActivity(ArchivedCardsActvitiy.createIntent(this, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), mainViewModel.currentBoardHasEditPermission()))); @@ -395,7 +401,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener } } else DeckLog.warn("ConnectivityManager is null"); refreshCapabilities(mainViewModel.getCurrentAccount()); - syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) { + mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) { @Override public void onResponse(Boolean response) { runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false)); @@ -422,7 +428,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener applyBrandToPrimaryTabLayout(mainColor, binding.stackTitles); applyBrandToFAB(mainColor, binding.fab); // TODO We assume, that the background of the spinner is always white - binding.swipeRefreshLayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : colorAccent); + binding.swipeRefreshLayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : DeckApplication.isDarkTheme(this) ? Color.DKGRAY : colorAccent); headerBinding.headerView.setBackgroundColor(mainColor); @ColorInt final int headerTextColor = contrastRatioIsSufficientBigAreas(mainColor, Color.WHITE) ? Color.WHITE : Color.BLACK; DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor); @@ -440,62 +446,49 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onCreateStack(String stackName) { - // TODO this outer call is only necessary to get the highest order. Move logic to SyncManager. - observeOnce(syncManager.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), MainActivity.this, fullStacks -> { - final Stack s = new Stack(stackName, mainViewModel.getCurrentBoardLocalId()); - int heighestOrder = 0; - for (FullStack fullStack : fullStacks) { - int currentStackOrder = fullStack.stack.getOrder(); - if (currentStackOrder >= heighestOrder) { - heighestOrder = currentStackOrder + 1; - } + DeckLog.info("Create Stack in account " + mainViewModel.getCurrentAccount().getName() + " on board " + mainViewModel.getCurrentBoardLocalId()); + WrappedLiveData<FullStack> createLiveData = mainViewModel.createStack(mainViewModel.getCurrentAccount().getId(), stackName, mainViewModel.getCurrentBoardLocalId()); + observeOnce(createLiveData, this, (fullStack) -> { + if (createLiveData.hasError()) { + final Throwable error = createLiveData.getError(); + assert error != null; + BrandedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(error.getLocalizedMessage()), Snackbar.LENGTH_LONG) + .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(error, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) + .show(); + } else { + binding.viewPager.setCurrentItem(stackAdapter.getItemCount()); } - s.setOrder(heighestOrder); - DeckLog.info("Create Stack in account " + mainViewModel.getCurrentAccount().getName() + " on board " + mainViewModel.getCurrentBoardLocalId()); - WrappedLiveData<FullStack> createLiveData = syncManager.createStack(mainViewModel.getCurrentAccount().getId(), s); - observeOnce(createLiveData, this, (fullStack) -> { - if (createLiveData.hasError()) { - final Throwable error = createLiveData.getError(); - assert error != null; - BrandedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(error.getLocalizedMessage()), Snackbar.LENGTH_LONG) - .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(error, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) - .show(); - } else { - binding.viewPager.setCurrentItem(stackAdapter.getItemCount()); - } - }); }); } @Override public void onUpdateStack(long localStackId, String stackName) { - observeOnce(syncManager.getStack(mainViewModel.getCurrentAccount().getId(), localStackId), MainActivity.this, fullStack -> { - fullStack.getStack().setTitle(stackName); - final WrappedLiveData<FullStack> archiveLiveData = syncManager.updateStack(fullStack); - observeOnce(archiveLiveData, this, (v) -> { - if (archiveLiveData.hasError()) { - ExceptionDialogFragment.newInstance(archiveLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } - }); + final WrappedLiveData<FullStack> liveData = mainViewModel.updateStackTitle(localStackId, stackName); + observeOnce(liveData, this, (v) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } }); } @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 = mainViewModel.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()); @@ -508,11 +501,16 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onUpdateBoard(FullBoard fullBoard) { - syncManager.updateBoard(fullBoard); + final WrappedLiveData<FullBoard> updateLiveData = mainViewModel.updateBoard(fullBoard); + observeOnce(updateLiveData, this, (next) -> { + if (updateLiveData.hasError()) { + ExceptionDialogFragment.newInstance(updateLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } private void refreshCapabilities(final Account account) { - syncManager.refreshCapabilities(new IResponseCallback<Capabilities>(account) { + mainViewModel.refreshCapabilities(new IResponseCallback<Capabilities>(account) { @Override public void onResponse(Capabilities response) { if (response.isMaintenanceEnabled()) { @@ -548,7 +546,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener if (stacksLiveData != null) { stacksLiveData.removeObservers(this); } - saveBrandColors(this, Color.parseColor('#' + board.getColor())); + saveBrandColors(this, board.getColor()); mainViewModel.setCurrentBoard(board); filterViewModel.clearFilterInformation(); @@ -569,12 +567,12 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener binding.emptyContentViewBoards.setVisibility(View.GONE); binding.swipeRefreshLayout.setVisibility(View.VISIBLE); - stacksLiveData = syncManager.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), board.getLocalId()); - stacksLiveData.observe(this, (List<FullStack> fullStacks) -> { - if (fullStacks == null) { + stacksLiveData = mainViewModel.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), board.getLocalId()); + stacksLiveData.observe(this, (List<Stack> stacks) -> { + if (stacks == null) { throw new IllegalStateException("Given List<FullStack> must not be null"); } - currentBoardStacksCount = fullStacks.size(); + currentBoardStacksCount = stacks.size(); if (currentBoardStacksCount == 0) { binding.emptyContentViewStacks.setVisibility(View.VISIBLE); @@ -586,19 +584,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener listMenu.findItem(R.id.archive_cards).setVisible(currentBoardHasStacks); int stackPositionInAdapter = 0; - stackAdapter.setStacks(fullStacks); + stackAdapter.setStacks(stacks); long currentStackId = readCurrentStackId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()); for (int i = 0; i < currentBoardStacksCount; i++) { - if (fullStacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) { + if (stacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) { stackPositionInAdapter = i; break; } } final int stackPositionInAdapterClone = stackPositionInAdapter; final TabTitleGenerator tabTitleGenerator = position -> { - if (fullStacks.size() > position) { - return fullStacks.get(position).getStack().getTitle(); + if (stacks.size() > position) { + return stacks.get(position).getTitle(); } else { DeckLog.logError(new IllegalStateException("Could not generate tab title for position " + position + " because list size is only " + currentBoardStacksCount)); return "ERROR"; @@ -671,71 +669,67 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.archive_cards: { - final FullStack fullStack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); - final long stackLocalId = fullStack.getLocalId(); - observeOnce(syncManager.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId), MainActivity.this, (numberOfCards) -> { - new BrandedAlertDialogBuilder(this) - .setTitle(R.string.archive_cards) - .setMessage(getString(R.string.do_you_want_to_archive_all_cards_of_the_list, fullStack.getStack().getTitle())) - .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> { - final WrappedLiveData<Void> archiveStackLiveData = syncManager.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId); - observeOnce(archiveStackLiveData, this, (result) -> { - if (archiveStackLiveData.hasError()) { - ExceptionDialogFragment.newInstance(archiveStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } - }); - }) - .setNeutralButton(android.R.string.cancel, null) - .create() - .show(); - }); - return true; - } - case R.id.add_list: { - EditStackDialogFragment.newInstance(NO_STACK_ID).show(getSupportFragmentManager(), addList); - return true; - } - case R.id.rename_list: { - final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); - observeOnce(syncManager.getStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, fullStack -> - EditStackDialogFragment.newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle()) - .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); - return true; - } - case R.id.move_list_left: { - final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); - // TODO error handling - final int stackLeftPosition = binding.viewPager.getCurrentItem() - 1; - final long stackLeftId = stackAdapter.getItem(stackLeftPosition).getLocalId(); - syncManager.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackLeftId)); - stackMoved = true; - return true; - } - case R.id.move_list_right: { - final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); - // TODO error handling - final int stackRightPosition = binding.viewPager.getCurrentItem() + 1; - final long stackRightId = stackAdapter.getItem(stackRightPosition).getLocalId(); - syncManager.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackRightId)); - stackMoved = true; - return true; - } - case R.id.delete_list: { - final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); - observeOnce(syncManager.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, (numberOfCards) -> { - if (numberOfCards != null && numberOfCards > 0) { - DeleteStackDialogFragment.newInstance(stackId, numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName()); - } else { - onStackDeleted(stackId); - } - }); - return true; - } - default: - return super.onOptionsItemSelected(item); + int itemId = item.getItemId(); + if (itemId == R.id.archive_cards) { + final Stack stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); + final long stackLocalId = stack.getLocalId(); + observeOnce(mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId), MainActivity.this, (numberOfCards) -> { + new BrandedAlertDialogBuilder(this) + .setTitle(R.string.archive_cards) + .setMessage(getString(FilterInformation.hasActiveFilter(filterViewModel.getFilterInformation().getValue()) + ? R.string.do_you_want_to_archive_all_cards_of_the_filtered_list + : R.string.do_you_want_to_archive_all_cards_of_the_list, stack.getTitle())) + .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> { + final FilterInformation filterInformation = filterViewModel.getFilterInformation().getValue(); + final WrappedLiveData<Void> archiveStackLiveData = mainViewModel.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation); + observeOnce(archiveStackLiveData, this, (result) -> { + if (archiveStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(archiveStackLiveData.getError())) { + ExceptionDialogFragment.newInstance(archiveStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + }) + .setNeutralButton(android.R.string.cancel, null) + .create() + .show(); + }); + return true; + } else if (itemId == R.id.add_list) { + EditStackDialogFragment.newInstance(NO_STACK_ID).show(getSupportFragmentManager(), addList); + return true; + } else if (itemId == R.id.rename_list) { + final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); + observeOnce(mainViewModel.getStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, fullStack -> + EditStackDialogFragment.newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle()) + .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); + return true; + } else if (itemId == R.id.move_list_left) { + final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); + // TODO error handling + final int stackLeftPosition = binding.viewPager.getCurrentItem() - 1; + final long stackLeftId = stackAdapter.getItem(stackLeftPosition).getLocalId(); + mainViewModel.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackLeftId)); + stackMoved = true; + return true; + } else if (itemId == R.id.move_list_right) { + final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); + // TODO error handling + final int stackRightPosition = binding.viewPager.getCurrentItem() + 1; + final long stackRightId = stackAdapter.getItem(stackRightPosition).getLocalId(); + mainViewModel.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackRightId)); + stackMoved = true; + return true; + } else if (itemId == R.id.delete_list) { + final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); + observeOnce(mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, (numberOfCards) -> { + if (numberOfCards != null && numberOfCards > 0) { + DeleteStackDialogFragment.newInstance(stackId, numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName()); + } else { + onStackDeleted(stackId); + } + }); + return true; } + return super.onOptionsItemSelected(item); } protected void showFabIfEditPermissionGranted() { @@ -780,7 +774,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener default: try { AccountImporter.onActivityResult(requestCode, resultCode, data, this, (account) -> { - final WrappedLiveData<Account> accountLiveData = this.syncManager.createAccount(new Account(account.name, account.userId, account.url)); + final WrappedLiveData<Account> accountLiveData = mainViewModel.createAccount(new Account(account.name, account.userId, account.url)); accountLiveData.observe(this, (createdAccount) -> { if (!accountLiveData.hasError()) { if (createdAccount == null) { @@ -789,12 +783,13 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener final SyncManager importSyncManager = new SyncManager(this, account.name); importSyncManager.refreshCapabilities(new IResponseCallback<Capabilities>(createdAccount) { + @SuppressLint("StringFormatInvalid") @Override public void onResponse(Capabilities response) { if (!response.isMaintenanceEnabled()) { if (response.getDeckVersion().isSupported(getApplicationContext())) { runOnUiThread(() -> { - syncManager = importSyncManager; + mainViewModel.setSyncManager(importSyncManager); mainViewModel.setCurrentAccount(account); final Snackbar importSnackbar = BrandedSnackbar.make(binding.coordinatorLayout, R.string.account_is_getting_imported, Snackbar.LENGTH_INDEFINITE); @@ -827,7 +822,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener startActivity(openURL); finish(); }).show()); - syncManager.deleteAccount(createdAccount.getId()); + mainViewModel.deleteAccount(createdAccount.getId()); } } else { DeckLog.warn("Cannot import account because server version is currently in maintenance mode."); @@ -836,14 +831,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener .setMessage(getString(R.string.maintenance_mode_explanation, createdAccount.getUrl())) .setPositiveButton(R.string.simple_close, null) .show()); - syncManager.deleteAccount(createdAccount.getId()); + mainViewModel.deleteAccount(createdAccount.getId()); } } @Override public void onError(Throwable throwable) { super.onError(throwable); - syncManager.deleteAccount(createdAccount.getId()); + mainViewModel.deleteAccount(createdAccount.getId()); if (throwable instanceof OfflineException) { DeckLog.warn("Cannot import account because device is currently offline."); runOnUiThread(() -> new BrandedAlertDialogBuilder(MainActivity.this) @@ -893,7 +888,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onAvailable(@NonNull Network network) { DeckLog.log("Got Network connection"); - syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) { + mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) { @Override public void onResponse(Boolean response) { DeckLog.log("Auto-Sync after connection available successful"); @@ -924,9 +919,9 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onStackDeleted(Long stackLocalId) { long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); - final WrappedLiveData<Void> deleteStackLiveData = syncManager.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId()); + final WrappedLiveData<Void> deleteStackLiveData = mainViewModel.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId()); observeOnce(deleteStackLiveData, this, (v) -> { - if (deleteStackLiveData.hasError()) { + if (deleteStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteStackLiveData.getError())) { ExceptionDialogFragment.newInstance(deleteStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } }); @@ -946,7 +941,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard); } } - syncManager.deleteBoard(board); + + final WrappedLiveData<Void> deleteLiveData = mainViewModel.deleteBoard(board); + observeOnce(deleteLiveData, this, (next) -> { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + binding.drawerLayout.closeDrawer(GravityCompat.START); } @@ -966,6 +968,39 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onArchive(@NonNull Board board) { - syncManager.archiveBoard(board); + final WrappedLiveData<FullBoard> liveData = mainViewModel.archiveBoard(board); + observeOnce(liveData, this, (fullBoard) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + } + + @Override + public void onClone(Board board) { + final String[] animals = {getString(R.string.clone_cards)}; + final boolean[] checkedItems = {false}; + new BrandedAlertDialogBuilder(this) + .setTitle(R.string.clone_board) + .setMultiChoiceItems(animals, checkedItems, (dialog, which, isChecked) -> checkedItems[0] = isChecked) + .setPositiveButton(R.string.simple_clone, (dialog, which) -> { + binding.drawerLayout.closeDrawer(GravityCompat.START); + final Snackbar snackbar = BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.cloning_board, board.getTitle()), Snackbar.LENGTH_INDEFINITE); + snackbar.show(); + final WrappedLiveData<FullBoard> liveData = mainViewModel.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0]); + observeOnce(liveData, this, (fullBoard -> { + snackbar.dismiss(); + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } else { + setCurrentBoard(fullBoard.getBoard()); + BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.successfully_cloned_board, fullBoard.getBoard().getTitle()), Snackbar.LENGTH_LONG) + .setAction(R.string.edit, v -> EditBoardDialogFragment.newInstance(fullBoard.getLocalId()).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName())) + .show(); + } + })); + }) + .setNeutralButton(android.R.string.cancel, null) + .show(); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java index e5bd4f482..ac244b88e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java @@ -2,19 +2,42 @@ package it.niedermann.nextcloud.deck.ui; import android.app.Application; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import java.io.File; +import java.util.List; + +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.Attachment; import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Card; +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.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.internal.FilterInformation; +import it.niedermann.nextcloud.deck.model.ocs.Capabilities; +import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; @SuppressWarnings("WeakerAccess") public class MainViewModel extends AndroidViewModel { - private MutableLiveData<Account> currentAccount = new MutableLiveData<>(); + private SyncManager syncManager; + + private final MutableLiveData<Account> currentAccount = new MutableLiveData<>(); + @Nullable private Board currentBoard; private boolean currentAccountHasArchivedBoards = false; @@ -22,6 +45,7 @@ public class MainViewModel extends AndroidViewModel { public MainViewModel(@NonNull Application application) { super(application); + this.syncManager = new SyncManager(application); } public Account getCurrentAccount() { @@ -37,16 +61,21 @@ public class MainViewModel extends AndroidViewModel { this.currentAccountIsSupportedVersion = currentAccount.getServerDeckVersionAsObject().isSupported(getApplication().getApplicationContext()); } - public void setCurrentBoard(Board currentBoard) { + public void setCurrentBoard(@NonNull Board currentBoard) { this.currentBoard = currentBoard; } public Long getCurrentBoardLocalId() { + if (currentBoard == null) { + throw new IllegalStateException("getCurrentBoardLocalId() called before setCurrentBoard()"); + } return this.currentBoard.getLocalId(); } - @Nullable public Long getCurrentBoardRemoteId() { + if (currentBoard == null) { + throw new IllegalStateException("getCurrentBoardRemoteId() called before setCurrentBoard()"); + } return this.currentBoard.getId(); } @@ -65,4 +94,192 @@ public class MainViewModel extends AndroidViewModel { public boolean isCurrentAccountIsSupportedVersion() { return currentAccountIsSupportedVersion; } + + public void recreateSyncManager() { + this.syncManager = new SyncManager(getApplication()); + } + + public void setSyncManager(@NonNull SyncManager syncManager) { + this.syncManager = syncManager; + } + + public void synchronize(@NonNull IResponseCallback<Boolean> responseCallback) { + syncManager.synchronize(responseCallback); + } + + public void refreshCapabilities(@NonNull IResponseCallback<Capabilities> callback) { + syncManager.refreshCapabilities(callback); + } + + public LiveData<Boolean> hasAccounts() { + return syncManager.hasAccounts(); + } + + public WrappedLiveData<Account> createAccount(@NonNull Account accout) { + return syncManager.createAccount(accout); + } + + public void deleteAccount(long id) { + syncManager.deleteAccount(id); + } + + public LiveData<List<Account>> readAccounts() { + return syncManager.readAccounts(); + } + + public WrappedLiveData<FullBoard> createBoard(long accountId, @NonNull Board board) { + return syncManager.createBoard(accountId, board); + } + + public WrappedLiveData<FullBoard> updateBoard(@NonNull FullBoard board) { + return syncManager.updateBoard(board); + } + + public LiveData<List<Board>> getBoards(long accountId, boolean archived) { + return syncManager.getBoards(accountId, archived); + } + + public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { + return syncManager.getFullBoardById(accountId, localId); + } + + public WrappedLiveData<FullBoard> archiveBoard(@NonNull Board board) { + return syncManager.archiveBoard(board); + } + + public WrappedLiveData<FullBoard> dearchiveBoard(@NonNull Board board) { + return syncManager.dearchiveBoard(board); + } + + public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards) { + return syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards); + } + + public WrappedLiveData<Void> deleteBoard(@NonNull Board board) { + return syncManager.deleteBoard(board); + } + + public LiveData<Boolean> hasArchivedBoards(long accountId) { + return syncManager.hasArchivedBoards(accountId); + } + + public WrappedLiveData<AccessControl> createAccessControl(long accountId, AccessControl entity) { + return syncManager.createAccessControl(accountId, entity); + } + + public WrappedLiveData<AccessControl> updateAccessControl(@NonNull AccessControl entity) { + return syncManager.updateAccessControl(entity); + } + + public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) { + return syncManager.getAccessControlByLocalBoardId(accountId, id); + } + + public WrappedLiveData<Void> deleteAccessControl(@NonNull AccessControl entity) { + return syncManager.deleteAccessControl(entity); + } + + public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) { + return syncManager.createLabel(accountId, label, localBoardId); + } + + public LiveData<Integer> countCardsWithLabel(long localLabelId) { + return syncManager.countCardsWithLabel(localLabelId); + } + + public WrappedLiveData<Label> updateLabel(@NonNull Label label) { + return syncManager.updateLabel(label); + } + + public WrappedLiveData<Void> deleteLabel(@NonNull Label label) { + return syncManager.deleteLabel(label); + } + + public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { + return syncManager.getStacksForBoard(accountId, localBoardId); + } + + public WrappedLiveData<FullStack> createStack(long accountId, @NonNull String title, long boardLocalId) { + return syncManager.createStack(accountId, title, boardLocalId); + } + + public LiveData<FullStack> getStack(long accountId, long localStackId) { + return syncManager.getStack(accountId, localStackId); + } + + public void swapStackOrder(long accountId, long boardLocalId, @NonNull Pair<Long, Long> stackLocalIds) { + syncManager.swapStackOrder(accountId, boardLocalId, stackLocalIds); + } + + public WrappedLiveData<FullStack> updateStackTitle(long localStackId, @NonNull String newTitle) { + return syncManager.updateStackTitle(localStackId, newTitle); + } + + public WrappedLiveData<Void> deleteStack(long accountId, long stackLocalId, long boardLocalId) { + return syncManager.deleteStack(accountId, stackLocalId, boardLocalId); + } + + public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) { + syncManager.reorder(accountId, movedCard, newStackId, newIndex); + } + + public LiveData<Integer> countCardsInStack(long accountId, long localStackId) { + return syncManager.countCardsInStack(accountId, localStackId); + } + + public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation) { + return syncManager.archiveCardsInStack(accountId, stackLocalId, filterInformation); + } + + public WrappedLiveData<FullCard> updateCard(@NonNull FullCard fullCard) { + return syncManager.updateCard(fullCard); + } + + public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) { + syncManager.addCommentToCard(accountId, cardId, comment); + } + + public WrappedLiveData<Attachment> addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file) { + return syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file); + } + + public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) { + syncManager.addOrUpdateSingleCardWidget(widgetId, accountId, boardId, localCardId); + } + + public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { + return syncManager.getFullCardsForStack(accountId, localStackId, filter); + } + + public WrappedLiveData<Void> moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) { + return syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId); + } + + public LiveData<List<FullCard>> getArchivedFullCardsForBoard(long accountId, long localBoardId) { + return syncManager.getArchivedFullCardsForBoard(accountId, localBoardId); + } + + public void assignUserToCard(@NonNull User user, @NonNull Card card) { + syncManager.assignUserToCard(user, card); + } + + public void unassignUserFromCard(@NonNull User user, @NonNull Card card) { + syncManager.unassignUserFromCard(user, card); + } + + public User getUserByUidDirectly(long accountId, String uid) { + return syncManager.getUserByUidDirectly(accountId, uid); + } + + public WrappedLiveData<FullCard> archiveCard(@NonNull FullCard card) { + return syncManager.archiveCard(card); + } + + public WrappedLiveData<FullCard> dearchiveCard(@NonNull FullCard card) { + return syncManager.dearchiveCard(card); + } + + public WrappedLiveData<Void> deleteCard(@NonNull Card card) { + return syncManager.deleteCard(card); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java new file mode 100644 index 000000000..2339a8783 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java @@ -0,0 +1,115 @@ +package it.niedermann.nextcloud.deck.ui; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Bundle; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.lifecycle.ViewModelProvider; + +import java.util.List; + +import it.niedermann.android.util.ColorUtil; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ActivityPickStackBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.ui.branding.Branded; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackFragment; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel; + +import static androidx.lifecycle.Transformations.switchMap; +import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; +import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; +import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; + +public abstract class PickStackActivity extends AppCompatActivity implements Branded, PickStackListener { + + protected ActivityPickStackBinding binding; + protected PickStackViewModel viewModel; + + private boolean brandingEnabled; + + private Account selectedAccount; + private Board selectedBoard; + private Stack selectedStack; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); + + brandingEnabled = isBrandingEnabled(this); + + binding = ActivityPickStackBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(PickStackViewModel.class); + + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + + switchMap(viewModel.hasAccounts(), hasAccounts -> { + if (hasAccounts) { + return viewModel.readAccounts(); + } else { + startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + return null; + } + }).observe(this, (List<Account> accounts) -> { + if (accounts == null || accounts.size() == 0) { + throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry"); + } + getSupportFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, PickStackFragment.newInstance(showBoardsWithoutEditPermission())) + .commit(); + }); + binding.cancel.setOnClickListener((v) -> finish()); + binding.submit.setOnClickListener((v) -> onSubmit(selectedAccount, selectedBoard.getLocalId(), selectedStack.getLocalId())); + } + + @Override + public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) { + this.selectedAccount = account; + this.selectedBoard = board; + this.selectedStack = stack; + if (board == null) { + binding.submit.setEnabled(false); + } else { + applyBrand(board.getColor()); + binding.submit.setEnabled(stack != null); + } + } + + @Override + public void applyBrand(int mainColor) { + try { + if (brandingEnabled) { + @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(this, R.color.primary)) + ? mainColor + : isDarkTheme(this) ? Color.WHITE : Color.BLACK; + DrawableCompat.setTintList(binding.submit.getBackground(), ColorStateList.valueOf(finalMainColor)); + binding.submit.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor)); + binding.cancel.setTextColor(getSecondaryForegroundColorDependingOnTheme(this, mainColor)); + } + } catch (Throwable t) { + DeckLog.logError(t); + } + } + + abstract protected void onSubmit(Account account, long boardId, long stackId); + + abstract protected boolean showBoardsWithoutEditPermission(); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java index b0b0d68ae..cdc20ed50 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java @@ -5,26 +5,28 @@ import android.net.Uri; import android.text.TextUtils; import android.view.View; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProvider; -import com.nextcloud.android.sso.helper.SingleAccountHelper; - +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.ActivityPushNotificationBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.card.EditActivity; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.util.ProjectUtil; -import static android.graphics.Color.parseColor; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; public class PushNotificationActivity extends AppCompatActivity { private ActivityPushNotificationBinding binding; + private PushNotificationViewModel viewModel; // Provided by Files app NotificationJob private static final String KEY_SUBJECT = "subject"; @@ -44,6 +46,8 @@ public class PushNotificationActivity extends AppCompatActivity { } binding = ActivityPushNotificationBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(PushNotificationViewModel.class); + setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); @@ -56,62 +60,105 @@ public class PushNotificationActivity extends AppCompatActivity { } final String link = getIntent().getStringExtra(KEY_LINK); + long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link); binding.cancel.setOnClickListener((v) -> finish()); - final SyncManager accountReadingSyncManager = new SyncManager(this); final String cardRemoteIdString = getIntent().getStringExtra(KEY_CARD_REMOTE_ID); final String accountString = getIntent().getStringExtra(KEY_ACCOUNT); DeckLog.verbose("cardRemoteIdString = " + cardRemoteIdString); - if (cardRemoteIdString != null) { - try { - final int cardRemoteId = Integer.parseInt(cardRemoteIdString); - observeOnce(accountReadingSyncManager.readAccount(accountString), this, (account -> { - if (account != null) { - SingleAccountHelper.setCurrentAccount(this, account.getName()); - final SyncManager syncManager = new SyncManager(this); - DeckLog.verbose("account: " + account); - observeOnce(syncManager.getLocalBoardIdByCardRemoteIdAndAccount(cardRemoteId, account), PushNotificationActivity.this, (boardLocalId -> { - DeckLog.verbose("BoardLocalId " + boardLocalId); - if (boardLocalId != null) { - observeOnce(syncManager.synchronizeCardByRemoteId(cardRemoteId, account), PushNotificationActivity.this, (fullCard -> { - DeckLog.verbose("FullCard: " + fullCard); - if (fullCard != null) { - runOnUiThread(() -> { - binding.submit.setOnClickListener((v) -> launchEditActivity(account, boardLocalId, fullCard.getLocalId())); - binding.submit.setText(R.string.simple_open); - applyBrandToSubmitButton(account); - binding.submit.setEnabled(true); - binding.progress.setVisibility(View.INVISIBLE); - }); - } else { - DeckLog.warn("Something went wrong while synchronizing the card " + cardRemoteId + " (cardRemoteId). Given fullCard is null."); - applyBrandToSubmitButton(account); - fallbackToBrowser(link); - } - })); - } else { - DeckLog.warn("Given localBoardId for cardRemoteId " + cardRemoteId + " is null."); - applyBrandToSubmitButton(account); - fallbackToBrowser(link); - } - })); - } else { - DeckLog.warn("Given account for " + accountString + " is null."); - fallbackToBrowser(link); - } - })); - } catch (NumberFormatException e) { - DeckLog.logError(e); + if (ids.length == 2) { + if (cardRemoteIdString != null) { + try { + final int cardRemoteId = Integer.parseInt(cardRemoteIdString); + observeOnce(viewModel.readAccount(accountString), this, (account -> { + if (account != null) { + viewModel.setAccount(account.getName()); + DeckLog.verbose("account: " + account); + observeOnce(viewModel.getBoardByRemoteId(account.getId(), ids[0]), PushNotificationActivity.this, (board -> { + DeckLog.verbose("BoardLocalId " + board); + if (board != null) { + observeOnce(viewModel.getCardByRemoteID(account.getId(), cardRemoteId), PushNotificationActivity.this, (card -> { + DeckLog.verbose("Card: " + card); + if (card != null) { + viewModel.synchronizeCard(new IResponseCallback<Boolean>(account) { + @Override + public void onResponse(Boolean response) { + openCardOnSubmit(account, board.getLocalId(), card.getLocalId()); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + openCardOnSubmit(account, board.getLocalId(), card.getLocalId()); + } + }, card); + } else { + DeckLog.info("Card is not yet available locally. Synchronize board with localId " + board); + + viewModel.synchronizeBoard(new IResponseCallback<Boolean>(account) { + @Override + public void onResponse(Boolean response) { + runOnUiThread(() -> { + observeOnce(viewModel.getCardByRemoteID(account.getId(), cardRemoteId), PushNotificationActivity.this, (card -> { + DeckLog.verbose("Card: " + card); + if (card != null) { + openCardOnSubmit(account, board.getLocalId(), card.getLocalId()); + } else { + DeckLog.warn("Something went wrong while synchronizing the card " + cardRemoteId + " (cardRemoteId). Given fullCard is null."); + applyBrandToSubmitButton(account); + fallbackToBrowser(link); + } + })); + }); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + DeckLog.warn("Something went wrong while synchronizing the board with localId " + board + "."); + applyBrandToSubmitButton(account); + fallbackToBrowser(link); + } + }, board.getLocalId()); + } + })); + } else { + DeckLog.warn("Given localBoardId for cardRemoteId " + cardRemoteId + " is null."); + applyBrandToSubmitButton(account); + fallbackToBrowser(link); + } + })); + } else { + DeckLog.warn("Given account for " + accountString + " is null."); + fallbackToBrowser(link); + } + })); + } catch (NumberFormatException e) { + DeckLog.logError(e); + fallbackToBrowser(link); + } + } else { + DeckLog.warn(KEY_CARD_REMOTE_ID + " is null."); fallbackToBrowser(link); } } else { - DeckLog.warn(KEY_CARD_REMOTE_ID + " is null."); + DeckLog.warn("Link does not contain two IDs (expected one board id and one card id): " + link); fallbackToBrowser(link); } } + private void openCardOnSubmit(@NonNull Account account, long boardLocalId, long cardlocalId) { + runOnUiThread(() -> { + binding.submit.setOnClickListener((v) -> launchEditActivity(account, boardLocalId, cardlocalId)); + binding.submit.setText(R.string.simple_open); + applyBrandToSubmitButton(account); + binding.submit.setEnabled(true); + binding.progress.setVisibility(View.INVISIBLE); + }); + } + /** * If anything goes wrong and we cannot open the card directly, we fall back to open the given link in the webbrowser */ @@ -146,10 +193,13 @@ public class PushNotificationActivity extends AppCompatActivity { return true; } + // TODO implement Branded interface + // TODO apply branding based on board color public void applyBrandToSubmitButton(@NonNull Account account) { + @ColorInt final int mainColor = account.getColor(); try { - binding.submit.setBackgroundColor(parseColor(account.getColor())); - binding.submit.setTextColor(parseColor(account.getTextColor())); + binding.submit.setBackgroundColor(mainColor); + binding.submit.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(mainColor)); } catch (Throwable t) { DeckLog.logError(t); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java new file mode 100644 index 000000000..d15b412f4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java @@ -0,0 +1,52 @@ +package it.niedermann.nextcloud.deck.ui; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.nextcloud.android.sso.helper.SingleAccountHelper; + +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; + +public class PushNotificationViewModel extends AndroidViewModel { + + private final SyncManager readAccountSyncManager; + private SyncManager accountSpecificSyncManager; + + public PushNotificationViewModel(@NonNull Application application) { + super(application); + this.readAccountSyncManager = new SyncManager(application); + } + + public LiveData<Account> readAccount(@Nullable String name) { + return readAccountSyncManager.readAccount(name); + } + + public void setAccount(@NonNull String accountName) { + SingleAccountHelper.setCurrentAccount(getApplication(), accountName); + accountSpecificSyncManager = new SyncManager(getApplication()); + } + + public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { + return accountSpecificSyncManager.getBoardByRemoteId(accountId, remoteId); + } + + public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) { + return accountSpecificSyncManager.getCardByRemoteID(accountId, remoteId); + } + + public void synchronizeCard(@NonNull IResponseCallback<Boolean> responseCallback, Card card) { + accountSpecificSyncManager.synchronizeCard(responseCallback, card); + } + + public void synchronizeBoard(@NonNull IResponseCallback<Boolean> responseCallback, long localBoadId) { + accountSpecificSyncManager.synchronizeBoard(responseCallback, localBoadId); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java index c00ff212a..0bd92bc78 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java @@ -14,13 +14,13 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.FragmentAboutLicenseTabBinding; import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; -import it.niedermann.nextcloud.deck.util.ColorUtil; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; import static it.niedermann.nextcloud.deck.util.SpannableUtil.setTextWithURL; public class AboutFragmentLicenseTab extends BrandedFragment { @@ -42,6 +42,6 @@ public class AboutFragmentLicenseTab extends BrandedFragment { ? mainColor : isDarkTheme(requireContext()) ? Color.WHITE : Color.BLACK; DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(finalMainColor)); - binding.aboutAppLicenseButton.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor)); + binding.aboutAppLicenseButton.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor)); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java index 592f2e8cc..744498c4a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java @@ -1,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.accountswitcher; import android.app.Dialog; -import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -16,43 +15,37 @@ import com.bumptech.glide.request.RequestOptions; import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; +import com.nextcloud.android.sso.ui.UiExceptionManager; +import it.niedermann.android.util.DimensionUtil; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogAccountSwitcherBinding; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; import it.niedermann.nextcloud.deck.ui.manageaccounts.ManageAccountsActivity; -import it.niedermann.nextcloud.deck.util.ExceptionUtil; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.MainActivity.ACTIVITY_MANAGE_ACCOUNTS; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; public class AccountSwitcherDialog extends BrandedDialogFragment { private AccountSwitcherAdapter adapter; - private SyncManager syncManager; private DialogAccountSwitcherBinding binding; private MainViewModel viewModel; - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); - syncManager = new SyncManager(requireActivity()); - } - @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater()); + viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); + binding.accountName.setText(viewModel.getCurrentAccount().getUserName()); binding.accountHost.setText(Uri.parse(viewModel.getCurrentAccount().getUrl()).getHost()); binding.check.setSelected(true); Glide.with(requireContext()) - .load(viewModel.getCurrentAccount().getAvatarUrl(dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size))) + .load(viewModel.getCurrentAccount().getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) @@ -65,7 +58,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { dismiss(); })); - observeOnce(syncManager.readAccounts(), requireActivity(), (accounts) -> { + observeOnce(viewModel.readAccounts(), requireActivity(), (accounts) -> { accounts.remove(viewModel.getCurrentAccount()); adapter.setAccounts(accounts); }); @@ -76,7 +69,10 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { try { AccountImporter.pickNewAccount(requireActivity()); } catch (NextcloudFilesAppNotInstalledException e) { - ExceptionUtil.handleNextcloudFilesAppNotInstalledException(requireContext(), e); + UiExceptionManager.showDialogForException(requireContext(), e); + DeckLog.warn("============================================================="); + DeckLog.warn("Nextcloud app is not installed. Cannot choose account"); + DeckLog.logError(e); } catch (AndroidGetAccountsPermissionNotGranted e) { AccountImporter.requestAndroidAccountPermissionsAndPickAccount(requireActivity()); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java index 9c93c422e..a60b6a0ea 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java @@ -10,12 +10,11 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import it.niedermann.android.glidesso.SingleSignOnUrl; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding; import it.niedermann.nextcloud.deck.model.Account; - -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { @@ -30,7 +29,7 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { binding.accountName.setText(account.getUserName()); binding.accountHost.setText(Uri.parse(account.getUrl()).getHost()); Glide.with(itemView.getContext()) - .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) + .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) 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 5ab94b4f6..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, "#" + 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/archivedboards/ArchivedBoardsActvitiy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java index 7c3a2d23c..d6d9cac1e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java @@ -15,13 +15,17 @@ import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener; import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener; import it.niedermann.nextcloud.deck.ui.board.EditBoardListener; import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; + public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoardListener, EditBoardListener, ArchiveBoardListener { private static final String BUNDLE_KEY_ACCOUNT = "accountId"; @@ -29,7 +33,6 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa private MainViewModel viewModel; private ActivityArchivedBinding binding; private ArchivedBoardsAdapter adapter; - private SyncManager syncManager; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -54,12 +57,18 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa viewModel = new ViewModelProvider(this).get(MainViewModel.class); viewModel.setCurrentAccount(account); - syncManager = new SyncManager(this); - adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), (board) -> syncManager.dearchiveBoard(board)); + adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), (board) -> { + final WrappedLiveData<FullBoard> liveData = viewModel.dearchiveBoard(board); + observeOnce(liveData, this, (fullBoard) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + }); binding.recyclerView.setAdapter(adapter); - syncManager.getBoards(account.getId(), true).observe(this, (boards) -> { + viewModel.getBoards(account.getId(), true).observe(this, (boards) -> { viewModel.setCurrentAccountHasArchivedBoards(boards != null && boards.size() > 0); adapter.setBoards(boards == null ? Collections.emptyList() : boards); }); @@ -80,16 +89,36 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa @Override public void onBoardDeleted(Board board) { - syncManager.deleteBoard(board); + final WrappedLiveData<Void> deleteLiveData = viewModel.deleteBoard(board); + observeOnce(deleteLiveData, this, (next) -> { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override public void onUpdateBoard(FullBoard fullBoard) { - syncManager.updateBoard(fullBoard); + final WrappedLiveData<FullBoard> updateLiveData = viewModel.updateBoard(fullBoard); + observeOnce(updateLiveData, this, (next) -> { + if (updateLiveData.hasError()) { + ExceptionDialogFragment.newInstance(updateLiveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override public void onArchive(Board board) { - syncManager.dearchiveBoard(board); + final WrappedLiveData<FullBoard> liveData = viewModel.dearchiveBoard(board); + observeOnce(liveData, this, (fullBoard) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + } + + @Override + public void onClone(Board board) { + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java index ed2ee7097..b3533528e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java @@ -6,12 +6,15 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; import it.niedermann.nextcloud.deck.databinding.ActivityArchivedBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper; +import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel; public class ArchivedCardsActvitiy extends BrandedActivity { @@ -21,7 +24,8 @@ public class ArchivedCardsActvitiy extends BrandedActivity { private ActivityArchivedBinding binding; private ArchivedCardsAdapter adapter; - private SyncManager syncManager; + private MainViewModel viewModel; + private PickStackViewModel pickStackViewModel; private Account account; private long boardId; @@ -50,16 +54,20 @@ public class ArchivedCardsActvitiy extends BrandedActivity { } binding = ActivityArchivedBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(MainViewModel.class); + pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class); + setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - syncManager = new SyncManager(this); + viewModel.setCurrentAccount(account); + LiveDataHelper.observeOnce(viewModel.getFullBoardById(account.getId(), boardId), this, (fullBoard) -> { + viewModel.setCurrentBoard(fullBoard.getBoard()); - adapter = new ArchivedCardsAdapter(this, getSupportFragmentManager(), account, boardId, false, syncManager, this); - binding.recyclerView.setAdapter(adapter); + adapter = new ArchivedCardsAdapter(this, getSupportFragmentManager(), viewModel, this); + binding.recyclerView.setAdapter(adapter); - syncManager.getArchivedFullCardsForBoard(account.getId(), boardId).observe(this, (fullCards) -> { - adapter.setCardList(fullCards); + viewModel.getArchivedFullCardsForBoard(account.getId(), boardId).observe(this, (fullCards) -> adapter.setCardList(fullCards)); }); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java index e6abf0ccc..b5034ebfa 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java @@ -1,67 +1,55 @@ package it.niedermann.nextcloud.deck.ui.archivedcards; import android.content.Context; -import android.view.Menu; import android.view.MenuItem; -import android.view.View; -import android.widget.PopupMenu; import androidx.annotation.NonNull; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LifecycleOwner; -import org.jetbrains.annotations.NotNull; - import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; +import it.niedermann.nextcloud.deck.ui.MainViewModel; +import it.niedermann.nextcloud.deck.ui.card.AbstractCardViewHolder; import it.niedermann.nextcloud.deck.ui.card.CardAdapter; -import it.niedermann.nextcloud.deck.ui.card.ItemCardViewHolder; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; + +import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; public class ArchivedCardsAdapter extends CardAdapter { @SuppressWarnings("WeakerAccess") - public ArchivedCardsAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull Account account, long boardId, boolean canEdit, @NonNull SyncManager syncManager, @NonNull LifecycleOwner lifecycleOwner) { - super(context, fragmentManager, account, boardId, 0L, 0L, canEdit, syncManager, lifecycleOwner, null); + public ArchivedCardsAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull MainViewModel viewModel, @NonNull LifecycleOwner lifecycleOwner) { + super(context, fragmentManager, 0L, viewModel, lifecycleOwner, null); } @Override - public void onBindViewHolder(@NonNull ItemCardViewHolder viewHolder, int position) { - super.onBindViewHolder(viewHolder, position); - viewHolder.binding.card.setOnClickListener(null); - viewHolder.binding.card.setOnLongClickListener(null); - } - - protected void onOverflowIconClicked(@NotNull View view, FullCard card) { - final Context context = view.getContext(); - final PopupMenu popup = new PopupMenu(context, view); - popup.inflate(R.menu.card_menu); - prepareOptionsMenu(popup.getMenu(), card); - - popup.setOnMenuItemClickListener(item -> optionsItemSelected(context, item, card)); - popup.show(); - } - - protected void prepareOptionsMenu(Menu menu, @NotNull FullCard card) { - // Nothing to do + public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) { + viewHolder.bind(cardList.get(position), mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), false, R.menu.archived_card_menu, this, counterMaxValue, mainColor); } - protected boolean optionsItemSelected(@NonNull Context context, @NotNull MenuItem item, FullCard fullCard) { - switch (item.getItemId()) { - case R.id.action_card_dearchive: { - // TODO error handling - new Thread(() -> syncManager.dearchiveCard(fullCard)).start(); - return true; - } - case R.id.action_card_delete: { - // TODO error handling - syncManager.deleteCard(fullCard.getCard()); - return true; - } - default: { - return false; - } + @Override + public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) { + int itemId = menuItem.getItemId(); + if (itemId == R.id.action_card_dearchive) { + final WrappedLiveData<FullCard> liveData = mainViewModel.dearchiveCard(fullCard); + observeOnce(liveData, lifecycleOwner, (next) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()); + } + }); + return true; + } else if (itemId == R.id.action_card_delete) { + final WrappedLiveData<Void> liveData = mainViewModel.deleteCard(fullCard.getCard()); + observeOnce(liveData, lifecycleOwner, (next) -> { + if (liveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(liveData.getError())) { + ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()); + } + }); + return true; } + return super.onCardOptionsItemSelected(menuItem, fullCard); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java index 0794323ec..c7d32bd37 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java @@ -1,43 +1,31 @@ package it.niedermann.nextcloud.deck.ui.attachments; import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.Build; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.engine.GlideException; -import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.target.Target; - +import java.util.ArrayList; import java.util.List; -import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; -import it.niedermann.nextcloud.deck.util.AttachmentUtil; -import it.niedermann.nextcloud.deck.util.MimeTypeUtil; public class AttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> { private final Account account; private final long cardRemoteId; @NonNull - private List<Attachment> attachments; - private Context context; + private final List<Attachment> attachments = new ArrayList<>(); @SuppressWarnings("WeakerAccess") public AttachmentAdapter(@NonNull Account account, long cardRemoteId, @NonNull List<Attachment> attachments) { super(); - this.attachments = attachments; + this.attachments.clear(); + this.attachments.addAll(attachments); this.account = account; this.cardRemoteId = cardRemoteId; } @@ -45,43 +33,13 @@ public class AttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder @NonNull @Override public AttachmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - this.context = parent.getContext(); - return new AttachmentViewHolder(ItemAttachmentBinding.inflate(LayoutInflater.from(context), parent, false)); + final Context context = parent.getContext(); + return new AttachmentViewHolder(context, ItemAttachmentBinding.inflate(LayoutInflater.from(context), parent, false)); } @Override public void onBindViewHolder(@NonNull AttachmentViewHolder holder, int position) { - final Attachment attachment = attachments.get(position); - final String uri = AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId()); - if (MimeTypeUtil.isImage(attachment.getMimetype())) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - holder.binding.preview.setTransitionName(context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId()))); - } - holder.binding.preview.setImageResource(R.drawable.ic_image_grey600_24dp); - Glide.with(context) - .load(uri) - .listener(new RequestListener<Drawable>() { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, - Target<Drawable> target, boolean isFirstResource) { - if (context instanceof FragmentActivity) { - ((FragmentActivity) context).supportStartPostponedEnterTransition(); - } - return false; - } - - @Override - public boolean onResourceReady(Drawable resource, Object model, - Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { - if (context instanceof FragmentActivity) { - ((FragmentActivity) context).supportStartPostponedEnterTransition(); - } - return false; - } - }) - .error(R.drawable.ic_image_grey600_24dp) - .into(holder.binding.preview); - } + holder.bind(account, attachments.get(position), cardRemoteId); } @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java index 584a57d1d..6f4fe3c74 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java @@ -1,15 +1,72 @@ package it.niedermann.nextcloud.deck.ui.attachments; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; + +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; +import it.niedermann.nextcloud.deck.util.MimeTypeUtil; public class AttachmentViewHolder extends RecyclerView.ViewHolder { - public ItemAttachmentBinding binding; + @NonNull + private final Context parentContext; + @NonNull + private final ItemAttachmentBinding binding; @SuppressWarnings("WeakerAccess") - public AttachmentViewHolder(ItemAttachmentBinding binding) { + public AttachmentViewHolder(@NonNull Context parentContext, @NonNull ItemAttachmentBinding binding) { super(binding.getRoot()); + this.parentContext = parentContext; this.binding = binding; } + + public void bind(@NonNull Account account, @NonNull Attachment attachment, long cardRemoteId) { + if (MimeTypeUtil.isImage(attachment.getMimetype())) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + binding.preview.setTransitionName(parentContext.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId()))); + } + binding.preview.setImageResource(R.drawable.ic_image_grey600_24dp); + binding.preview.post(() -> { + final String uri = AttachmentUtil.getThumbnailUrl(account.getServerDeckVersionAsObject(), account.getUrl(), cardRemoteId, attachment, binding.preview.getWidth()); + Glide.with(parentContext) + .load(uri) + .listener(new RequestListener<Drawable>() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, + Target<Drawable> target, boolean isFirstResource) { + if (parentContext instanceof FragmentActivity) { + ((FragmentActivity) parentContext).supportStartPostponedEnterTransition(); + } + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, + Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { + if (parentContext instanceof FragmentActivity) { + ((FragmentActivity) parentContext).supportStartPostponedEnterTransition(); + } + return false; + } + }) + .error(R.drawable.ic_image_grey600_24dp) + .into(binding.preview); + }); + } + } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java index 98cfd4440..6618f7f72 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java @@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.ui.attachments; import android.content.Context; import android.content.Intent; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; @@ -9,6 +10,9 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.SharedElementCallback; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.widget.ViewPager2; @@ -21,7 +25,6 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ActivityAttachmentsBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; import it.niedermann.nextcloud.deck.util.MimeTypeUtil; @@ -32,6 +35,7 @@ public class AttachmentsActivity extends AppCompatActivity { private static final String BUNDLE_KEY_CURRENT_ATTACHMENT_LOCAL_ID = "currentAttachmenLocaltId"; private ActivityAttachmentsBinding binding; + private AttachmentsViewModel viewModel; private ViewPager2.OnPageChangeCallback onPageChangeCallback; @Override @@ -40,10 +44,15 @@ public class AttachmentsActivity extends AppCompatActivity { Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); binding = ActivityAttachmentsBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(AttachmentsViewModel.class); + setContentView(binding.getRoot()); supportPostponeEnterTransition(); setSupportActionBar(binding.toolbar); + final Drawable navigationIcon = getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); + DrawableCompat.setTint(navigationIcon, ContextCompat.getColor(this, android.R.color.white)); + binding.toolbar.setNavigationIcon(navigationIcon); final Bundle args = getIntent().getExtras(); if (args == null || !args.containsKey(BUNDLE_KEY_ACCOUNT) || !args.containsKey(BUNDLE_KEY_CARD_ID)) { @@ -58,8 +67,7 @@ public class AttachmentsActivity extends AppCompatActivity { long cardId = args.getLong(BUNDLE_KEY_CARD_ID); - final SyncManager syncManager = new SyncManager(this); - syncManager.getCardByLocalId(account.getId(), cardId).observe(this, fullCard -> { + viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardId).observe(this, fullCard -> { final List<Attachment> attachments = new ArrayList<>(); for (Attachment a : fullCard.getAttachments()) { if (MimeTypeUtil.isImage(a.getMimetype())) { @@ -67,7 +75,7 @@ public class AttachmentsActivity extends AppCompatActivity { } } if (fullCard.getAttachments().size() == 0) { - DeckLog.logError(new IllegalStateException(AttachmentsActivity.class.getSimpleName() + " called, but card " + fullCard.getLocalId() + "has no attachments")); + DeckLog.logError(new IllegalStateException(AttachmentsActivity.class.getSimpleName() + " called, but card " + fullCard.getCard().getTitle() + " has no attachments")); supportFinishAfterTransition(); return; } @@ -79,7 +87,7 @@ public class AttachmentsActivity extends AppCompatActivity { binding.toolbar.setTitle(attachments.get(position).getBasename()); } }; - RecyclerView.Adapter adapter = new AttachmentAdapter(account, fullCard.getId(), attachments); + RecyclerView.Adapter<AttachmentViewHolder> adapter = new AttachmentAdapter(account, fullCard.getId(), attachments); binding.viewPager.setAdapter(adapter); binding.viewPager.registerOnPageChangeCallback(onPageChangeCallback); @@ -104,7 +112,7 @@ public class AttachmentsActivity extends AppCompatActivity { long currentAttachmentLocalId = attachments.get(binding.viewPager.getCurrentItem()).getLocalId(); String transitionKey = getString(R.string.transition_attachment_preview, String.valueOf(currentAttachmentLocalId)); if (transitionKey.equals(names.get(0))) { - sharedElements.put(transitionKey, binding.viewPager.getRootView().findViewById(R.id.preview) + sharedElements.put(transitionKey, binding.viewPager.getRootView().findViewById(R.id.avatar) ); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java new file mode 100644 index 000000000..87a17470c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java @@ -0,0 +1,25 @@ +package it.niedermann.nextcloud.deck.ui.attachments; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; + +@SuppressWarnings("WeakerAccess") +public class AttachmentsViewModel extends AndroidViewModel { + + private final SyncManager syncManager; + + public AttachmentsViewModel(@NonNull Application application) { + super(application); + this.syncManager = new SyncManager(application); + } + + public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { + return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java index b7e27aa97..ff0d3e941 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java @@ -4,4 +4,5 @@ import it.niedermann.nextcloud.deck.model.Board; public interface ArchiveBoardListener { void onArchive(Board board); + void onClone(Board board); }
\ No newline at end of file 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 7049cc16c..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, "#" + 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 e5d0a482b..9da836c4c 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 @@ -7,13 +7,13 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogTextColorInputBinding; import it.niedermann.nextcloud.deck.model.full.FullBoard; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; @@ -53,24 +53,24 @@ public class EditBoardDialogFragment extends BrandedDialogFragment { if (args != null && args.containsKey(KEY_BOARD_ID)) { dialogBuilder.setTitle(R.string.edit_board); dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> { - this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor().substring(1)); + 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) -> { + viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), args.getLong(KEY_BOARD_ID)).observe(EditBoardDialogFragment.this, (FullBoard fb) -> { if (fb.board != null) { this.fullBoard = fb; String title = this.fullBoard.getBoard().getTitle(); binding.input.setText(title); binding.input.setSelection(title.length()); - binding.colorChooser.selectColor("#" + 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 & getResources().getColor(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/accesscontrol/AccessControlAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java index e5a50d9f4..0a1281fae 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java @@ -10,6 +10,7 @@ import android.view.ViewGroup; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.widget.SwitchCompat; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.recyclerview.widget.RecyclerView; @@ -51,7 +52,7 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View this.account = account; this.accessControlChangedListener = accessControlChangedListener; this.context = context; - this.mainColor = context.getResources().getColor(R.color.primary); + this.mainColor = ContextCompat.getColor(context, R.color.primary); setHasStableIds(true); } @@ -172,9 +173,9 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); DrawableCompat.setTintList(switchCompat.getThumbDrawable(), new ColorStateList( new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}}, - new int[]{finalMainColor, context.getResources().getColor(R.color.fg_secondary)} + new int[]{finalMainColor, ContextCompat.getColor(context, R.color.fg_secondary)} )); - final int trackColor = context.getResources().getColor(R.color.fg_secondary); + final int trackColor = ContextCompat.getColor(context, R.color.fg_secondary); final int lightTrackColor = Color.argb(77, Color.red(trackColor), Color.green(trackColor), Color.blue(trackColor)); final int lightTrackColorChecked = Color.argb(77, Color.red(finalMainColor), Color.green(finalMainColor), Color.blue(finalMainColor)); DrawableCompat.setTintList(switchCompat.getTrackDrawable(), new ColorStateList( diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java index 33c0fe5b1..78ccd5333 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java @@ -43,7 +43,6 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement private static final String KEY_BOARD_ID = "board_id"; private long boardId; - private SyncManager syncManager; private UserAutoCompleteAdapter userAutoCompleteAdapter; private AccessControlAdapter adapter; @@ -75,10 +74,9 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement adapter = new AccessControlAdapter(viewModel.getCurrentAccount(), this, requireContext()); binding.peopleList.setAdapter(adapter); - syncManager = new SyncManager(requireActivity()); - syncManager.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (FullBoard fullBoard) -> { + viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (FullBoard fullBoard) -> { if (fullBoard != null) { - syncManager.getAccessControlByLocalBoardId(viewModel.getCurrentAccount().getId(), boardId).observe(this, (List<AccessControl> accessControlList) -> { + viewModel.getAccessControlByLocalBoardId(viewModel.getCurrentAccount().getId(), boardId).observe(this, (List<AccessControl> accessControlList) -> { final AccessControl ownerControl = new AccessControl(); ownerControl.setLocalId(HEADER_ITEM_LOCAL_ID); ownerControl.setUser(fullBoard.getOwner()); @@ -103,15 +101,20 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement @Override public void updateAccessControl(AccessControl accessControl) { - syncManager.updateAccessControl(accessControl); + WrappedLiveData<AccessControl> updateLiveData = viewModel.updateAccessControl(accessControl); + observeOnce(updateLiveData, requireActivity(), (next) -> { + if (updateLiveData.hasError()) { + ExceptionDialogFragment.newInstance(updateLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override public void deleteAccessControl(AccessControl ac) { - final WrappedLiveData<Void> wrappedDeleteLiveData = syncManager.deleteAccessControl(ac); + final WrappedLiveData<Void> wrappedDeleteLiveData = viewModel.deleteAccessControl(ac); adapter.remove(ac); observeOnce(wrappedDeleteLiveData, this, (ignored) -> { - if (wrappedDeleteLiveData.hasError()) { + if (wrappedDeleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(wrappedDeleteLiveData.getError())) { DeckLog.logError(wrappedDeleteLiveData.getError()); BrandedSnackbar.make(requireView(), getString(R.string.error_revoking_ac, ac.getUser().getDisplayname()), Snackbar.LENGTH_LONG) .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(wrappedDeleteLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) @@ -129,7 +132,12 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement ac.setType(0L); // https://github.com/nextcloud/deck/blob/master/docs/API.md#post-boardsboardidacl---add-new-acl-rule ac.setUserId(user.getLocalId()); ac.setUser(user); - syncManager.createAccessControl(viewModel.getCurrentAccount().getId(), ac); + final WrappedLiveData<AccessControl> createLiveData = viewModel.createAccessControl(viewModel.getCurrentAccount().getId(), ac); + observeOnce(createLiveData, this, (next) -> { + if (createLiveData.hasError()) { + ExceptionDialogFragment.newInstance(createLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); binding.people.setText(""); userAutoCompleteAdapter.exclude(user); } 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 d460d1590..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 @@ -58,14 +58,14 @@ public class EditLabelDialogFragment extends BrandedDialogFragment { dialogBuilder.setTitle(getString(R.string.edit_tag, label.getTitle())); dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> { - this.label.setColor(binding.colorChooser.getSelectedColor().substring(1)); + this.label.setColor(binding.colorChooser.getSelectedColor()); this.label.setTitle(binding.input.getText().toString()); listener.onLabelUpdated(this.label); }); String title = this.label.getTitle(); binding.input.setText(title); binding.input.setSelection(title.length()); - binding.colorChooser.selectColor("#" + 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/board/managelabels/ManageLabelsDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java index 3391c7a99..bc0d98ca3 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java @@ -38,7 +38,6 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements private static final String KEY_BOARD_ID = "board_id"; private long boardId; - private SyncManager syncManager; @Override public void onAttach(@NonNull Context context) { @@ -67,8 +66,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements colors = getResources().getStringArray(R.array.board_default_colors); adapter = new ManageLabelsAdapter(this, requireContext()); binding.labels.setAdapter(adapter); - syncManager = new SyncManager(requireActivity()); - syncManager.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (fullBoard) -> { + viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (fullBoard) -> { if (fullBoard == null) { throw new IllegalStateException("FullBoard should not be null"); } @@ -80,9 +78,9 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements final Label label = new Label(); label.setBoardId(boardId); label.setTitle(binding.addLabelTitle.getText().toString()); - label.setColor(colors[new Random().nextInt(colors.length)].substring(1)); + label.setColor(colors[new Random().nextInt(colors.length)]); - WrappedLiveData<Label> createLiveData = syncManager.createLabel(viewModel.getCurrentAccount().getId(), label, boardId); + WrappedLiveData<Label> createLiveData = viewModel.createLabel(viewModel.getCurrentAccount().getId(), label, boardId); observeOnce(createLiveData, this, (createdLabel) -> { if (createLiveData.hasError()) { final Throwable error = createLiveData.getError(); @@ -126,7 +124,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements @Override public void requestDelete(@NonNull Label label) { - observeOnce(syncManager.countCardsWithLabel(label.getLocalId()), this, (count) -> { + observeOnce(viewModel.countCardsWithLabel(label.getLocalId()), this, (count) -> { if (count > 0) { new BrandedDeleteAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.delete_something, label.getTitle())) @@ -141,9 +139,9 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements } private void deleteLabel(@NonNull Label label) { - final WrappedLiveData<Void> deleteLiveData = syncManager.deleteLabel(label); + final WrappedLiveData<Void> deleteLiveData = viewModel.deleteLabel(label); observeOnce(deleteLiveData, this, (v) -> { - if (deleteLiveData.hasError()) { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { final Throwable error = deleteLiveData.getError(); assert error != null; Toast.makeText(requireContext(), error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); @@ -159,7 +157,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements @Override public void onLabelUpdated(@NonNull Label label) { - WrappedLiveData<Label> updateLiveData = syncManager.updateLabel(label); + WrappedLiveData<Label> updateLiveData = viewModel.updateLabel(label); observeOnce(updateLiveData, this, (updatedLabel) -> { if (updateLiveData.hasError()) { final Throwable error = updateLiveData.getError(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java index 7fa3abd89..381a290e6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java @@ -1,14 +1,13 @@ package it.niedermann.nextcloud.deck.ui.board.managelabels; import android.content.res.ColorStateList; -import android.graphics.Color; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.databinding.ItemManageLabelBinding; import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.util.ColorUtil; public class ManageLabelsViewHolder extends RecyclerView.ViewHolder { private ItemManageLabelBinding binding; @@ -17,13 +16,14 @@ public class ManageLabelsViewHolder extends RecyclerView.ViewHolder { public ManageLabelsViewHolder(ItemManageLabelBinding binding) { super(binding.getRoot()); this.binding = binding; + this.binding.label.setClickable(false); } public void bind(@NonNull Label label, @NonNull ManageLabelListener listener) { binding.label.setText(label.getTitle()); - final int labelColor = Color.parseColor("#" + label.getColor()); + final int labelColor = label.getColor(); binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); - final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor); + final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); binding.label.setTextColor(color); binding.delete.setOnClickListener((v) -> listener.requestDelete(label)); binding.editText.setOnClickListener((v) -> listener.requestEdit(label)); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java index cfeffe7dc..880e21073 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java @@ -9,8 +9,6 @@ import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; -import org.jetbrains.annotations.NotNull; - import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; @@ -22,7 +20,7 @@ public class BrandedAlertDialogBuilder extends AlertDialog.Builder implements Br super(context); } - @NotNull + @NonNull @Override public AlertDialog create() { this.dialog = super.create(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java index 5bef66f2c..319df7f79 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java @@ -17,7 +17,7 @@ import com.wdullaer.materialdatetimepicker.date.DatePickerDialog; import java.util.Calendar; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.util.ColorUtil; +import it.niedermann.nextcloud.deck.util.DeckColorUtil; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; @@ -44,7 +44,7 @@ public class BrandedDatePickerDialog extends DatePickerDialog implements Branded setOkColor(buttonTextColor); setCancelColor(buttonTextColor); // Text in picker title is always white - setAccentColor(ColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent)); + setAccentColor(DeckColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent)); } /** @@ -52,13 +52,13 @@ public class BrandedDatePickerDialog extends DatePickerDialog implements Branded * * @param callBack How the parent is notified that the date is set. * @param year The initial year of the dialog. - * @param monthOfYear The initial month of the dialog. + * @param monthOfYear The initial month of the dialog. [0 - 11] * @param dayOfMonth The initial day of the dialog. * @return a new DatePickerDialog instance. */ public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { DatePickerDialog ret = new BrandedDatePickerDialog(); - ret.initialize(callBack, year, monthOfYear, dayOfMonth); + ret.initialize(callBack, year, monthOfYear - 1, dayOfMonth); return ret; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java index ec3cef553..d88fdd6cc 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java @@ -5,6 +5,7 @@ import android.content.DialogInterface; import android.widget.Button; import androidx.annotation.CallSuper; +import androidx.core.content.ContextCompat; import it.niedermann.nextcloud.deck.R; @@ -20,7 +21,7 @@ public class BrandedDeleteAlertDialogBuilder extends BrandedAlertDialogBuilder { super.applyBrand(mainColor); final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); if (positiveButton != null) { - positiveButton.setTextColor(getContext().getResources().getColor(R.color.danger)); + positiveButton.setTextColor(ContextCompat.getColor(getContext(), R.color.danger)); } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java index 20e6f8dc8..0159a59dc 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java @@ -11,8 +11,8 @@ import androidx.core.content.ContextCompat; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.util.ColorUtil; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; @@ -25,9 +25,9 @@ public class BrandedSnackbar { final Snackbar snackbar = Snackbar.make(view, text, duration); if (isBrandingEnabled(view.getContext())) { @ColorInt final int color = readBrandMainColor(view.getContext()); - snackbar.setActionTextColor(ColorUtil.isColorDark(color) ? Color.WHITE : color); + snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color); } else { - snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.primary)); + snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.defaultBrand)); } return snackbar; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java index a1963aa18..2e0c4d3b8 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java @@ -14,10 +14,10 @@ import androidx.core.content.ContextCompat; import com.wdullaer.materialdatetimepicker.time.TimePickerDialog; -import java.util.Calendar; +import java.time.LocalTime; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.util.ColorUtil; +import it.niedermann.nextcloud.deck.util.DeckColorUtil; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; @@ -44,7 +44,7 @@ public class BrandedTimePickerDialog extends TimePickerDialog implements Branded setOkColor(buttonTextColor); setCancelColor(buttonTextColor); // Text in picker title is always white - setAccentColor(ColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent)); + setAccentColor(DeckColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent)); } /** @@ -86,9 +86,9 @@ public class BrandedTimePickerDialog extends TimePickerDialog implements Branded * @param is24HourMode True to render 24 hour mode, false to render AM / PM selectors. * @return a new TimePickerDialog instance. */ - @SuppressWarnings({"unused", "SameParameterValue"}) + @SuppressWarnings({"SameParameterValue"}) public static TimePickerDialog newInstance(OnTimeSetListener callback, boolean is24HourMode) { - Calendar now = Calendar.getInstance(); - return newInstance(callback, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), is24HourMode); + LocalTime now = LocalTime.now(); + return newInstance(callback, now.getHour(), now.getMinute(), is24HourMode); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java index 02ad6b309..b780efec5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java @@ -17,14 +17,13 @@ import androidx.preference.PreferenceManager; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficient; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas; -import static it.niedermann.nextcloud.deck.util.ColorUtil.getContrastRatio; -import static it.niedermann.nextcloud.deck.util.ColorUtil.getForegroundColorForBackgroundColor; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficient; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; public abstract class BrandingUtil { @@ -44,7 +43,7 @@ public abstract class BrandingUtil { DeckLog.log("--- Read: shared_preference_theme_main"); return sharedPreferences.getInt(context.getString(R.string.shared_preference_theme_main), context.getApplicationContext().getResources().getColor(R.color.defaultBrand)); } else { - return context.getResources().getColor(R.color.defaultBrand); + return ContextCompat.getColor(context, R.color.defaultBrand); } } @@ -87,13 +86,13 @@ public abstract class BrandingUtil { fab.setSupportBackgroundTintList(ColorStateList.valueOf(contrastRatioIsSufficient ? mainColor : ContextCompat.getColor(fab.getContext(), R.color.accent))); - fab.setColorFilter(contrastRatioIsSufficient ? getForegroundColorForBackgroundColor(mainColor) : mainColor); + fab.setColorFilter(contrastRatioIsSufficient ? ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(mainColor) : mainColor); } public static void applyBrandToPrimaryTabLayout(@ColorInt int mainColor, @NonNull TabLayout tabLayout) { - @ColorInt int finalMainColor = getSecondaryForegroundColorDependingOnTheme(tabLayout.getContext(), mainColor); + @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(tabLayout.getContext(), mainColor); tabLayout.setBackgroundColor(ContextCompat.getColor(tabLayout.getContext(), R.color.primary)); - final boolean contrastRatioIsSufficient = getContrastRatio(mainColor, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)) > 1.7d; + final boolean contrastRatioIsSufficient = ColorUtil.INSTANCE.getContrastRatio(mainColor, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)) > 1.7d; tabLayout.setSelectedTabIndicatorColor(contrastRatioIsSufficient ? mainColor : finalMainColor); } @@ -112,7 +111,7 @@ public abstract class BrandingUtil { finalMainColor, finalMainColor, finalMainColor, - editText.getContext().getResources().getColor(R.color.fg_secondary) + ContextCompat.getColor(editText.getContext(), R.color.fg_secondary) } )); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java new file mode 100644 index 000000000..4d3b8bb35 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java @@ -0,0 +1,122 @@ +package it.niedermann.nextcloud.deck.ui.card; + +import android.content.Context; +import android.view.Menu; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.CallSuper; +import androidx.annotation.ColorInt; +import androidx.annotation.MenuRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.PopupMenu; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.card.MaterialCardView; + +import org.jetbrains.annotations.Contract; + +import java.time.ZoneId; +import java.util.List; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.enums.DBStatus; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.util.DateUtil; +import it.niedermann.nextcloud.deck.util.ViewUtil; + +public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder { + + public AbstractCardViewHolder(@NonNull View itemView) { + super(itemView); + } + + /** + * Removes all {@link OnClickListener} and {@link OnLongClickListener} + */ + @CallSuper + public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @ColorInt int mainColor) { + final Context context = itemView.getContext(); + + bindCardClickListener(null); + bindCardLongClickListener(null); + + getCardMenu().setVisibility(hasEditPermission ? View.VISIBLE : View.GONE); + getCardTitle().setText(fullCard.getCard().getTitle().trim()); + + DrawableCompat.setTint(getNotSyncedYet().getDrawable(), mainColor); + getNotSyncedYet().setVisibility(DBStatus.LOCAL_EDITED.equals(fullCard.getStatusEnum()) ? View.VISIBLE : View.GONE); + + if (fullCard.getCard().getDueDate() != null) { + setupDueDate(getCardDueDate(), fullCard.getCard()); + getCardDueDate().setVisibility(View.VISIBLE); + } else { + getCardDueDate().setVisibility(View.GONE); + } + + getCardMenu().setOnClickListener(view -> { + final PopupMenu popup = new PopupMenu(context, view); + popup.inflate(optionsMenu); + final Menu menu = popup.getMenu(); + if (containsUser(fullCard.getAssignedUsers(), account.getUserName())) { + menu.removeItem(menu.findItem(R.id.action_card_assign).getItemId()); + } else { + menu.removeItem(menu.findItem(R.id.action_card_unassign).getItemId()); + } + if (boardRemoteId == null || fullCard.getCard().getId() == null) { + menu.removeItem(R.id.share_link); + } + + popup.setOnMenuItemClickListener(item -> optionsItemsSelectedListener.onCardOptionsItemSelected(item, fullCard)); + popup.show(); + }); + } + + protected abstract TextView getCardDueDate(); + + protected abstract ImageView getNotSyncedYet(); + + protected abstract TextView getCardTitle(); + + protected abstract View getCardMenu(); + + protected abstract MaterialCardView getCard(); + + public void bindCardClickListener(@Nullable OnClickListener l) { + getCard().setOnClickListener(l); + } + + public void bindCardLongClickListener(@Nullable OnLongClickListener l) { + getCard().setOnLongClickListener(l); + } + + public MaterialCardView getDraggable() { + return getCard(); + } + + private static void setupDueDate(@NonNull TextView cardDueDate, @NonNull Card card) { + final Context context = cardDueDate.getContext(); + cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().toEpochMilli())); + ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate().atZone(ZoneId.systemDefault()).toLocalDate()); + } + + @Contract("null, _ -> false") + private static boolean containsUser(List<User> userList, String username) { + if (userList != null) { + for (User user : userList) { + if (user.getPrimaryKey().equals(username)) { + return true; + } + } + } + return false; + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java index 986a66cad..87140e544 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java @@ -1,99 +1,82 @@ package it.niedermann.nextcloud.deck.ui.card; -import android.annotation.SuppressLint; import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.view.LayoutInflater; -import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.PopupMenu; -import android.widget.TextView; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; -import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LifecycleOwner; import androidx.recyclerview.widget.RecyclerView; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; - import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import it.niedermann.android.crosstabdnd.DragAndDropAdapter; import it.niedermann.android.crosstabdnd.DraggedItemLocalState; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.databinding.ItemCardBinding; +import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding; +import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding; +import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultOnlyTitleBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Card; -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.enums.DBStatus; import it.niedermann.nextcloud.deck.model.full.FullCard; -import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; -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.MainViewModel; import it.niedermann.nextcloud.deck.ui.branding.Branded; -import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import it.niedermann.nextcloud.deck.util.DateUtil; -import it.niedermann.nextcloud.deck.util.ViewUtil; +import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment; +import static androidx.preference.PreferenceManager.getDefaultSharedPreferences; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN; -public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implements DragAndDropAdapter<FullCard>, Branded { - - protected final SyncManager syncManager; +public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener, Branded { - private final FragmentManager fragmentManager; - private final Account account; - @Nullable - private final Long currentBoardRemoteId; - private final long boardId; + private final boolean compactMode; + @NonNull + protected final MainViewModel mainViewModel; + @NonNull + protected final FragmentManager fragmentManager; private final long stackId; - private final boolean canEdit; @NonNull private final Context context; @Nullable private final SelectCardListener selectCardListener; - private List<FullCard> cardList = new LinkedList<>(); - private LifecycleOwner lifecycleOwner; - private List<FullStack> availableStacks = new ArrayList<>(); - private String counterMaxValue; - - private int mainColor; + @NonNull + protected List<FullCard> cardList = new ArrayList<>(); + @NonNull + protected LifecycleOwner lifecycleOwner; + @NonNull + protected String counterMaxValue; + @ColorInt + protected int mainColor; @StringRes - private int shareLinkRes; + private final int shareLinkRes; - public CardAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull Account account, long boardId, @Nullable Long currentBoardRemoteId, long stackId, boolean canEdit, @NonNull SyncManager syncManager, @NonNull LifecycleOwner lifecycleOwner, @Nullable SelectCardListener selectCardListener) { + public CardAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, long stackId, @NonNull MainViewModel mainViewModel, @NonNull LifecycleOwner lifecycleOwner, @Nullable SelectCardListener selectCardListener) { this.context = context; + this.counterMaxValue = context.getString(R.string.counter_max_value); this.fragmentManager = fragmentManager; this.lifecycleOwner = lifecycleOwner; - this.account = account; - this.shareLinkRes = account.getServerDeckVersionAsObject().getShareLinkResource(); - this.boardId = boardId; - this.currentBoardRemoteId = currentBoardRemoteId; + this.shareLinkRes = mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().getShareLinkResource(); this.stackId = stackId; - this.canEdit = canEdit; - this.syncManager = syncManager; + this.mainViewModel = mainViewModel; this.selectCardListener = selectCardListener; - this.mainColor = context.getResources().getColor(R.color.primary); - syncManager.getStacksForBoard(account.getId(), boardId).observe(this.lifecycleOwner, (stacks) -> { - availableStacks.clear(); - availableStacks.addAll(stacks); - }); + this.mainColor = ContextCompat.getColor(context, R.color.defaultBrand); + this.compactMode = getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.pref_key_compact), false); setHasStableIds(true); } @@ -104,118 +87,62 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem @NonNull @Override - public ItemCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) { - final Context context = viewGroup.getContext(); - counterMaxValue = context.getString(R.string.counter_max_value); + public AbstractCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + if (viewType == R.layout.item_card_compact) { + return new CompactCardViewHolder(ItemCardCompactBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); + } else if (viewType == R.layout.item_card_default_only_title) { + return new DefaultCardOnlyTitleViewHolder(ItemCardDefaultOnlyTitleBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); + } + return new DefaultCardViewHolder(ItemCardDefaultBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); + } - LayoutInflater layoutInflater = LayoutInflater.from(context); - ItemCardBinding binding = ItemCardBinding.inflate(layoutInflater, viewGroup, false); - return new ItemCardViewHolder(binding); + @Override + public int getItemViewType(int position) { + if (compactMode) { + return R.layout.item_card_compact; + } else { + final FullCard fullCard = cardList.get(position); + if (fullCard.getAttachments().size() == 0 + && fullCard.getAssignedUsers().size() == 0 + && fullCard.getLabels().size() == 0 + && fullCard.getCommentCount() == 0) { + return R.layout.item_card_default_only_title; + } + return R.layout.item_card_default; + } } - @SuppressLint("SetTextI18n") @Override - public void onBindViewHolder(@NonNull ItemCardViewHolder viewHolder, int position) { - final Context context = viewHolder.itemView.getContext(); - final FullCard card = cardList.get(position); + public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) { + @NonNull FullCard fullCard = cardList.get(position); + viewHolder.bind(fullCard, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), mainViewModel.currentBoardHasEditPermission(), R.menu.card_menu, this, counterMaxValue, mainColor); - viewHolder.binding.card.setOnClickListener((v) -> { + // Only enable details view if there is no one waiting for selecting a card. + viewHolder.bindCardClickListener((v) -> { if (selectCardListener == null) { - context.startActivity(EditActivity.createEditCardIntent(context, account, boardId, card.getLocalId())); + context.startActivity(EditActivity.createEditCardIntent(context, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId())); } else { - selectCardListener.onCardSelected(card); + selectCardListener.onCardSelected(fullCard); } }); - if (canEdit && selectCardListener == null) { - viewHolder.binding.card.setOnLongClickListener((v) -> { + + // Only enable Drag and Drop if there is no one waiting for selecting a card. + if (selectCardListener == null) { + viewHolder.bindCardLongClickListener((v) -> { DeckLog.log("Starting drag and drop"); - v.startDrag(ClipData.newPlainText("cardid", String.valueOf(card.getLocalId())), + v.startDrag(ClipData.newPlainText("cardid", String.valueOf(fullCard.getLocalId())), new View.DragShadowBuilder(v), - new DraggedItemLocalState<>(card, viewHolder.binding.card, this, position), + new DraggedItemLocalState<>(fullCard, viewHolder.getDraggable(), this, position), 0 ); return true; }); - } else { - viewHolder.binding.cardMenu.setVisibility(View.GONE); - } - viewHolder.binding.cardTitle.setText(card.getCard().getTitle().trim()); - - if (card.getAssignedUsers() != null && card.getAssignedUsers().size() > 0) { - viewHolder.binding.overlappingAvatars.setAvatars(account, card.getAssignedUsers()); - viewHolder.binding.overlappingAvatars.setVisibility(View.VISIBLE); - } else { - viewHolder.binding.overlappingAvatars.setVisibility(View.GONE); } - - DrawableCompat.setTint(viewHolder.binding.notSyncedYet.getDrawable(), mainColor); - viewHolder.binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(card.getStatusEnum()) ? View.VISIBLE : View.GONE); - - if (card.getCard().getDueDate() != null) { - setupDueDate(viewHolder.binding.cardDueDate, card.getCard()); - viewHolder.binding.cardDueDate.setVisibility(View.VISIBLE); - } else { - viewHolder.binding.cardDueDate.setVisibility(View.GONE); - } - - final int attachmentsCount = card.getAttachments().size(); - - if (attachmentsCount == 0) { - viewHolder.binding.cardCountAttachments.setVisibility(View.GONE); - } else { - setupCounter(viewHolder.binding.cardCountAttachments, attachmentsCount); - viewHolder.binding.cardCountAttachments.setVisibility(View.VISIBLE); - } - - final int commentsCount = card.getCommentCount(); - - if (commentsCount == 0) { - viewHolder.binding.cardCountComments.setVisibility(View.GONE); - } else { - setupCounter(viewHolder.binding.cardCountComments, commentsCount); - - viewHolder.binding.cardCountComments.setVisibility(View.VISIBLE); - } - - List<Label> labels = card.getLabels(); - if (labels != null && labels.size() > 0) { - viewHolder.binding.labels.updateLabels(labels); - viewHolder.binding.labels.setVisibility(View.VISIBLE); - } else { - viewHolder.binding.labels.removeAllViews(); - viewHolder.binding.labels.setVisibility(View.GONE); - } - - Card.TaskStatus taskStatus = card.getCard().getTaskStatus(); - if (taskStatus.taskCount > 0) { - viewHolder.binding.cardCountTasks.setText(context.getResources().getString(R.string.task_count, String.valueOf(taskStatus.doneCount), String.valueOf(taskStatus.taskCount))); - viewHolder.binding.cardCountTasks.setVisibility(View.VISIBLE); - } else { - viewHolder.binding.cardCountTasks.setVisibility(View.GONE); - } - - viewHolder.binding.cardMenu.setOnClickListener(v -> onOverflowIconClicked(v, card)); - } - - private void setupCounter(@NonNull TextView textView, int count) { - if (count > 99) { - textView.setText(counterMaxValue); - } else if (count > 1) { - textView.setText(String.valueOf(count)); - } else if (count == 1) { - textView.setText(""); - } - } - - private void setupDueDate(@NonNull TextView cardDueDate, @NotNull Card card) { - final Context context = cardDueDate.getContext(); - cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().getTime())); - ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate()); } @Override public int getItemCount() { - return cardList == null ? 0 : cardList.size(); + return cardList.size(); } public void insertItem(FullCard fullCard, int position) { @@ -223,6 +150,7 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem notifyItemInserted(position); } + @NonNull @Override public List<FullCard> getItemList() { return this.cardList; @@ -240,114 +168,59 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem notifyItemRemoved(position); } - protected void onOverflowIconClicked(@NotNull View view, FullCard card) { - final Context context = view.getContext(); - final PopupMenu popup = new PopupMenu(context, view); - popup.inflate(R.menu.card_menu); - prepareOptionsMenu(popup.getMenu(), card); - - popup.setOnMenuItemClickListener(item -> optionsItemSelected(context, item, card)); - popup.show(); - } - - protected void prepareOptionsMenu(Menu menu, @NotNull FullCard card) { - if (containsUser(card.getAssignedUsers(), account.getUserName())) { - menu.removeItem(menu.findItem(R.id.action_card_assign).getItemId()); - } else { - menu.removeItem(menu.findItem(R.id.action_card_unassign).getItemId()); - } - if (currentBoardRemoteId == null || card.getCard().getId() == null) { - menu.removeItem(R.id.share_link); - } - } - public void setCardList(@NonNull List<FullCard> cardList) { this.cardList.clear(); this.cardList.addAll(cardList); notifyDataSetChanged(); } - @Contract("null, _ -> false") - private boolean containsUser(List<User> userList, String username) { - if (userList != null) { - for (User user : userList) { - if (user.getPrimaryKey().equals(username)) { - return true; - } - } - } - return false; + @Override + public void applyBrand(int mainColor) { + this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); + notifyDataSetChanged(); } - protected boolean optionsItemSelected(@NonNull Context context, @NotNull MenuItem item, FullCard fullCard) { - switch (item.getItemId()) { - case R.id.share_link: { - Intent shareIntent = new Intent() - .setAction(Intent.ACTION_SEND) - .setType(TEXT_PLAIN) - .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle()) - .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle()) - .putExtra(Intent.EXTRA_TEXT, account.getUrl() + context.getString(shareLinkRes, currentBoardRemoteId, fullCard.getCard().getId())); - context.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle())); - } - case R.id.action_card_assign: { - new Thread(() -> syncManager.assignUserToCard(syncManager.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start(); - return true; - } - case R.id.action_card_unassign: { - new Thread(() -> syncManager.unassignUserFromCard(syncManager.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start(); - return true; - } - case R.id.action_card_move: { - int currentStackItem = 0; - CharSequence[] items = new CharSequence[availableStacks.size()]; - for (int i = 0; i < availableStacks.size(); i++) { - final Stack stack = availableStacks.get(i).getStack(); - items[i] = stack.getTitle(); - if (stack.getLocalId().equals(stackId)) { - currentStackItem = i; - } + @Override + public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) { + int itemId = menuItem.getItemId(); + final Account account = mainViewModel.getCurrentAccount(); + if (itemId == R.id.share_link) { + Intent shareIntent = new Intent() + .setAction(Intent.ACTION_SEND) + .setType(TEXT_PLAIN) + .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle()) + .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle()) + .putExtra(Intent.EXTRA_TEXT, account.getUrl() + context.getString(shareLinkRes, mainViewModel.getCurrentBoardRemoteId(), fullCard.getCard().getId())); + context.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle())); + new Thread(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start(); + return true; + } else if (itemId == R.id.action_card_assign) { + new Thread(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start(); + return true; + } else if (itemId == R.id.action_card_unassign) { + new Thread(() -> mainViewModel.unassignUserFromCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start(); + return true; + } else if (itemId == R.id.action_card_move) { + DeckLog.verbose("[Move card] Launch move dialog for " + Card.class.getSimpleName() + " \"" + fullCard.getCard().getTitle() + "\" (#" + fullCard.getLocalId() + ") from " + Stack.class.getSimpleName() + " #" + +stackId); + MoveCardDialogFragment.newInstance(fullCard.getAccountId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getCard().getTitle(), fullCard.getLocalId()).show(fragmentManager, MoveCardDialogFragment.class.getSimpleName()); + return true; + } else if (itemId == R.id.action_card_archive) { + final WrappedLiveData<FullCard> archiveLiveData = mainViewModel.archiveCard(fullCard); + observeOnce(archiveLiveData, lifecycleOwner, (v) -> { + if (archiveLiveData.hasError()) { + ExceptionDialogFragment.newInstance(archiveLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()); } - final FullCard newCard = fullCard; - new BrandedAlertDialogBuilder(context) - .setSingleChoiceItems(items, currentStackItem, (dialog, which) -> { - dialog.cancel(); - newCard.getCard().setStackId(availableStacks.get(which).getStack().getLocalId()); - LiveDataHelper.observeOnce(syncManager.updateCard(newCard), lifecycleOwner, (c) -> { - // Nothing to do here... - }); - DeckLog.log("Moved card \"" + fullCard.getCard().getTitle() + "\" to \"" + availableStacks.get(which).getStack().getTitle() + "\""); - }) - .setNeutralButton(android.R.string.cancel, null) - .setTitle(context.getString(R.string.action_card_move_title, fullCard.getCard().getTitle())) - .show(); - return true; - } - case R.id.action_card_archive: { - final WrappedLiveData<FullCard> archiveLiveData = syncManager.archiveCard(fullCard); - observeOnce(archiveLiveData, lifecycleOwner, (v) -> { - if (archiveLiveData.hasError()) { - ExceptionDialogFragment.newInstance(archiveLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()); - } - }); - return true; - } - case R.id.action_card_delete: { - final WrappedLiveData<Void> deleteLiveData = syncManager.deleteCard(fullCard.getCard()); - observeOnce(deleteLiveData, lifecycleOwner, (v) -> { - if (deleteLiveData.hasError()) { - ExceptionDialogFragment.newInstance(deleteLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()); - } - }); - return true; - } + }); + return true; + } else if (itemId == R.id.action_card_delete) { + final WrappedLiveData<Void> deleteLiveData = mainViewModel.deleteCard(fullCard.getCard()); + observeOnce(deleteLiveData, lifecycleOwner, (v) -> { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()); + } + }); + return true; } return true; } - - @Override - public void applyBrand(int mainColor) { - this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); - notifyDataSetChanged(); - } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java new file mode 100644 index 000000000..d3050b732 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java @@ -0,0 +1,11 @@ +package it.niedermann.nextcloud.deck.ui.card; + +import android.view.MenuItem; + +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.full.FullCard; + +public interface CardOptionsItemSelectedListener { + boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java new file mode 100644 index 000000000..e9f366d99 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java @@ -0,0 +1,84 @@ +package it.niedermann.nextcloud.deck.ui.card; + +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.MenuRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.card.MaterialCardView; + +import java.util.List; + +import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.model.full.FullCard; + +public class CompactCardViewHolder extends AbstractCardViewHolder { + private ItemCardCompactBinding binding; + + @SuppressWarnings("WeakerAccess") + public CompactCardViewHolder(@NonNull ItemCardCompactBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + /** + * Removes all {@link OnClickListener} and {@link OnLongClickListener} + */ + public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @ColorInt int mainColor) { + super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, mainColor); + + List<Label> labels = fullCard.getLabels(); + if (labels != null && labels.size() > 0) { + binding.labels.updateLabels(labels); + binding.labels.setVisibility(View.VISIBLE); + } else { + binding.labels.removeAllViews(); + binding.labels.setVisibility(View.GONE); + } + } + + public void bindCardClickListener(@Nullable OnClickListener l) { + binding.card.setOnClickListener(l); + } + + public void bindCardLongClickListener(@Nullable OnLongClickListener l) { + binding.card.setOnLongClickListener(l); + } + + public MaterialCardView getDraggable() { + return binding.card; + } + + @Override + protected TextView getCardDueDate() { + return binding.cardDueDate; + } + + @Override + protected ImageView getNotSyncedYet() { + return binding.notSyncedYet; + } + + @Override + protected TextView getCardTitle() { + return binding.cardTitle; + } + + @Override + protected View getCardMenu() { + return binding.cardMenu; + } + + @Override + protected MaterialCardView getCard() { + return binding.card; + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java new file mode 100644 index 000000000..2f9e132c9 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java @@ -0,0 +1,61 @@ +package it.niedermann.nextcloud.deck.ui.card; + +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.card.MaterialCardView; + +import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultOnlyTitleBinding; + +public class DefaultCardOnlyTitleViewHolder extends AbstractCardViewHolder { + private ItemCardDefaultOnlyTitleBinding binding; + + @SuppressWarnings("WeakerAccess") + public DefaultCardOnlyTitleViewHolder(@NonNull ItemCardDefaultOnlyTitleBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bindCardClickListener(@Nullable OnClickListener l) { + binding.card.setOnClickListener(l); + } + + public void bindCardLongClickListener(@Nullable OnLongClickListener l) { + binding.card.setOnLongClickListener(l); + } + + public MaterialCardView getDraggable() { + return binding.card; + } + + @Override + protected TextView getCardDueDate() { + return binding.cardDueDate; + } + + @Override + protected ImageView getNotSyncedYet() { + return binding.notSyncedYet; + } + + @Override + protected TextView getCardTitle() { + return binding.cardTitle; + } + + @Override + protected View getCardMenu() { + return binding.cardMenu; + } + + @Override + protected MaterialCardView getCard() { + return binding.card; + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java new file mode 100644 index 000000000..6025cdada --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java @@ -0,0 +1,157 @@ +package it.niedermann.nextcloud.deck.ui.card; + +import android.content.Context; +import android.text.TextUtils; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.MenuRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import com.google.android.material.card.MaterialCardView; + +import org.jetbrains.annotations.Contract; + +import java.util.List; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Card.TaskStatus; +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.full.FullCard; + +public class DefaultCardViewHolder extends AbstractCardViewHolder { + private ItemCardDefaultBinding binding; + + @SuppressWarnings("WeakerAccess") + public DefaultCardViewHolder(@NonNull ItemCardDefaultBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + /** + * Removes all {@link OnClickListener} and {@link OnLongClickListener} + */ + public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @ColorInt int mainColor) { + super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, mainColor); + + final Context context = itemView.getContext(); + + if (fullCard.getAssignedUsers() != null && fullCard.getAssignedUsers().size() > 0) { + binding.overlappingAvatars.setAvatars(account, fullCard.getAssignedUsers()); + binding.overlappingAvatars.setVisibility(View.VISIBLE); + } else { + binding.overlappingAvatars.setVisibility(View.GONE); + } + + final int attachmentsCount = fullCard.getAttachments().size(); + if (attachmentsCount == 0) { + binding.cardCountAttachments.setVisibility(View.GONE); + } else { + setupCounter(binding.cardCountAttachments, counterMaxValue, attachmentsCount); + binding.cardCountAttachments.setVisibility(View.VISIBLE); + } + + final int commentsCount = fullCard.getCommentCount(); + if (commentsCount == 0) { + binding.cardCountComments.setVisibility(View.GONE); + } else { + setupCounter(binding.cardCountComments, counterMaxValue, commentsCount); + + binding.cardCountComments.setVisibility(View.VISIBLE); + } + + final List<Label> labels = fullCard.getLabels(); + if (labels != null && labels.size() > 0) { + binding.labels.updateLabels(labels); + binding.labels.setVisibility(View.VISIBLE); + } else { + binding.labels.removeAllViews(); + binding.labels.setVisibility(View.GONE); + } + + final TaskStatus taskStatus = fullCard.getCard().getTaskStatus(); + if (taskStatus.taskCount > 0) { + binding.cardCountTasks.setText(context.getResources().getString(R.string.task_count, String.valueOf(taskStatus.doneCount), String.valueOf(taskStatus.taskCount))); + binding.cardCountTasks.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.ic_check_grey600_24dp), null, null, null); + binding.cardCountTasks.setVisibility(View.VISIBLE); + } else { + final String description = fullCard.getCard().getDescription(); + if (!TextUtils.isEmpty(description)) { + binding.cardCountTasks.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.ic_baseline_subject_24), null, null, null); + binding.cardCountTasks.setText(null); + binding.cardCountTasks.setVisibility(View.VISIBLE); + } else { + binding.cardCountTasks.setVisibility(View.GONE); + } + } + } + + @Override + protected TextView getCardDueDate() { + return binding.cardDueDate; + } + + @Override + protected ImageView getNotSyncedYet() { + return binding.notSyncedYet; + } + + @Override + protected TextView getCardTitle() { + return binding.cardTitle; + } + + @Override + protected View getCardMenu() { + return binding.cardMenu; + } + + @Override + protected MaterialCardView getCard() { + return binding.card; + } + + public void bindCardClickListener(@Nullable OnClickListener l) { + binding.card.setOnClickListener(l); + } + + public void bindCardLongClickListener(@Nullable OnLongClickListener l) { + binding.card.setOnLongClickListener(l); + } + + public MaterialCardView getDraggable() { + return binding.card; + } + + + private static void setupCounter(@NonNull TextView textView, @NonNull String counterMaxValue, int count) { + if (count > 99) { + textView.setText(counterMaxValue); + } else if (count > 1) { + textView.setText(String.valueOf(count)); + } else if (count == 1) { + textView.setText(""); + } + } + + @Contract("null, _ -> false") + private static boolean containsUser(List<User> userList, String username) { + if (userList != null) { + for (User user : userList) { + if (user.getPrimaryKey().equals(username)) { + return true; + } + } + } + return false; + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java index 0710b7d7c..2e1ff5e06 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java @@ -25,13 +25,12 @@ import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ActivityEditBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity; import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; import it.niedermann.nextcloud.deck.util.CardUtil; -import static android.graphics.Color.parseColor; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToPrimaryTabLayout; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; @@ -45,7 +44,6 @@ public class EditActivity extends BrandedActivity { private ActivityEditBinding binding; private EditCardViewModel viewModel; - private SyncManager syncManager; private static final int[] tabTitles = new int[]{ R.string.card_edit_details, @@ -83,7 +81,6 @@ public class EditActivity extends BrandedActivity { setSupportActionBar(binding.toolbar); viewModel = new ViewModelProvider(this).get(EditCardViewModel.class); - syncManager = new SyncManager(this); loadDataFromIntent(); } @@ -120,8 +117,8 @@ public class EditActivity extends BrandedActivity { final long boardId = args.getLong(BUNDLE_KEY_BOARD_ID); - observeOnce(syncManager.getFullBoardById(account.getId(), boardId), EditActivity.this, (fullBoard -> { - applyBrand(parseColor('#' + fullBoard.getBoard().getColor())); + observeOnce(viewModel.getFullBoardById(account.getId(), boardId), EditActivity.this, (fullBoard -> { + applyBrand(fullBoard.getBoard().getColor()); viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit()); invalidateOptionsMenu(); if (viewModel.isCreateMode()) { @@ -138,7 +135,7 @@ public class EditActivity extends BrandedActivity { setupViewPager(); setupTitle(); } else { - observeOnce(syncManager.getCardByLocalId(account.getId(), cardId), EditActivity.this, (fullCard) -> { + observeOnce(viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardId), EditActivity.this, (fullCard) -> { if (fullCard == null) { new BrandedAlertDialogBuilder(this) .setTitle(R.string.card_not_found) @@ -154,6 +151,8 @@ public class EditActivity extends BrandedActivity { }); } })); + + DeckLog.verbose("Finished loading intent data: { accountId = " + viewModel.getAccount().getId() + " , cardId = " + cardId + " }"); } @Override @@ -170,12 +169,16 @@ public class EditActivity extends BrandedActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_card_save) { - saveAndFinish(); + saveAndRun(super::finish); } return super.onOptionsItemSelected(item); } - private void saveAndFinish() { + /** + * Tries to save the current {@link FullCard} from the {@link EditCardViewModel} and then runs the given {@link Runnable} + * @param runnable + */ + private void saveAndRun(@NonNull Runnable runnable) { if (!viewModel.isPendingCreation()) { viewModel.setPendingCreation(true); final String title = viewModel.getFullCard().getCard().getTitle(); @@ -193,9 +196,9 @@ public class EditActivity extends BrandedActivity { .show(); } else { if (viewModel.isCreateMode()) { - observeOnce(syncManager.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> super.finish()); + observeOnce(viewModel.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run()); } else { - observeOnce(syncManager.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> super.finish()); + observeOnce(viewModel.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run()); } } } @@ -264,26 +267,15 @@ public class EditActivity extends BrandedActivity { } @Override - public boolean onSupportNavigateUp() { - finish(); // close this activity as oppose to navigating up - return true; - } - - @Override - public void onBackPressed() { - finish(); - } - - @Override public void finish() { if (!viewModel.hasChanges() && viewModel.canEdit()) { new BrandedAlertDialogBuilder(this) .setTitle(R.string.simple_save) .setMessage(R.string.do_you_want_to_save_your_changes) - .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndFinish()) + .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndRun(super::finish)) .setNegativeButton(R.string.simple_discard, (dialog, whichButton) -> super.finish()).show(); } else { - directFinish(); + super.finish(); } } @@ -296,10 +288,10 @@ public class EditActivity extends BrandedActivity { @Override public void applyBrand(int mainColor) { - if(isBrandingEnabled(this)) { + if (isBrandingEnabled(this)) { final Drawable navigationIcon = binding.toolbar.getNavigationIcon(); if (navigationIcon == null) { - DeckLog.error("Excpected navigationIcon to be present."); + DeckLog.error("Expected navigationIcon to be present."); } else { DrawableCompat.setTint(binding.toolbar.getNavigationIcon(), colorAccent); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java index c8c93c838..c754d0800 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java @@ -1,38 +1,57 @@ package it.niedermann.nextcloud.deck.ui.card; +import android.app.Application; + import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModel; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import java.io.File; import java.util.ArrayList; +import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.Label; +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.ocs.Activity; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; @SuppressWarnings("WeakerAccess") -public class EditCardViewModel extends ViewModel { +public class EditCardViewModel extends AndroidViewModel { + private SyncManager syncManager; private Account account; private long boardId; - private FullCard originalCard; - private FullCard fullCard; + private FullCardWithProjects originalCard; + private FullCardWithProjects fullCard; private boolean isSupportedVersion = false; private boolean hasCommentsAbility = false; private boolean pendingCreation = false; private boolean canEdit = false; private boolean createMode = false; + public EditCardViewModel(@NonNull Application application) { + super(application); + this.syncManager = new SyncManager(application); + } + /** * Stores a deep copy of the given fullCard to be able to compare the state at every time in #{@link EditCardViewModel#hasChanges()} * * @param boardId Local ID, expecting a positive long value * @param fullCard The card that is currently edited */ - public void initializeExistingCard(long boardId, @NonNull FullCard fullCard, boolean isSupportedVersion) { + public void initializeExistingCard(long boardId, @NonNull FullCardWithProjects fullCard, boolean isSupportedVersion) { this.boardId = boardId; this.fullCard = fullCard; - this.originalCard = new FullCard(this.fullCard); + this.originalCard = new FullCardWithProjects(this.fullCard); this.isSupportedVersion = isSupportedVersion; } @@ -43,7 +62,7 @@ public class EditCardViewModel extends ViewModel { * @param stackId Local ID, expecting a positive long value where the card should be created */ public void initializeNewCard(long boardId, long stackId, boolean isSupportedVersion) { - final FullCard fullCard = new FullCard(); + final FullCardWithProjects fullCard = new FullCardWithProjects(); fullCard.setLabels(new ArrayList<>()); fullCard.setAssignedUsers(new ArrayList<>()); fullCard.setAttachments(new ArrayList<>()); @@ -55,11 +74,12 @@ public class EditCardViewModel extends ViewModel { public void setAccount(@NonNull Account account) { this.account = account; + this.syncManager = new SyncManager(getApplication(), account.getName()); hasCommentsAbility = account.getServerDeckVersionAsObject().supportsComments(); } public boolean hasChanges() { - if(fullCard == null) { + if (fullCard == null) { DeckLog.info("Can not check for changes because fullCard is null → assuming no changes have been made yet."); return false; } @@ -74,7 +94,7 @@ public class EditCardViewModel extends ViewModel { return account; } - public FullCard getFullCard() { + public FullCardWithProjects getFullCard() { return fullCard; } @@ -105,4 +125,44 @@ public class EditCardViewModel extends ViewModel { public long getBoardId() { return boardId; } + + public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { + return syncManager.getFullBoardById(accountId, localId); + } + + public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) { + return syncManager.createLabel(accountId, label, localBoardId); + } + + public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { + return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId); + } + + public WrappedLiveData<FullCard> createFullCard(long accountId, long localBoardId, long localStackId, @NonNull FullCard card) { + return syncManager.createFullCard(accountId, localBoardId, localStackId, card); + } + + public WrappedLiveData<FullCard> updateCard(@NonNull FullCard card) { + return syncManager.updateCard(card); + } + + public LiveData<List<Activity>> syncActivitiesForCard(@NonNull Card card) { + return syncManager.syncActivitiesForCard(card); + } + + public WrappedLiveData<Attachment> addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file) { + return syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file); + } + + public WrappedLiveData<Void> deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId) { + return syncManager.deleteAttachmentOfCard(accountId, localCardId, localAttachmentId); + } + + public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) { + return syncManager.getCardByRemoteID(accountId, remoteId); + } + + public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { + return syncManager.getBoardByRemoteId(accountId, remoteId); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java deleted file mode 100644 index c9e41c37f..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java +++ /dev/null @@ -1,15 +0,0 @@ -package it.niedermann.nextcloud.deck.ui.card; - -import androidx.recyclerview.widget.RecyclerView; - -import it.niedermann.nextcloud.deck.databinding.ItemCardBinding; - -public class ItemCardViewHolder extends RecyclerView.ViewHolder { - public ItemCardBinding binding; - - @SuppressWarnings("WeakerAccess") - public ItemCardViewHolder(ItemCardBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } -}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java index f77282e11..fe974f2a0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java @@ -9,6 +9,7 @@ import android.view.ViewGroup; import android.widget.Filter; import androidx.activity.ComponentActivity; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.drawable.DrawableCompat; @@ -18,11 +19,11 @@ import java.util.Collection; import java.util.List; import java.util.Random; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteLabelBinding; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter; -import it.niedermann.nextcloud.deck.util.ColorUtil; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; @@ -35,7 +36,7 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> { public LabelAutoCompleteAdapter(@NonNull ComponentActivity activity, long accountId, long boardId, long cardId) { super(activity, accountId, boardId, cardId); final String[] colors = activity.getResources().getStringArray(R.array.board_default_colors); - final String createLabelColor = colors[new Random().nextInt(colors.length)].substring(1); + @ColorInt int createLabelColor = Color.parseColor(colors[new Random().nextInt(colors.length)]); observeOnce(syncManager.getFullBoardById(accountId, boardId), activity, (fullBoard) -> { if (fullBoard.getBoard().isPermissionManage()) { canManage = true; @@ -59,8 +60,8 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> { } final Label label = getItem(position); - final int labelColor = Color.parseColor("#" + label.getColor()); - final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor); + final int labelColor = label.getColor(); + final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); binding.label.setText(label.getTitle()); binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java index d2fd4d40d..473ad06c1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java @@ -8,6 +8,7 @@ import android.widget.Filter; import androidx.activity.ComponentActivity; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; import java.util.List; @@ -15,14 +16,16 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteUserBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData; import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter; import it.niedermann.nextcloud.deck.util.ViewUtil; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; - public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> { @NonNull private Account account; + private UserSearchLiveData liveSearchForACL; + private LiveData<List<User>> liveData; + private Observer<List<User>> observer; public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId) { this(activity, account, boardId, NO_CARD); @@ -31,6 +34,7 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> { public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) { super(activity, account.getId(), boardId, cardId); this.account = account; + this.liveSearchForACL = syncManager.searchUserByUidOrDisplayNameForACL(); } @Override @@ -56,23 +60,24 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> { protected FilterResults performFiltering(CharSequence constraint) { if (constraint != null) { activity.runOnUiThread(() -> { - LiveData<List<User>> liveData; final int constraintLength = constraint.toString().trim().length(); if (cardId == NO_CARD) { liveData = constraintLength > 0 - ? syncManager.searchUserByUidOrDisplayNameForACL(accountId, boardId, constraint.toString()) + ? liveSearchForACL.search(accountId, boardId, constraint.toString()) : syncManager.findProposalsForUsersToAssignForACL(accountId, boardId, activity.getResources().getInteger(R.integer.max_users_suggested)); } else { liveData = constraintLength > 0 ? syncManager.searchUserByUidOrDisplayName(accountId, boardId, cardId, constraint.toString()) : syncManager.findProposalsForUsersToAssign(accountId, boardId, cardId, activity.getResources().getInteger(R.integer.max_users_suggested)); } - observeOnce(liveData, activity, users -> { + liveData.removeObservers(activity); + observer = users -> { users.removeAll(itemsToExclude); filterResults.values = users; filterResults.count = users.size(); publishResults(constraint, filterResults); - }); + }; + liveData.observe(activity, observer); }); } return filterResults; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java index 08d960257..f95eea89f 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java @@ -8,11 +8,9 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.RecyclerView; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabActivitiesBinding; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; public class CardActivityFragment extends Fragment { @@ -39,17 +37,14 @@ public class CardActivityFragment extends Fragment { } if (!viewModel.isCreateMode()) { - final SyncManager syncManager = new SyncManager(requireContext()); - - syncManager.syncActivitiesForCard(viewModel.getFullCard().getCard()).observe(getViewLifecycleOwner(), (activities -> { + viewModel.syncActivitiesForCard(viewModel.getFullCard().getCard()).observe(getViewLifecycleOwner(), (activities -> { if (activities == null || activities.size() == 0) { binding.emptyContentView.setVisibility(View.VISIBLE); binding.activitiesList.setVisibility(View.GONE); } else { binding.emptyContentView.setVisibility(View.GONE); binding.activitiesList.setVisibility(View.VISIBLE); - RecyclerView.Adapter adapter = new CardActivityAdapter(activities, requireActivity().getMenuInflater()); - binding.activitiesList.setAdapter(adapter); + binding.activitiesList.setAdapter(new CardActivityAdapter(activities, requireActivity().getMenuInflater())); } })); } else { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java index 7d49b932d..6362f90dd 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java @@ -3,17 +3,18 @@ package it.niedermann.nextcloud.deck.ui.card.activities; import android.content.Context; import android.view.MenuInflater; import android.view.View; +import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import it.niedermann.android.util.ClipboardUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemActivityBinding; import it.niedermann.nextcloud.deck.model.enums.ActivityType; import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.util.DateUtil; - -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; +import it.niedermann.nextcloud.deck.util.ViewUtil; public class CardActivityViewHolder extends RecyclerView.ViewHolder { public ItemActivityBinding binding; @@ -26,38 +27,61 @@ public class CardActivityViewHolder extends RecyclerView.ViewHolder { public void bind(@NonNull Activity activity, @NonNull MenuInflater inflater) { final Context context = itemView.getContext(); - binding.date.setText(DateUtil.getRelativeDateTimeString(context, activity.getLastModified().getTime())); + binding.date.setText(DateUtil.getRelativeDateTimeString(context, activity.getLastModified().toEpochMilli())); binding.subject.setText(activity.getSubject()); itemView.setOnClickListener(View::showContextMenu); itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { inflater.inflate(R.menu.activity_menu, menu); - menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> copyToClipboard(context, activity.getSubject())); + menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(context, activity.getSubject())); }); - switch (ActivityType.findById(activity.getType())) { + final ActivityType type = ActivityType.findById(activity.getType()); + setImageResource(binding.type, type); + setImageColor(context, binding.type, type); + } + + private static void setImageResource(@NonNull ImageView imageView, @NonNull ActivityType type) { + switch (type) { case CHANGE: - binding.type.setImageResource(R.drawable.type_change_36dp); + imageView.setImageResource(R.drawable.type_change_36dp); break; case ADD: - binding.type.setImageResource(R.drawable.type_add_color_36dp); + imageView.setImageResource(R.drawable.type_add_color_36dp); break; case DELETE: - binding.type.setImageResource(R.drawable.type_delete_color_36dp); + imageView.setImageResource(R.drawable.type_delete_color_36dp); break; case ARCHIVE: - binding.type.setImageResource(R.drawable.type_archive_grey600_36dp); + imageView.setImageResource(R.drawable.type_archive_grey600_36dp); break; case TAGGED_WITH_LABEL: - binding.type.setImageResource(R.drawable.type_label_grey600_36dp); + imageView.setImageResource(R.drawable.type_label_grey600_36dp); break; case COMMENT: - binding.type.setImageResource(R.drawable.type_comment_grey600_36dp); + imageView.setImageResource(R.drawable.type_comment_grey600_36dp); break; case FILES: - binding.type.setImageResource(R.drawable.type_file_36dp); + imageView.setImageResource(R.drawable.type_file_36dp); + break; case HISTORY: - binding.type.setImageResource(R.drawable.type_file_36dp); + imageView.setImageResource(R.drawable.type_history_36dp); + break; case DECK: default: + imageView.setImageResource(R.drawable.ic_app_logo); + break; + } + } + + private static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @NonNull ActivityType type) { + switch (type) { + case ADD: + ViewUtil.setImageColor(context, imageView, R.color.activity_create); + break; + case DELETE: + ViewUtil.setImageColor(context, imageView, R.color.activity_delete); + break; + default: + ViewUtil.setImageColor(context, imageView, R.color.grey600); break; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java new file mode 100644 index 000000000..34d2eb3f3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java @@ -0,0 +1,113 @@ +package it.niedermann.nextcloud.deck.ui.card.assignee; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.swiperefreshlayout.widget.CircularProgressDrawable; + +import com.bumptech.glide.Glide; + +import java.io.Serializable; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.DialogPreviewBinding; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDeleteAlertDialogBuilder; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; +import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; + +import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; + +@Deprecated +public class CardAssigneeDialog extends BrandedDialogFragment { + + private static final String KEY_USER = "user"; + private DialogPreviewBinding binding; + private EditCardViewModel viewModel; + + @Nullable + private CardAssigneeListener cardAssigneeListener = null; + @SuppressWarnings("NotNullFieldNotInitialized") + @NonNull + private User user; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (getParentFragment() instanceof CardAssigneeListener) { + this.cardAssigneeListener = (CardAssigneeListener) getParentFragment(); + } else if (context instanceof CardAssigneeListener) { + this.cardAssigneeListener = (CardAssigneeListener) context; + } + + final Bundle args = requireArguments(); + if (!args.containsKey(KEY_USER)) { + throw new IllegalArgumentException("Provide at least " + KEY_USER); + } + final Serializable user = args.getSerializable(KEY_USER); + if (user == null) { + throw new IllegalArgumentException(KEY_USER + " must not be null."); + } + this.user = (User) user; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext())); + viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + + AlertDialog.Builder dialogBuilder = new BrandedDeleteAlertDialogBuilder(requireContext()); + + if (viewModel.canEdit() && cardAssigneeListener != null) { + dialogBuilder.setPositiveButton(R.string.simple_unassign, (d, w) -> cardAssigneeListener.onUnassignUser(user)); + } + + return dialogBuilder + .setView(binding.getRoot()) + .setNeutralButton(R.string.simple_close, null) + .create(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Context context = requireContext(); + + final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(context); + circularProgressDrawable.setStrokeWidth(5f); + circularProgressDrawable.setCenterRadius(30f); + circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY); + circularProgressDrawable.start(); + + binding.avatar.post(() -> Glide.with(binding.avatar.getContext()) + .load(viewModel.getAccount().getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + binding.avatar.getWidth()) + .placeholder(circularProgressDrawable) + .error(R.drawable.ic_person_grey600_24dp) + .into(binding.avatar)); + binding.title.setText(user.getDisplayname()); + } + + @Override + public void applyBrand(int mainColor) { + } + + public static DialogFragment newInstance(@NonNull User user) { + final DialogFragment fragment = new CardAssigneeDialog(); + final Bundle args = new Bundle(); + args.putSerializable(KEY_USER, user); + fragment.setArguments(args); + return fragment; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java new file mode 100644 index 000000000..259a8b57c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java @@ -0,0 +1,11 @@ +package it.niedermann.nextcloud.deck.ui.card.assignee; + +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.User; + +public interface CardAssigneeListener { + + void onUnassignUser(@NonNull User user); + +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java index c236fa4c5..2d3ece255 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java @@ -3,5 +3,5 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; import it.niedermann.nextcloud.deck.model.Attachment; public interface AttachmentDeletedListener { - void onAttachmentDeleted(Attachment attachment); - }
\ No newline at end of file + void onAttachmentDeleted(Attachment attachment); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java index ed3031b7c..533bbe322 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java @@ -1,18 +1,59 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; +import android.view.MenuInflater; import android.view.View; import android.widget.ImageView; +import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; +import it.niedermann.android.util.ClipboardUtil; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.model.enums.DBStatus; +import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; + public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder { AttachmentViewHolder(@NonNull View itemView) { super(itemView); } + public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) { + bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment)); + } + + @CallSuper + public void bind(@NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor, @Nullable String attachmentUri) { + setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), mainColor); + itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { + menuInflater.inflate(R.menu.attachment_menu, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> { + DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName()); + return false; + }); + if (attachmentUri == null || attachment.getId() == null || cardRemoteId == null) { + menu.findItem(android.R.id.copyUrl).setVisible(false); + } else { + menu.findItem(android.R.id.copyUrl).setVisible(true); + menu.findItem(android.R.id.copyUrl).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), attachment.getFilename(), attachmentUri)); + } + }); + } + abstract protected ImageView getPreview(); - abstract protected void setNotSyncedYetStatus(boolean synced, @ColorInt int color); + protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) { + final ImageView notSyncedYet = getNotSyncedYetStatusIcon(); + DrawableCompat.setTint(notSyncedYet.getDrawable(), BrandingUtil.getSecondaryForegroundColorDependingOnTheme(notSyncedYet.getContext(), mainColor)); + notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE); + } + + abstract protected ImageView getNotSyncedYetStatusIcon(); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java index a72ed8ef2..d601f6bbd 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java @@ -3,9 +3,7 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.Build; -import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.View; @@ -16,10 +14,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityOptionsCompat; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; - import java.util.ArrayList; import java.util.List; @@ -28,21 +26,23 @@ import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; -import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.ui.attachments.AttachmentsActivity; -import it.niedermann.nextcloud.deck.util.AttachmentUtil; -import it.niedermann.nextcloud.deck.util.DateUtil; +import it.niedermann.nextcloud.deck.ui.branding.Branded; import it.niedermann.nextcloud.deck.util.MimeTypeUtil; +import static androidx.lifecycle.Transformations.distinctUntilChanged; import static androidx.recyclerview.widget.RecyclerView.NO_ID; -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; +import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser; @SuppressWarnings("WeakerAccess") -public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> { +public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> implements Branded { public static final int VIEW_TYPE_DEFAULT = 2; public static final int VIEW_TYPE_IMAGE = 1; + @NonNull + private final MutableLiveData<Boolean> isEmpty = new MutableLiveData<>(true); + @NonNull private final MenuInflater menuInflater; @ColorInt private int mainColor; @@ -51,17 +51,16 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo private Long cardRemoteId = null; private final long cardLocalId; @NonNull - FragmentManager fragmentManager; + private final FragmentManager fragmentManager; + @NonNull + private final List<Attachment> attachments = new ArrayList<>(); @NonNull - private List<Attachment> attachments = new ArrayList<>(); - @Nullable private final AttachmentClickedListener attachmentClickedListener; CardAttachmentAdapter( - @NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull MenuInflater menuInflater, - @Nullable AttachmentClickedListener attachmentClickedListener, + @NonNull AttachmentClickedListener attachmentClickedListener, @NonNull Account account, @Nullable Long cardLocalId ) { @@ -95,39 +94,14 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo @Override public void onBindViewHolder(@NonNull AttachmentViewHolder holder, int position) { - final Context context = holder.itemView.getContext(); final Attachment attachment = attachments.get(position); - final int viewType = getItemViewType(position); - - @Nullable final String uri = (attachment.getId() == null || cardRemoteId == null) - ? attachment.getLocalPath() : - AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId()); - holder.setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), mainColor); - holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { - menuInflater.inflate(R.menu.attachment_menu, menu); - menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> { - DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName()); - return false; - }); - if (uri == null) { - menu.findItem(android.R.id.copyUrl).setVisible(false); - } else { - menu.findItem(android.R.id.copyUrl).setOnMenuItemClickListener(item -> copyToClipboard(context, attachment.getFilename(), uri)); - } - }); + final Context context = holder.itemView.getContext(); + final View.OnClickListener onClickListener; - switch (viewType) { + switch (getItemViewType(position)) { case VIEW_TYPE_IMAGE: { - holder.getPreview().setImageResource(R.drawable.ic_image_grey600_24dp); - Glide.with(context) - .load(uri) - .placeholder(R.drawable.ic_image_grey600_24dp) - .error(R.drawable.ic_image_grey600_24dp) - .into(holder.getPreview()); - holder.itemView.setOnClickListener((v) -> { - if (attachmentClickedListener != null) { - attachmentClickedListener.onAttachmentClicked(position); - } + onClickListener = (event) -> { + attachmentClickedListener.onAttachmentClicked(position); final Intent intent = AttachmentsActivity.createIntent(context, account, cardLocalId, attachment.getLocalId()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && context instanceof Activity) { String transitionName = context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId())); @@ -136,46 +110,16 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo } else { context.startActivity(intent); } - }); + }; break; } case VIEW_TYPE_DEFAULT: default: { - DefaultAttachmentViewHolder defaultHolder = (DefaultAttachmentViewHolder) holder; - - if (MimeTypeUtil.isAudio(attachment.getMimetype())) { - holder.getPreview().setImageResource(R.drawable.ic_music_note_grey600_24dp); - } else if (MimeTypeUtil.isVideo(attachment.getMimetype())) { - holder.getPreview().setImageResource(R.drawable.ic_local_movies_grey600_24dp); - } else if (MimeTypeUtil.isPdf(attachment.getMimetype())) { - holder.getPreview().setImageResource(R.drawable.ic_baseline_picture_as_pdf_24); - } else if (MimeTypeUtil.isContact(attachment.getMimetype())) { - holder.getPreview().setImageResource(R.drawable.ic_baseline_contact_mail_24); - } else { - holder.getPreview().setImageResource(R.drawable.ic_attach_file_grey600_24dp); - } - - if (cardRemoteId != null) { - defaultHolder.itemView.setOnClickListener((event) -> { - Intent openURL = new Intent(Intent.ACTION_VIEW); - openURL.setData(Uri.parse(AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId()))); - context.startActivity(openURL); - }); - } - defaultHolder.binding.filename.setText(attachment.getBasename()); - defaultHolder.binding.filesize.setText(Formatter.formatFileSize(context, attachment.getFilesize())); - if (attachment.getLastModifiedLocal() != null) { - defaultHolder.binding.modified.setText(DateUtil.getRelativeDateTimeString(context, attachment.getLastModifiedLocal().getTime())); - defaultHolder.binding.modified.setVisibility(View.VISIBLE); - } else if (attachment.getLastModified() != null) { - defaultHolder.binding.modified.setText(DateUtil.getRelativeDateTimeString(context, attachment.getLastModified().getTime())); - defaultHolder.binding.modified.setVisibility(View.VISIBLE); - } else { - defaultHolder.binding.modified.setVisibility(View.GONE); - } + onClickListener = (event) -> openAttachmentInBrowser(context, account.getUrl(), cardRemoteId, attachment.getId()); break; } } + holder.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor); } @Override @@ -188,21 +132,46 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo return attachments.size(); } + private void updateIsEmpty() { + this.isEmpty.postValue(getItemCount() <= 0); + } + + @NonNull + public LiveData<Boolean> isEmpty() { + return distinctUntilChanged(this.isEmpty); + } + public void setAttachments(@NonNull List<Attachment> attachments, @Nullable Long cardRemoteId) { this.cardRemoteId = cardRemoteId; this.attachments.clear(); this.attachments.addAll(attachments); notifyDataSetChanged(); + this.updateIsEmpty(); } public void addAttachment(Attachment a) { - this.attachments.add(a); + this.attachments.add(0, a); notifyItemInserted(this.attachments.size()); + this.updateIsEmpty(); } public void removeAttachment(Attachment a) { final int index = this.attachments.indexOf(a); this.attachments.remove(a); notifyItemRemoved(index); + this.updateIsEmpty(); + } + + public void replaceAttachment(Attachment toReplace, Attachment with) { + final int index = this.attachments.indexOf(toReplace); + this.attachments.remove(toReplace); + this.attachments.add(index, with); + notifyItemChanged(index); + } + + @Override + public void applyBrand(@ColorInt int mainColor) { + this.mainColor = mainColor; + notifyDataSetChanged(); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java new file mode 100644 index 000000000..6b60bbffd --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java @@ -0,0 +1,91 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments; + +import android.content.Context; +import android.view.View; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.core.content.ContextCompat; + +import com.google.android.material.animation.ArgbEvaluatorCompat; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import it.niedermann.android.util.DimensionUtil; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN; + +public class CardAttachmentsBottomsheetBehaviorCallback extends BottomSheetBehavior.BottomSheetCallback { + @NonNull + private final OnBackPressedCallback backPressedCallback; + @NonNull + private final FloatingActionButton fab; + @NonNull + private final View pickerBackdrop; + @NonNull + private final BottomNavigationView bottomNavigation; + @ColorInt + private final int backdropColorExpanded; + @ColorInt + private final int backdropColorCollapsed; + @Px + private final int bottomNavigationHeight; + + private float lastOffset = -1; + + public CardAttachmentsBottomsheetBehaviorCallback(@NonNull Context context, + @NonNull OnBackPressedCallback backPressedCallback, + @NonNull FloatingActionButton fab, + @NonNull View pickerBackdrop, + @NonNull BottomNavigationView bottomNavigation, + @ColorRes int backdropColorExpanded, + @ColorRes int backdropColorCollapsed, + @DimenRes int bottomNavigationHeight + ) { + this.backPressedCallback = backPressedCallback; + this.fab = fab; + this.pickerBackdrop = pickerBackdrop; + this.bottomNavigation = bottomNavigation; + this.backdropColorExpanded = ContextCompat.getColor(context, backdropColorExpanded); + this.backdropColorCollapsed = ContextCompat.getColor(context, backdropColorCollapsed); + this.bottomNavigationHeight = DimensionUtil.INSTANCE.dpToPx(context, bottomNavigationHeight); + } + + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + if (newState == STATE_HIDDEN) { + backPressedCallback.setEnabled(false); + if (pickerBackdrop.getVisibility() != GONE) { + pickerBackdrop.setVisibility(GONE); + } + } else if (pickerBackdrop.getVisibility() != VISIBLE) { + pickerBackdrop.setVisibility(VISIBLE); + } + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + if (slideOffset <= 0) { + final float bottomSheetPercentageShown = slideOffset * -1; + pickerBackdrop.setBackgroundColor(ArgbEvaluatorCompat.getInstance().evaluate(bottomSheetPercentageShown, backdropColorExpanded, backdropColorCollapsed)); + bottomNavigation.setTranslationY(bottomSheetPercentageShown * bottomNavigationHeight); + if (slideOffset <= lastOffset && slideOffset != 0) { + if (fab.getVisibility() == GONE) { + fab.show(); + } + } else { + if (fab.getVisibility() == VISIBLE) { + fab.hide(); + } + } + } + lastOffset = slideOffset; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java index c291c5bdf..07e8d39dd 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java @@ -1,35 +1,42 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; -import android.Manifest; -import android.app.Activity; import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; import android.net.Uri; -import android.os.Build; import android.os.Bundle; +import android.provider.ContactsContract; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.core.app.SharedElementCallback; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.snackbar.Snackbar; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import java.io.File; import java.io.IOException; -import java.util.Date; +import java.time.Instant; import java.util.List; import java.util.Map; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabAttachmentsBinding; @@ -41,10 +48,36 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiv import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; import it.niedermann.nextcloud.deck.ui.branding.BrandedSnackbar; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; +import it.niedermann.nextcloud.deck.ui.card.attachments.picker.AbstractPickerAdapter; +import it.niedermann.nextcloud.deck.ui.card.attachments.picker.ContactAdapter; +import it.niedermann.nextcloud.deck.ui.card.attachments.picker.FileAdapter; +import it.niedermann.nextcloud.deck.ui.card.attachments.picker.FileAdapterLegacy; +import it.niedermann.nextcloud.deck.ui.card.attachments.picker.GalleryAdapter; +import it.niedermann.nextcloud.deck.ui.card.attachments.picker.GalleryItemDecoration; +import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialog; +import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialogViewModel; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; - +import it.niedermann.nextcloud.deck.ui.takephoto.TakePhotoActivity; +import it.niedermann.nextcloud.deck.util.DeckColorUtil; +import it.niedermann.nextcloud.deck.util.VCardUtil; + +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.READ_CONTACTS; +import static android.Manifest.permission.READ_EXTERNAL_STORAGE; +import static android.app.Activity.RESULT_OK; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.M; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED; +import static androidx.core.content.PermissionChecker.checkSelfPermission; +import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED; +import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB; +import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; +import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_DEFAULT; import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_IMAGE; import static it.niedermann.nextcloud.deck.util.AttachmentUtil.copyContentUriToTempFile; @@ -53,14 +86,35 @@ import static java.net.HttpURLConnection.HTTP_CONFLICT; public class CardAttachmentsFragment extends BrandedFragment implements AttachmentDeletedListener, AttachmentClickedListener { private FragmentCardEditTabAttachmentsBinding binding; - private EditCardViewModel viewModel; + private EditCardViewModel editViewModel; + private PreviewDialogViewModel previewViewModel; + private BottomSheetBehavior<LinearLayout> mBottomSheetBehaviour; + + private RecyclerView.ItemDecoration galleryItemDecoration; + + private static final int REQUEST_CODE_PICK_FILE = 1; + private static final int REQUEST_CODE_PICK_FILE_PERMISSION = 2; + private static final int REQUEST_CODE_PICK_CAMERA = 3; + private static final int REQUEST_CODE_PICK_GALLERY_PERMISSION = 4; + private static final int REQUEST_CODE_PICK_CONTACT = 5; + private static final int REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION = 6; - private static final int REQUEST_CODE_ADD_ATTACHMENT = 1; - private static final int REQUEST_PERMISSION = 2; + @ColorInt + private int accentColor; + @ColorInt + private int primaryColor; - private SyncManager syncManager; private CardAttachmentAdapter adapter; + private AbstractPickerAdapter<?> pickerAdapter; + + private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + mBottomSheetBehaviour.setState(STATE_HIDDEN); + } + }; + private int clickedItemPosition; @Override @@ -69,30 +123,58 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme Bundle savedInstanceState) { binding = FragmentCardEditTabAttachmentsBinding.inflate(inflater, container, false); - viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + editViewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + previewViewModel = new ViewModelProvider(requireActivity()).get(PreviewDialogViewModel.class); + binding.bottomNavigation.setOnNavigationItemSelectedListener(item -> { + if (item.getItemId() == R.id.gallery) { + showGalleryPicker(); + } else if (item.getItemId() == R.id.contacts) { + showContactPicker(); + } else if (item.getItemId() == R.id.files) { + showFilePicker(); + } + return true; + }); + accentColor = ContextCompat.getColor(requireContext(), R.color.accent); + primaryColor = ContextCompat.getColor(requireContext(), R.color.primary); // This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance // See https://github.com/stefan-niedermann/nextcloud-deck/issues/478 - if (viewModel.getFullCard() == null) { + if (editViewModel.getFullCard() == null) { DeckLog.logError(new IllegalStateException("Cannot populate " + CardAttachmentsFragment.class.getSimpleName() + " because viewModel.getFullCard() is null")); return binding.getRoot(); } - syncManager = new SyncManager(requireContext()); adapter = new CardAttachmentAdapter( - requireContext(), getChildFragmentManager(), requireActivity().getMenuInflater(), this, - viewModel.getAccount(), - viewModel.getFullCard().getLocalId()); + editViewModel.getAccount(), + editViewModel.getFullCard().getLocalId()); binding.attachmentsList.setAdapter(adapter); - updateEmptyContentView(); + adapter.isEmpty().observe(getViewLifecycleOwner(), (isEmpty) -> { + if (isEmpty) { + this.binding.emptyContentView.setVisibility(VISIBLE); + this.binding.attachmentsList.setVisibility(GONE); + } else { + this.binding.emptyContentView.setVisibility(GONE); + this.binding.attachmentsList.setVisibility(VISIBLE); + } + }); + galleryItemDecoration = new GalleryItemDecoration(DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1qx)); + mBottomSheetBehaviour = BottomSheetBehavior.from(binding.bottomSheetParent); + mBottomSheetBehaviour.setDraggable(true); + mBottomSheetBehaviour.setHideable(true); + mBottomSheetBehaviour.setState(STATE_HIDDEN); + mBottomSheetBehaviour.addBottomSheetCallback(new CardAttachmentsBottomsheetBehaviorCallback( + requireContext(), backPressedCallback, binding.fab, binding.pickerBackdrop, binding.bottomNavigation, + R.color.mdtp_transparent_black, android.R.color.transparent, R.dimen.attachments_bottom_navigation_height)); + binding.pickerBackdrop.setOnClickListener(v -> mBottomSheetBehaviour.setState(STATE_HIDDEN)); final DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); - int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column)); - GridLayoutManager glm = new GridLayoutManager(getContext(), spanCount); + final int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column)); + final GridLayoutManager glm = new GridLayoutManager(getContext(), spanCount); glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { @@ -106,7 +188,7 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme } }); binding.attachmentsList.setLayoutManager(glm); - if (!viewModel.isCreateMode()) { + if (!editViewModel.isCreateMode()) { // https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html?m=1 // https://github.com/android/animation-samples/blob/master/GridToPager/app/src/main/java/com/google/samples/gridtopager/fragment/ImagePagerFragment.java setExitSharedElementCallback(new SharedElementCallback() { @@ -119,17 +201,19 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme } } }); - adapter.setAttachments(viewModel.getFullCard().getAttachments(), viewModel.getFullCard().getId()); - updateEmptyContentView(); + adapter.setAttachments(editViewModel.getFullCard().getAttachments(), editViewModel.getFullCard().getId()); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && viewModel.canEdit()) { + if (editViewModel.canEdit()) { binding.fab.setOnClickListener(v -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERMISSION); + if (SDK_INT < LOLLIPOP) { + openNativeFilePicker(); } else { - startFilePickerIntent(); + binding.bottomNavigation.setSelectedItemId(R.id.gallery); + showGalleryPicker(); + mBottomSheetBehaviour.setState(STATE_COLLAPSED); + backPressedCallback.setEnabled(true); + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback); } }); binding.fab.show(); @@ -146,116 +230,281 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme binding.fab.hide(); binding.emptyContentView.hideDescription(); } + @Nullable Context context = requireContext(); + applyBrand(isBrandingEnabled(context) + ? readBrandMainColor(context) + : ContextCompat.getColor(context, R.color.defaultBrand)); return binding.getRoot(); } - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - private void startFilePickerIntent() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - startActivityForResult(intent, REQUEST_CODE_ADD_ATTACHMENT); + @Override + public void onPause() { + super.onPause(); + backPressedCallback.setEnabled(false); } @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE_ADD_ATTACHMENT && resultCode == Activity.RESULT_OK) { - if (data == null) { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - return; + public void onResume() { + super.onResume(); + backPressedCallback.setEnabled(binding.bottomNavigation.getTranslationY() == 0); + } + + private void showGalleryPicker() { + if (!(pickerAdapter instanceof GalleryAdapter)) { + if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE) || isPermissionRequestNeeded(CAMERA)) { + requestPermissions(new String[]{READ_EXTERNAL_STORAGE, CAMERA}, REQUEST_CODE_PICK_GALLERY_PERMISSION); + } else { + unbindPickerAdapter(); + pickerAdapter = new GalleryAdapter(requireContext(), (uri, pair) -> { + previewViewModel.prepareDialog(pair.first, pair.second); + PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName()); + observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> { + if (submitPositive) { + onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)); + } + }); + }, this::openNativeCameraPicker, getViewLifecycleOwner()); + if (binding.pickerRecyclerView.getItemDecorationCount() == 0) { + binding.pickerRecyclerView.addItemDecoration(galleryItemDecoration); + } + binding.pickerRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3)); + binding.pickerRecyclerView.setAdapter(pickerAdapter); } - final Uri sourceUri = data.getData(); - if (sourceUri == null) { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("sourceUri is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - return; + } + } + + private void showContactPicker() { + if (!(pickerAdapter instanceof ContactAdapter)) { + if (isPermissionRequestNeeded(READ_CONTACTS)) { + requestPermissions(new String[]{READ_CONTACTS}, REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION); + } else { + unbindPickerAdapter(); + pickerAdapter = new ContactAdapter(requireContext(), (uri, pair) -> { + previewViewModel.prepareDialog(pair.first, pair.second); + PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName()); + observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> { + if (submitPositive) { + onActivityResult(REQUEST_CODE_PICK_CONTACT, RESULT_OK, new Intent().setData(uri)); + } + }); + }, this::openNativeContactPicker); + removeGalleryItemDecoration(); + binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.pickerRecyclerView.setAdapter(pickerAdapter); } - if (!ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - return; + } + } + + private void showFilePicker() { + if (!(pickerAdapter instanceof FileAdapter) && !(pickerAdapter instanceof FileAdapterLegacy)) { + if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE)) { + requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, REQUEST_CODE_PICK_FILE_PERMISSION); + } else { + unbindPickerAdapter(); + if (SDK_INT >= LOLLIPOP) { +// if (SDK_INT >= Build.VERSION_CODES.Q) { +// // TODO Only usable with Scoped Storage +// pickerAdapter = new FileAdapter(requireContext(), uri -> onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)), this::openNativeFilePicker); +// } else { + pickerAdapter = new FileAdapterLegacy((uri, pair) -> { + previewViewModel.prepareDialog(pair.first, pair.second); + PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName()); + observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> { + if (submitPositive) { + onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)); + } + }); + }, this::openNativeFilePicker); +// } + removeGalleryItemDecoration(); + binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.pickerRecyclerView.setAdapter(pickerAdapter); + } } + } + } - DeckLog.verbose("--- found content URL " + sourceUri.getPath()); - File fileToUpload; + private void openNativeCameraPicker() { + if (SDK_INT >= LOLLIPOP) { + startActivityForResult(TakePhotoActivity.createIntent(requireContext()), REQUEST_CODE_PICK_CAMERA); + } else { + ExceptionDialogFragment.newInstance(new UnsupportedOperationException("This feature requires Android 5"), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + } - try { - DeckLog.verbose("---- so, now copy & upload: " + sourceUri.getPath()); - fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, viewModel.getAccount().getId(), viewModel.getFullCard().getCard().getLocalId()); - } catch (IllegalArgumentException | IOException e) { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Could not copy content URI to temporary file", e), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - return; - } + private void openNativeContactPicker() { + final Intent intent = new Intent(Intent.ACTION_PICK).setType(ContactsContract.Contacts.CONTENT_TYPE); + if (intent.resolveActivity(requireContext().getPackageManager()) != null) { + startActivityForResult(intent, REQUEST_CODE_PICK_CONTACT); + } + } + + private void openNativeFilePicker() { + startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*"), REQUEST_CODE_PICK_FILE); + } + + /** + * Checks the current Android version and whether the permission has already been granted. + * + * @param permission see {@link android.Manifest.permission} + * @return whether or not requesting permission is needed + */ + private boolean isPermissionRequestNeeded(@NonNull String permission) { + return SDK_INT >= M && checkSelfPermission(requireActivity(), permission) != PERMISSION_GRANTED; + } + + private void unbindPickerAdapter() { + if (pickerAdapter != null) { + pickerAdapter.onDestroy(); + } + } + + private void removeGalleryItemDecoration() { + if (binding.pickerRecyclerView.getItemDecorationCount() > 0) { + binding.pickerRecyclerView.removeItemDecoration(galleryItemDecoration); + } + } - for (Attachment existingAttachment : viewModel.getFullCard().getAttachments()) { - final String existingPath = existingAttachment.getLocalPath(); - if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) { - BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show(); - return; + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case REQUEST_CODE_PICK_CONTACT: + case REQUEST_CODE_PICK_CAMERA: + case REQUEST_CODE_PICK_FILE: { + if (resultCode == RESULT_OK) { + final Uri sourceUri = requestCode == REQUEST_CODE_PICK_CONTACT + ? VCardUtil.getVCardContentUri(requireContext(), Uri.parse(data.getDataString())) + : data.getData(); + try { + uploadNewAttachmentFromUri(sourceUri, requestCode == REQUEST_CODE_PICK_CAMERA + ? data.getType() + : requireContext().getContentResolver().getType(sourceUri)); + mBottomSheetBehaviour.setState(STATE_HIDDEN); + } catch (Exception e) { + ExceptionDialogFragment.newInstance(e, editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } } + break; + } + default: { + super.onActivityResult(requestCode, resultCode, data); } + } + } + + @Override + public void onDestroy() { + if (this.pickerAdapter != null) { + this.pickerAdapter.onDestroy(); + this.binding.pickerRecyclerView.setAdapter(null); + } + super.onDestroy(); + } - final Date now = new Date(); - final Attachment a = new Attachment(); - a.setMimetype(requireContext().getContentResolver().getType(sourceUri)); - a.setData(fileToUpload.getName()); - a.setFilename(fileToUpload.getName()); - a.setBasename(fileToUpload.getName()); - a.setFilesize(fileToUpload.length()); - a.setLocalPath(fileToUpload.getAbsolutePath()); - a.setLastModifiedLocal(now); - a.setStatusEnum(DBStatus.LOCAL_EDITED); - a.setCreatedAt(now); - viewModel.getFullCard().getAttachments().add(a); - adapter.addAttachment(a); - if (!viewModel.isCreateMode()) { - WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload); - observeOnce(liveData, getViewLifecycleOwner(), (next) -> { - if (liveData.hasError()) { - Throwable t = liveData.getError(); - if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_CONFLICT) { - // https://github.com/stefan-niedermann/nextcloud-deck/issues/534 - viewModel.getFullCard().getAttachments().remove(a); - adapter.removeAttachment(a); - BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show(); + private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) throws UploadAttachmentFailedException, IOException { + if (sourceUri == null) { + throw new UploadAttachmentFailedException("sourceUri is null"); + } + switch (sourceUri.getScheme()) { + case ContentResolver.SCHEME_CONTENT: + case ContentResolver.SCHEME_FILE: { + DeckLog.verbose("--- found content URL " + sourceUri.getPath()); + final File fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId()); + for (Attachment existingAttachment : editViewModel.getFullCard().getAttachments()) { + final String existingPath = existingAttachment.getLocalPath(); + if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) { + BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show(); + return; + } + } + final Instant now = Instant.now(); + final Attachment a = new Attachment(); + a.setMimetype(mimeType); + a.setData(fileToUpload.getName()); + a.setFilename(fileToUpload.getName()); + a.setBasename(fileToUpload.getName()); + a.setFilesize(fileToUpload.length()); + a.setLocalPath(fileToUpload.getAbsolutePath()); + a.setLastModifiedLocal(now); + a.setCreatedAt(now); + a.setStatusEnum(DBStatus.LOCAL_EDITED); + editViewModel.getFullCard().getAttachments().add(0, a); + adapter.addAttachment(a); + if (!editViewModel.isCreateMode()) { + WrappedLiveData<Attachment> liveData = editViewModel.addAttachmentToCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload); + observeOnce(liveData, getViewLifecycleOwner(), (next) -> { + if (liveData.hasError()) { + Throwable t = liveData.getError(); + if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_CONFLICT) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/534 + editViewModel.getFullCard().getAttachments().remove(a); + adapter.removeAttachment(a); + BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show(); + } else { + ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } } else { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + editViewModel.getFullCard().getAttachments().remove(a); + editViewModel.getFullCard().getAttachments().add(0, next); + adapter.replaceAttachment(a, next); } - } else { - viewModel.getFullCard().getAttachments().remove(a); - adapter.removeAttachment(a); - viewModel.getFullCard().getAttachments().add(next); - adapter.addAttachment(next); - } - }); + }); + } + break; + } + default: { + throw new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()); } - updateEmptyContentView(); } - } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == REQUEST_PERMISSION) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - startFilePickerIntent(); + switch (requestCode) { + case REQUEST_CODE_PICK_FILE_PERMISSION: { + if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) { + showFilePicker(); + } else { + Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); + } + break; } - } else { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); + case REQUEST_CODE_PICK_GALLERY_PERMISSION: { + if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED && checkSelfPermission(requireActivity(), CAMERA) == PERMISSION_GRANTED) { + showGalleryPicker(); + } else { + Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); + } + break; + } + case REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION: { + if (checkSelfPermission(requireActivity(), READ_CONTACTS) == PERMISSION_GRANTED) { + showContactPicker(); + } else { + Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); + } + break; + } + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); } } - public static Fragment newInstance() { - return new CardAttachmentsFragment(); - } - @Override public void onAttachmentDeleted(Attachment attachment) { adapter.removeAttachment(attachment); - viewModel.getFullCard().getAttachments().remove(attachment); - if (!viewModel.isCreateMode() && attachment.getLocalId() != null) { - syncManager.deleteAttachmentOfCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), attachment.getLocalId()); + editViewModel.getFullCard().getAttachments().remove(attachment); + if (!editViewModel.isCreateMode() && attachment.getLocalId() != null) { + final WrappedLiveData<Void> deleteLiveData = editViewModel.deleteAttachmentOfCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), attachment.getLocalId()); + observeOnce(deleteLiveData, this, (next) -> { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } - updateEmptyContentView(); } @Override @@ -263,19 +512,28 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme this.clickedItemPosition = position; } - - private void updateEmptyContentView() { - if (this.adapter == null || this.adapter.getItemCount() == 0) { - this.binding.emptyContentView.setVisibility(View.VISIBLE); - this.binding.attachmentsList.setVisibility(View.GONE); - } else { - this.binding.emptyContentView.setVisibility(View.GONE); - this.binding.attachmentsList.setVisibility(View.VISIBLE); - } - } - @Override public void applyBrand(int mainColor) { applyBrandToFAB(mainColor, binding.fab); + adapter.applyBrand(mainColor); + @ColorInt final int finalMainColor = DeckColorUtil.contrastRatioIsSufficient(mainColor, primaryColor) + ? mainColor + : accentColor; + final ColorStateList list = new ColorStateList( + new int[][]{ + new int[]{android.R.attr.state_checked}, + new int[]{} + }, + new int[]{ + finalMainColor, + accentColor + } + ); + binding.bottomNavigation.setItemIconTintList(list); + binding.bottomNavigation.setItemTextColor(list); + } + + public static Fragment newInstance() { + return new CardAttachmentsFragment(); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java index 7acdd390e..2b5358eb9 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java @@ -1,15 +1,25 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; +import android.text.format.Formatter; +import android.view.MenuInflater; import android.view.View; import android.widget.ImageView; import androidx.annotation.ColorInt; -import androidx.core.graphics.drawable.DrawableCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.util.DateUtil; + +import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType; +import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser; public class DefaultAttachmentViewHolder extends AttachmentViewHolder { - ItemAttachmentDefaultBinding binding; + private final ItemAttachmentDefaultBinding binding; @SuppressWarnings("WeakerAccess") public DefaultAttachmentViewHolder(ItemAttachmentDefaultBinding binding) { @@ -23,8 +33,24 @@ public class DefaultAttachmentViewHolder extends AttachmentViewHolder { } @Override - protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) { - DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor); - binding.notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE); + protected ImageView getNotSyncedYetStatusIcon() { + return binding.notSyncedYet; + } + + public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) { + super.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor); + getPreview().setImageResource(getIconForMimeType(attachment.getMimetype())); + itemView.setOnClickListener((event) -> openAttachmentInBrowser(itemView.getContext(), account.getUrl(), cardRemoteId, attachment.getId())); + binding.filename.setText(attachment.getBasename()); + binding.filesize.setText(Formatter.formatFileSize(binding.filesize.getContext(), attachment.getFilesize())); + if (attachment.getLastModifiedLocal() != null) { + binding.modified.setText(DateUtil.getRelativeDateTimeString(binding.modified.getContext(), attachment.getLastModifiedLocal().toEpochMilli())); + binding.modified.setVisibility(View.VISIBLE); + } else if (attachment.getLastModified() != null) { + binding.modified.setText(DateUtil.getRelativeDateTimeString(binding.modified.getContext(), attachment.getLastModified().toEpochMilli())); + binding.modified.setVisibility(View.VISIBLE); + } else { + binding.modified.setVisibility(View.GONE); + } } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java index d13675a30..3c95da1b7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java @@ -1,15 +1,24 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; +import android.view.MenuInflater; import android.view.View; import android.widget.ImageView; import androidx.annotation.ColorInt; -import androidx.core.graphics.drawable.DrawableCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; +import com.bumptech.glide.Glide; + +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; public class ImageAttachmentViewHolder extends AttachmentViewHolder { - private ItemAttachmentImageBinding binding; + private final ItemAttachmentImageBinding binding; @SuppressWarnings("WeakerAccess") public ImageAttachmentViewHolder(ItemAttachmentImageBinding binding) { @@ -23,8 +32,22 @@ public class ImageAttachmentViewHolder extends AttachmentViewHolder { } @Override - protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) { - DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor); - binding.notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE); + protected ImageView getNotSyncedYetStatusIcon() { + return binding.notSyncedYet; + } + + public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) { + super.bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment)); + + getPreview().post(() -> { + @Nullable final String uri = AttachmentUtil.getThumbnailUrl(account.getServerDeckVersionAsObject(), account.getUrl(), cardRemoteId, attachment, getPreview().getWidth()); + Glide.with(getPreview().getContext()) + .load(uri) + .placeholder(R.drawable.ic_image_grey600_24dp) + .error(R.drawable.ic_image_grey600_24dp) + .into(getPreview()); + }); + + itemView.setOnClickListener(onClickListener); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java new file mode 100644 index 000000000..a2ea6dd37 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java @@ -0,0 +1,100 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.RequestBuilder; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.BiConsumer; + +import static android.database.Cursor.FIELD_TYPE_INTEGER; +import static android.database.Cursor.FIELD_TYPE_NULL; +import static androidx.recyclerview.widget.RecyclerView.NO_ID; +import static java.util.Objects.requireNonNull; + +/** + * An {@link RecyclerView.Adapter} which provides previews of one type of files and also an option to open a native dialog. + * <p> + * Example: Previews for images of the gallery as well a one option to take a photo + */ +public abstract class AbstractCursorPickerAdapter<T extends RecyclerView.ViewHolder> extends AbstractPickerAdapter<T> { + + private final int count; + protected final int columnIndex; + private final int columnIndexType; + @NonNull + protected final BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect; + @NonNull + protected final Runnable openNativePicker; + @NonNull + protected final Cursor cursor; + @NonNull + protected final ContentResolver contentResolver; + + /** + * Should be used to bind heavy operations like when dealing with {@link Bitmap}. + * This must only be one {@link Thread} because otherwise the cursor might change while fetching data from it. + */ + @NonNull + protected final ExecutorService bindExecutor = Executors.newFixedThreadPool(1); + + public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, Uri subject, String idColumn, String sortOrder) { + this(context, onSelect, openNativePicker, subject, idColumn, new String[]{idColumn}, sortOrder); + } + + public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, Uri subject, String idColumn, String[] requestedColumns, String sortOrder) { + this(context, onSelect, openNativePicker, idColumn, requireNonNull(context.getContentResolver().query(subject, requestedColumns, null, null, sortOrder))); + } + + public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, String idColumn, @NonNull Cursor cursor) { + this.contentResolver = context.getContentResolver(); + this.onSelect = onSelect; + this.openNativePicker = openNativePicker; + this.cursor = cursor; + this.cursor.moveToFirst(); + this.columnIndex = this.cursor.getColumnIndex(idColumn); + this.count = cursor.getCount() + 1; + this.columnIndexType = (this.count > 1) ? this.cursor.getType(columnIndex) : FIELD_TYPE_NULL; + setHasStableIds(true); + } + + /** + * Moves the {@link #cursor} to the given position + */ + @Override + public long getItemId(int position) { + if (!cursor.isClosed() && cursor.moveToPosition(position - 1)) { + //noinspection SwitchStatementWithTooFewBranches + switch (columnIndexType) { + case FIELD_TYPE_INTEGER: + return cursor.getLong(columnIndex); + default: + throw new IllegalStateException("Unknown type for columnIndex \"" + columnIndex + "\": " + columnIndexType); + } + } else { + return NO_ID; + } + } + + @Override + public int getItemCount() { + return count; + } + + /** + * Call this method when the {@link AbstractCursorPickerAdapter} is no longer need to free resources. + */ + public void onDestroy() { + cursor.close(); + bindExecutor.shutdownNow(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java new file mode 100644 index 000000000..901d204cd --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java @@ -0,0 +1,26 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import androidx.recyclerview.widget.RecyclerView; + +public abstract class AbstractPickerAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> { + + protected static final int VIEW_TYPE_NONE = -1; + protected static final int VIEW_TYPE_ITEM = 0; + protected static final int VIEW_TYPE_ITEM_NATIVE = 1; + + @Override + public int getItemViewType(int position) { + if (position > 0) { + return VIEW_TYPE_ITEM; + } else if (position == 0) { + return VIEW_TYPE_ITEM_NATIVE; + } else { + return VIEW_TYPE_NONE; + } + } + + /** + * Call this method when the {@link AbstractPickerAdapter} is no longer need to free resources. + */ + public abstract void onDestroy(); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java new file mode 100644 index 000000000..22ac0c694 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java @@ -0,0 +1,104 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.ContactsContract; +import android.text.TextUtils; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.RequestBuilder; + +import java.io.IOException; +import java.io.InputStream; +import java.util.function.BiConsumer; + +import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding; +import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding; + +import static android.provider.ContactsContract.CommonDataKinds.Email.DATA; +import static android.provider.ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY; +import static android.provider.ContactsContract.CommonDataKinds.Phone.NUMBER; +import static android.provider.ContactsContract.Contacts.CONTENT_LOOKUP_URI; +import static android.provider.ContactsContract.Contacts.CONTENT_URI; +import static android.provider.ContactsContract.Contacts.DISPLAY_NAME; +import static android.provider.ContactsContract.Contacts.SORT_KEY_PRIMARY; +import static android.provider.ContactsContract.Contacts._ID; + +public class ContactAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> { + + private final int lookupKeyColumnIndex; + private final int displayNameColumnIndex; + + public ContactAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable onSelectPicker) { + super(context, onSelect, onSelectPicker, CONTENT_URI, _ID, new String[]{_ID, LOOKUP_KEY, DISPLAY_NAME}, SORT_KEY_PRIMARY); + lookupKeyColumnIndex = cursor.getColumnIndex(LOOKUP_KEY); + displayNameColumnIndex = cursor.getColumnIndex(DISPLAY_NAME); + notifyItemRangeInserted(0, getItemCount() + 1); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_ITEM_NATIVE: + return new ContactNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + case VIEW_TYPE_ITEM: + return new ContactItemViewHolder(ItemPickerUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + default: + throw new IllegalStateException("Unknown viewType " + viewType); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case VIEW_TYPE_ITEM_NATIVE: { + ((ContactNativeItemViewHolder) holder).bind(openNativePicker); + break; + } + case VIEW_TYPE_ITEM: { + final ContactItemViewHolder viewHolder = (ContactItemViewHolder) holder; + if (!cursor.isClosed()) { + cursor.moveToPosition(position - 1); + final String displayName = cursor.getString(displayNameColumnIndex); + final String lookupKey = cursor.getString(lookupKeyColumnIndex); + bindExecutor.execute(() -> { + try (InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, Uri.withAppendedPath(CONTENT_LOOKUP_URI, lookupKey))) { + final Bitmap thumbnail = BitmapFactory.decodeStream(inputStream); + String contactInformation = ""; + try (final Cursor phoneCursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[]{NUMBER}, LOOKUP_KEY + " = ?", new String[]{lookupKey}, null)) { + if (phoneCursor != null && phoneCursor.moveToFirst()) { + contactInformation = phoneCursor.getString(phoneCursor.getColumnIndex(NUMBER)); + } + } + if (TextUtils.isEmpty(contactInformation)) { + try (final Cursor emailCursor = contentResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, new String[]{DATA}, LOOKUP_KEY + " = ?", new String[]{lookupKey}, null)) { + if (emailCursor != null && emailCursor.moveToFirst()) { + contactInformation = emailCursor.getString(emailCursor.getColumnIndex(DATA)); + } + } + } + final String finalContactInformation = contactInformation; + new Handler(Looper.getMainLooper()).post(() -> viewHolder.bind(Uri.withAppendedPath(CONTENT_LOOKUP_URI, lookupKey), thumbnail, displayName, finalContactInformation, onSelect)); + } catch (IOException ignored) { + new Handler(Looper.getMainLooper()).post(viewHolder::bindError); + } + }); + } else { + new Handler(Looper.getMainLooper()).post(viewHolder::bindError); + } + break; + } + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java new file mode 100644 index 000000000..f403fed21 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java @@ -0,0 +1,66 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.graphics.Bitmap; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.request.RequestOptions; + +import java.util.function.BiConsumer; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static it.niedermann.nextcloud.deck.util.VCardUtil.getColorBasedOnDisplayName; + +public class ContactItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemPickerUserBinding binding; + + public ContactItemViewHolder(@NonNull ItemPickerUserBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Uri uri, @Nullable Bitmap image, @NonNull String displayName, @Nullable String contactInformation, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) { + itemView.setOnClickListener((v) -> onSelect.accept(uri, new Pair<>(displayName, image == null ? null : Glide.with(itemView.getContext()).load(image)))); + binding.title.setText(displayName); + binding.contactInformation.setText(contactInformation); + if (image == null) { + binding.initials.setVisibility(VISIBLE); + binding.initials.setText(TextUtils.isEmpty(displayName) + ? null + : String.valueOf(displayName.charAt(0)) + ); + Glide.with(itemView.getContext()) + .load(new ColorDrawable(getColorBasedOnDisplayName(itemView.getContext(), displayName))) + .apply(RequestOptions.circleCropTransform()) + .into(binding.avatar); + } else { + binding.initials.setVisibility(GONE); + binding.initials.setText(null); + Glide.with(itemView.getContext()) + .load(image) + .placeholder(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(binding.avatar); + } + } + + public void bindError() { + itemView.setOnClickListener(null); + Glide.with(itemView.getContext()) + .load(R.drawable.ic_person_grey600_24dp) + .into(binding.avatar); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java new file mode 100644 index 000000000..a1d7d5921 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java @@ -0,0 +1,23 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding; + +public class ContactNativeItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemPickerNativeBinding binding; + + public ContactNativeItemViewHolder(@NonNull ItemPickerNativeBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Runnable onOpenMajorPicker) { + binding.title.setText(R.string.show_all_contacts); + binding.subtitle.setText(R.string.contacts); + itemView.setOnClickListener((v) -> onOpenMajorPicker.run()); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java new file mode 100644 index 000000000..aa96a0e69 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java @@ -0,0 +1,85 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.content.ContentUris; +import android.content.Context; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.RequestBuilder; + +import java.util.function.BiConsumer; + +import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding; +import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding; + +import static android.provider.MediaStore.Downloads.DATE_ADDED; +import static android.provider.MediaStore.Downloads.DATE_MODIFIED; +import static android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI; +import static android.provider.MediaStore.Downloads.MIME_TYPE; +import static android.provider.MediaStore.Downloads.SIZE; +import static android.provider.MediaStore.Downloads.TITLE; +import static android.provider.MediaStore.Downloads._ID; +import static java.util.Objects.requireNonNull; + +@RequiresApi(api = 29) +public class FileAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> { + + private final int displayNameColumnIndex; + private final int sizeColumnIndex; + private final int modifiedColumnIndex; + private final int mimeTypeColumnIndex; + + private FileAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable onSelectPicker) { + super(context, onSelect, onSelectPicker, _ID, requireNonNull(context.getContentResolver().query(EXTERNAL_CONTENT_URI, new String[]{_ID, TITLE, SIZE, DATE_MODIFIED, MIME_TYPE}, null, null, DATE_ADDED + " DESC"))); + displayNameColumnIndex = cursor.getColumnIndex(TITLE); + sizeColumnIndex = cursor.getColumnIndex(SIZE); + modifiedColumnIndex = cursor.getColumnIndex(DATE_MODIFIED); + mimeTypeColumnIndex = cursor.getColumnIndex(MIME_TYPE); + notifyItemRangeInserted(0, getItemCount() + 1); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_ITEM_NATIVE: + return new FileNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + case VIEW_TYPE_ITEM: + return new FileItemViewHolder(ItemAttachmentDefaultBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + default: + throw new IllegalStateException("Unknown viewType " + viewType); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case VIEW_TYPE_ITEM_NATIVE: { + ((FileNativeItemViewHolder) holder).bind(openNativePicker); + break; + } + case VIEW_TYPE_ITEM: { + if (!cursor.isClosed()) { + bindExecutor.execute(() -> { + final long id = getItemId(position); + final String name = cursor.getString(displayNameColumnIndex); + final String mimeType = cursor.getString(mimeTypeColumnIndex); + final long size = cursor.getLong(sizeColumnIndex); + final long modified = cursor.getLong(modifiedColumnIndex); + new Handler(Looper.getMainLooper()).post(() -> ((FileItemViewHolder) holder).bind(ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id), name, mimeType, size, modified, onSelect)); + }); + } + break; + } + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java new file mode 100644 index 000000000..1ac14361a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java @@ -0,0 +1,88 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.net.Uri; +import android.os.Environment; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.RequestBuilder; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; + +import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding; +import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; + +import static java.util.Collections.reverseOrder; +import static java.util.Comparator.comparingLong; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; + +@Deprecated +public class FileAdapterLegacy extends AbstractPickerAdapter<RecyclerView.ViewHolder> { + + @NonNull + private final List<File> files; + @NonNull + protected final BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect; + @NonNull + protected final Runnable openNativePicker; + + public FileAdapterLegacy(@NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker) { + // TODO run in separate thread? + this.onSelect = onSelect; + this.openNativePicker = openNativePicker; + this.files = Arrays.stream(requireNonNull(requireNonNull(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)).listFiles())) + .sorted(reverseOrder(comparingLong(File::lastModified))) + .collect(toList()); + + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_ITEM_NATIVE: + return new FileNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + case VIEW_TYPE_ITEM: + return new FileItemViewHolder(ItemAttachmentDefaultBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + default: + throw new IllegalStateException("Unknown viewType " + viewType); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case VIEW_TYPE_ITEM_NATIVE: { + ((FileNativeItemViewHolder) holder).bind(openNativePicker); + break; + } + case VIEW_TYPE_ITEM: { + final File file = files.get(position - 1); + if (file.isFile()) { + ((FileItemViewHolder) holder).bind(Uri.fromFile(file), file.getName(), AttachmentUtil.getMimeType(file.getAbsolutePath()), file.length(), file.lastModified(), onSelect); + } else { + ((FileItemViewHolder) holder).bindError(); + } + break; + } + } + } + + @Override + public int getItemCount() { + return files.size(); + } + + public void onDestroy() { + // Let GarbageCollection do this stuff... + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java new file mode 100644 index 000000000..f7d64aca8 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java @@ -0,0 +1,45 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.net.Uri; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.RequestBuilder; + +import java.util.function.BiConsumer; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding; + +import static android.text.format.Formatter.formatFileSize; +import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType; +import static it.niedermann.nextcloud.deck.util.DateUtil.getRelativeDateTimeString; + +public class FileItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemAttachmentDefaultBinding binding; + + public FileItemViewHolder(@NonNull ItemAttachmentDefaultBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Uri uri, @NonNull String name, String mimeType, long size, long modified, @Nullable BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) { + itemView.setOnClickListener(onSelect == null ? null : (v) -> onSelect.accept(uri, new Pair<>(name, null))); + binding.filename.setText(name); + binding.filesize.setText(formatFileSize(binding.filesize.getContext(), size)); + binding.modified.setText(getRelativeDateTimeString(binding.modified.getContext(), modified)); + binding.preview.setImageResource(getIconForMimeType(mimeType)); + } + + public void bindError() { + binding.filename.setText(R.string.simple_exception); + binding.filesize.setText(null); + binding.modified.setText(null); + itemView.setOnClickListener(null); + binding.preview.setImageResource(R.drawable.ic_attach_file_grey600_24dp); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java new file mode 100644 index 000000000..79129f26a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java @@ -0,0 +1,23 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding; + +public class FileNativeItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemPickerNativeBinding binding; + + public FileNativeItemViewHolder(@NonNull ItemPickerNativeBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(Runnable onOpenMajorPicker) { + binding.title.setText(R.string.show_all_files); + binding.subtitle.setText(R.string.downloads); + itemView.setOnClickListener((v) -> onOpenMajorPicker.run()); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java new file mode 100644 index 000000000..658eb1ee3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java @@ -0,0 +1,100 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.annotation.SuppressLint; +import android.content.ContentUris; +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.util.Pair; +import android.util.Size; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.RequestBuilder; + +import java.io.IOException; +import java.util.function.BiConsumer; + +import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding; +import it.niedermann.nextcloud.deck.databinding.ItemPhotoPreviewBinding; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.Q; +import static android.provider.BaseColumns._ID; +import static android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + +public class GalleryAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> { + + @NonNull + private final LifecycleOwner lifecycleOwner; + + @SuppressLint("InlinedApi") + private static final String sortOrder = (SDK_INT >= Q) + ? MediaStore.Images.Media.DATE_TAKEN + : MediaStore.Images.Media.DATE_ADDED; + + public GalleryAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, @NonNull LifecycleOwner lifecycleOwner) { + super(context, onSelect, openNativePicker, EXTERNAL_CONTENT_URI, _ID, sortOrder + " DESC"); + this.lifecycleOwner = lifecycleOwner; + notifyItemRangeInserted(0, getItemCount() + 1); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_TYPE_ITEM_NATIVE: + return new GalleryPhotoPreviewItemViewHolder(ItemPhotoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + case VIEW_TYPE_ITEM: + return new GalleryItemViewHolder(ItemAttachmentImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + default: + throw new IllegalStateException("Unknown viewType " + viewType); + } + + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case VIEW_TYPE_ITEM_NATIVE: { + ((GalleryPhotoPreviewItemViewHolder) holder).bind(openNativePicker, lifecycleOwner); + break; + } + case VIEW_TYPE_ITEM: { + final long id = getItemId(position); + bindExecutor.execute(() -> { + try { + final Bitmap thumbnail; + if (SDK_INT >= Q) { + thumbnail = contentResolver.loadThumbnail(ContentUris.withAppendedId( + EXTERNAL_CONTENT_URI, id), new Size(512, 384), null); + } else { + thumbnail = MediaStore.Images.Thumbnails.getThumbnail( + contentResolver, id, + MediaStore.Images.Thumbnails.MINI_KIND, null); + } + new Handler(Looper.getMainLooper()).post(() -> ((GalleryItemViewHolder) holder).bind(ContentUris.withAppendedId( + EXTERNAL_CONTENT_URI, id), thumbnail, onSelect)); + } catch (IOException ignored) { + new Handler(Looper.getMainLooper()).post(((GalleryItemViewHolder) holder)::bindError); + } + }); + } + } + } + + @Override + public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) { + super.onViewDetachedFromWindow(holder); + if (holder instanceof GalleryPhotoPreviewItemViewHolder) { + ((GalleryPhotoPreviewItemViewHolder) holder).unbind(); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java new file mode 100644 index 000000000..c70dc8277 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java @@ -0,0 +1,29 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.recyclerview.widget.RecyclerView; + +public class GalleryItemDecoration extends RecyclerView.ItemDecoration { + + @Px + private final int gutter; + + public GalleryItemDecoration(@Px int gutter) { + this.gutter = gutter; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + final int position = parent.getChildAdapterPosition(view); + if (position >= 0) { + outRect.left = gutter; + outRect.top = gutter; + outRect.right = gutter; + outRect.bottom = gutter; + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java new file mode 100644 index 000000000..346fca9c3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java @@ -0,0 +1,42 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; + +import java.util.function.BiConsumer; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding; + +public class GalleryItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemAttachmentImageBinding binding; + + public GalleryItemViewHolder(@NonNull ItemAttachmentImageBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Uri uri, @Nullable Bitmap image, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) { + itemView.setOnClickListener((v) -> onSelect.accept(uri, new Pair<>(null, Glide.with(itemView.getContext()).load(image)))); + Glide.with(itemView.getContext()) + .load(image) + .placeholder(R.drawable.ic_image_grey600_24dp) + .into(binding.preview); + } + + public void bindError() { + itemView.setOnClickListener(null); + Glide.with(itemView.getContext()) + .load(R.drawable.ic_image_grey600_24dp) + .into(binding.preview); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java new file mode 100644 index 000000000..00a833e57 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java @@ -0,0 +1,51 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.picker; + +import androidx.annotation.NonNull; +import androidx.camera.core.Preview; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.concurrent.ExecutionException; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.databinding.ItemPhotoPreviewBinding; + +import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA; + +public class GalleryPhotoPreviewItemViewHolder extends RecyclerView.ViewHolder { + + private final ItemPhotoPreviewBinding binding; + private ProcessCameraProvider cameraProvider; + + public GalleryPhotoPreviewItemViewHolder(@NonNull ItemPhotoPreviewBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Runnable openNativePicker, @NonNull LifecycleOwner lifecycleOwner) { + itemView.setOnClickListener((v) -> openNativePicker.run()); + ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(itemView.getContext()); + cameraProviderFuture.addListener(() -> { + try { + unbind(); + cameraProvider = cameraProviderFuture.get(); + Preview previewUseCase = new Preview.Builder().build(); + previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider()); + cameraProvider.bindToLifecycle(lifecycleOwner, DEFAULT_BACK_CAMERA, previewUseCase); + } catch (ExecutionException | InterruptedException | IllegalArgumentException e) { + DeckLog.logError(e); + } + }, ContextCompat.getMainExecutor(itemView.getContext())); + } + + + public void unbind() { + if (cameraProvider != null) { + cameraProvider.unbindAll(); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java new file mode 100644 index 000000000..8ebdf1b50 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java @@ -0,0 +1,102 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Color; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; +import androidx.swiperefreshlayout.widget.CircularProgressDrawable; + +import com.bumptech.glide.RequestBuilder; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.DialogPreviewBinding; +import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; + +public class PreviewDialog extends BrandedDialogFragment { + + private DialogPreviewBinding binding; + private PreviewDialogViewModel viewModel; + private LiveData<RequestBuilder<?>> imageBuilder$; + private LiveData<String> title$; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + viewModel = new ViewModelProvider(requireActivity()).get(PreviewDialogViewModel.class); + binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext())); + + final Context context = requireContext(); + + this.imageBuilder$ = this.viewModel.getImageBuilder(); + this.imageBuilder$.observe(requireActivity(), builder -> { + if (builder == null) { + binding.avatar.setVisibility(GONE); + } else { + final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(context); + circularProgressDrawable.setStrokeWidth(5f); + circularProgressDrawable.setCenterRadius(30f); + circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY); + circularProgressDrawable.start(); + binding.avatar.setVisibility(VISIBLE); + binding.avatar.post(() -> builder + .placeholder(circularProgressDrawable) + .into(binding.avatar)); + } + }); + this.title$ = this.viewModel.getTitle(); + this.title$.observe(requireActivity(), title -> { + if (TextUtils.isEmpty(title)) { + binding.title.setVisibility(GONE); + } else { + binding.title.setVisibility(VISIBLE); + binding.title.setText(title); + } + }); + + return new BrandedAlertDialogBuilder(requireContext()) + .setPositiveButton(R.string.simple_attach, (d, w) -> { + viewModel.setResult(true); + dismiss(); + }) + .setNeutralButton(R.string.simple_close, (d, w) -> { + viewModel.setResult(false); + dismiss(); + }) + .setView(binding.getRoot()) + .create(); + } + + @Override + public void onCancel(@NonNull DialogInterface dialog) { + viewModel.setResult(false); + super.onCancel(dialog); + } + + @Override + public void applyBrand(int mainColor) { + } + + @Override + public void onDestroy() { + this.imageBuilder$.removeObservers(requireActivity()); + this.title$.removeObservers(requireActivity()); + super.onDestroy(); + } + + public static DialogFragment newInstance() { + return new PreviewDialog(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java new file mode 100644 index 000000000..8ee8a0e08 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java @@ -0,0 +1,50 @@ +package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.bumptech.glide.RequestBuilder; + +import static androidx.lifecycle.Transformations.distinctUntilChanged; + +public class PreviewDialogViewModel extends ViewModel { + + @NonNull + private final MutableLiveData<String> title$ = new MutableLiveData<>(); + @NonNull + private final MutableLiveData<RequestBuilder<?>> imageBuilder$ = new MutableLiveData<>(); + private MutableLiveData<Boolean> result$ = new MutableLiveData<>(); + + /** + * Call this before observing {@link #getResult()} to prepare the {@link PreviewDialog}. + */ + public void prepareDialog(@Nullable String title, @Nullable RequestBuilder<?> imageBuilder) { + this.result$ = new MutableLiveData<>(); + this.title$.setValue(title); + this.imageBuilder$.setValue(imageBuilder); + } + + /** + * This will be a new instance after each call of {@link #prepareDialog(String, RequestBuilder)}. + * + * @return {@link Boolean#TRUE} if a positive action has been submitted, {@link Boolean#FALSE} if the dialog has been canceled. + */ + public LiveData<Boolean> getResult() { + return this.result$; + } + + protected LiveData<String> getTitle() { + return distinctUntilChanged(this.title$); + } + + protected LiveData<RequestBuilder<?>> getImageBuilder() { + return distinctUntilChanged(this.imageBuilder$); + } + + protected void setResult(boolean submittedPositive) { + result$.setValue(submittedPositive); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java index e261c37a2..3fd536aa6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java @@ -15,7 +15,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; -import java.util.Date; +import java.time.Instant; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; @@ -23,12 +23,15 @@ import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabCommentsBindi import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; import it.niedermann.nextcloud.deck.ui.card.EditActivity; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB; import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions; @@ -38,7 +41,6 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit private FragmentCardEditTabCommentsBinding binding; private EditCardViewModel mainViewModel; private CommentsViewModel commentsViewModel; - private SyncManager syncManager; private CardCommentsAdapter adapter; public static Fragment newInstance() { @@ -68,7 +70,6 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class); - syncManager = new SyncManager(requireActivity()); adapter = new CardCommentsAdapter(requireContext(), mainViewModel.getAccount(), requireActivity().getMenuInflater(), this, this, getChildFragmentManager()); binding.comments.setAdapter(adapter); @@ -82,7 +83,7 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText); } }); - syncManager.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(), + commentsViewModel.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(), (comments) -> { if (comments != null && comments.size() > 0) { binding.emptyContentView.setVisibility(GONE); @@ -100,13 +101,13 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit if (!TextUtils.isEmpty(binding.message.getText().toString().trim())) { binding.emptyContentView.setVisibility(GONE); binding.comments.setVisibility(VISIBLE); - final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), mainViewModel.getAccount().getUserName(), new Date()); + final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), mainViewModel.getAccount().getUserName(), Instant.now()); final FullDeckComment parent = commentsViewModel.getReplyToComment().getValue(); if (parent != null) { comment.setParentId(parent.getId()); commentsViewModel.setReplyToComment(null); } - syncManager.addCommentToCard(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), comment); + commentsViewModel.addCommentToCard(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), comment); } binding.message.setText(null); }); @@ -116,6 +117,7 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit } return true; }); + binding.message.addTextChangedListener(new CardCommentsMentionProposer(getViewLifecycleOwner(), mainViewModel.getAccount(), mainViewModel.getBoardId(), binding.message, binding.mentionProposerWrapper, binding.mentionProposer)); } else { binding.addCommentLayout.setVisibility(GONE); } @@ -133,12 +135,17 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit @Override public void onCommentEdited(Long id, String comment) { - syncManager.updateComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), id, comment); + commentsViewModel.updateComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), id, comment); } @Override public void onCommentDeleted(Long localId) { - syncManager.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId); + final WrappedLiveData<Void> deleteLiveData = commentsViewModel.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId); + observeOnce(deleteLiveData, this, (next) -> { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java new file mode 100644 index 000000000..7ca7a6384 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java @@ -0,0 +1,139 @@ +package it.niedermann.nextcloud.deck.ui.card.comments; + +import android.annotation.SuppressLint; +import android.net.Uri; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.core.util.Pair; +import androidx.lifecycle.LifecycleOwner; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.android.util.DimensionUtil; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.card.comments.util.CommentsUtil; + +import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; + +public class CardCommentsMentionProposer implements TextWatcher { + + private final int avatarSize; + @NonNull + private final SyncManager syncManager; + @NonNull + private final LinearLayout.LayoutParams layoutParams; + @NonNull + private final LifecycleOwner owner; + @NonNull + private final Account account; + private final long boardLocalId; + @NonNull + private final EditText editText; + @NonNull + private final LinearLayout mentionProposer; + @NonNull + private final LinearLayout mentionProposerWrapper; + + @NonNull + private final List<User> users = new ArrayList<>(); + + public CardCommentsMentionProposer(@NonNull LifecycleOwner owner, @NonNull Account account, long boardLocalId, @NonNull EditText editText, LinearLayout mentionProposerWrapper, @NonNull LinearLayout avatarProposer) { + this.owner = owner; + this.account = account; + this.boardLocalId = boardLocalId; + this.editText = editText; + this.mentionProposerWrapper = mentionProposerWrapper; + this.mentionProposer = avatarProposer; + syncManager = new SyncManager(editText.getContext()); + avatarSize = DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.avatar_size_small); + layoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize); + layoutParams.setMarginEnd(DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.spacer_1x)); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final int selectionStart = editText.getSelectionStart(); + final int selectionEnd = editText.getSelectionEnd(); + final Pair<String, Integer> mentionProposal = CommentsUtil.getUserNameForMentionProposal(s.toString(), selectionStart); + if (mentionProposal == null || (mentionProposal.first != null && mentionProposal.first.length() == 0) || selectionStart != selectionEnd) { + mentionProposer.removeAllViews(); + mentionProposerWrapper.setVisibility(View.GONE); + this.users.clear(); + } else { + if (mentionProposal.first != null && mentionProposal.second != null) { + observeOnce(syncManager.searchUserByUidOrDisplayName(account.getId(), boardLocalId, -1L, mentionProposal.first), owner, (users) -> { + if (!users.equals(this.users)) { + mentionProposer.removeAllViews(); + if (users.size() > 0) { + mentionProposerWrapper.setVisibility(View.VISIBLE); + for (User user : users) { + final ImageView avatar = new ImageView(mentionProposer.getContext()); + avatar.setLayoutParams(layoutParams); + updateListenerOfView(avatar, s, mentionProposal, user); + + mentionProposer.addView(avatar); + + Glide.with(avatar.getContext()) + .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + avatarSize) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(avatar); + } + } else { + mentionProposerWrapper.setVisibility(View.GONE); + } + this.users.clear(); + this.users.addAll(users); + } else { + int i = 0; + for (User user : users) { + updateListenerOfView(mentionProposer.getChildAt(i), s, mentionProposal, user); + i++; + } + } + }); + } else { + this.users.clear(); + mentionProposer.removeAllViews(); + mentionProposerWrapper.setVisibility(View.GONE); + } + } + } + + @SuppressLint("SetTextI18n") + private void updateListenerOfView(View avatar, CharSequence s, Pair<String, Integer> mentionProposal, User user) { + avatar.setOnClickListener((c) -> { + editText.setText( + s.subSequence(0, mentionProposal.second) + + user.getUid() + + s.subSequence(mentionProposal.second + mentionProposal.first.length(), s.length()) + ); + editText.setSelection(mentionProposal.second + user.getUid().length()); + mentionProposerWrapper.setVisibility(View.GONE); + }); + } + + @Override + public void afterTextChanged(Editable s) { + + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java index f7fd247a9..dada94d5b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java @@ -1,15 +1,30 @@ package it.niedermann.nextcloud.deck.ui.card.comments; +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; +import java.util.List; + +import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; @SuppressWarnings("WeakerAccess") -public class CommentsViewModel extends ViewModel { +public class CommentsViewModel extends AndroidViewModel { - private MutableLiveData<FullDeckComment> replyToComment = new MutableLiveData<>(); + private final SyncManager syncManager; + + private final MutableLiveData<FullDeckComment> replyToComment = new MutableLiveData<>(); + + public CommentsViewModel(@NonNull Application application) { + super(application); + this.syncManager = new SyncManager(application); + } public void setReplyToComment(FullDeckComment replyToComment) { this.replyToComment.postValue(replyToComment); @@ -18,4 +33,20 @@ public class CommentsViewModel extends ViewModel { public LiveData<FullDeckComment> getReplyToComment() { return this.replyToComment; } + + public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) { + return syncManager.getFullCommentsForLocalCardId(localCardId); + } + + public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) { + syncManager.addCommentToCard(accountId, cardId, comment); + } + + public void updateComment(long accountId, long localCardId, long localCommentId, String comment) { + syncManager.updateComment(accountId, localCardId, localCommentId, comment); + } + + public WrappedLiveData<Void> deleteComment(long accountId, long localCardId, long localCommentId) { + return syncManager.deleteComment(accountId, localCardId, localCommentId); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java index 086d799af..3e540c95e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java @@ -11,22 +11,25 @@ import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; -import java.text.DateFormat; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import it.niedermann.android.util.ClipboardUtil; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; import it.niedermann.nextcloud.deck.util.DateUtil; -import it.niedermann.nextcloud.deck.util.DimensionUtil; import it.niedermann.nextcloud.deck.util.ViewUtil; -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions; public class ItemCommentViewHolder extends RecyclerView.ViewHolder { - private ItemCommentBinding binding; + private final ItemCommentBinding binding; + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); @SuppressWarnings("WeakerAccess") public ItemCommentViewHolder(ItemCommentBinding binding) { @@ -35,15 +38,15 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { } public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @ColorInt int mainColor, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager) { - ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp); + ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp); binding.message.setText(comment.getComment().getMessage()); binding.actorDisplayName.setText(comment.getComment().getActorDisplayName()); - binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().getTime())); + binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().toEpochMilli())); itemView.setOnClickListener(View::showContextMenu); itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { inflater.inflate(R.menu.comment_menu, menu); - menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> copyToClipboard(itemView.getContext(), comment.getComment().getMessage())); + menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), comment.getComment().getMessage())); final MenuItem replyMenuItem = menu.findItem(R.id.reply); if (comment.getStatusEnum() != DBStatus.LOCAL_EDITED && account.getServerDeckVersionAsObject().supportsCommentsReplys()) { replyMenuItem.setOnMenuItemClickListener(item -> { @@ -72,7 +75,7 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor); binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(comment.getStatusEnum()) ? View.VISIBLE : View.GONE); - TooltipCompat.setTooltipText(binding.creationDateTime, DateFormat.getDateTimeInstance().format(comment.getComment().getCreationDateTime())); + TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter)); setupMentions(account, comment.getComment().getMentions(), binding.message); if (comment.getParent() == null) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java new file mode 100644 index 000000000..5251291c8 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java @@ -0,0 +1,48 @@ +package it.niedermann.nextcloud.deck.ui.card.comments.util; + + +import androidx.core.util.Pair; + +public class CommentsUtil { + + public static Pair<String, Integer> getUserNameForMentionProposal(String text, int cursorPosition) { + Pair result = null; + + if (text != null) { + // find start of relevant substring + int cursor = cursorPosition; + if (cursor < 1) { + return null; + } + int start = 0; + while (cursor > 0) { + cursor--; + if (Character.isWhitespace(text.charAt(cursor))) { + start = cursor + 1; + break; + } + } + if (text.length()-1 < start || text.charAt(start) != '@') { + return null; + } + + // find end of relevant substring + cursor = cursorPosition; + int textLength = text.length(); + int end = textLength; + while (cursor < textLength) { + if (Character.isWhitespace(text.charAt(cursor))) { + end = cursor; + break; + } + cursor++; + } + + start++; + result = Pair.create(text.substring(start, end), start); + + } + + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java new file mode 100644 index 000000000..aa8c3e8f6 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java @@ -0,0 +1,80 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.util.Consumer; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.User; + +import static androidx.recyclerview.widget.RecyclerView.NO_ID; + +@SuppressWarnings("WeakerAccess") +public class AssigneeAdapter extends RecyclerView.Adapter<AssigneeViewHolder> { + + private final Account account; + @NonNull + private List<User> users = new ArrayList<>(); + @NonNull + private final Consumer<User> userClickedListener; + + AssigneeAdapter( + @NonNull Consumer<User> userClickedListener, + @NonNull Account account + ) { + super(); + this.userClickedListener = userClickedListener; + this.account = account; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + Long id = users.get(position).getLocalId(); + return id == null ? NO_ID : id; + } + + @NonNull + @Override + public AssigneeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final Context context = parent.getContext(); + return new AssigneeViewHolder(ItemAssigneeBinding.inflate(LayoutInflater.from(context))); + } + + @Override + public void onBindViewHolder(@NonNull AssigneeViewHolder holder, int position) { + final User user = users.get(position); + holder.bind(account, user, userClickedListener); + } + + @Override + public int getItemCount() { + return users.size(); + } + + public void setUsers(@NonNull List<User> users) { + this.users.clear(); + this.users.addAll(users); + notifyDataSetChanged(); + } + + public void addUser(@NonNull User user) { + this.users.add(user); + notifyItemInserted(this.users.size()); + } + + public void removeUser(@NonNull User user) { + final int index = this.users.indexOf(user); + this.users.remove(user); + notifyItemRemoved(index); + } + +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java new file mode 100644 index 000000000..096dcfa53 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java @@ -0,0 +1,28 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.recyclerview.widget.RecyclerView; + +public class AssigneeDecoration extends RecyclerView.ItemDecoration { + + private final int gutter; + + public AssigneeDecoration(@Px int gutter) { + this.gutter = gutter; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + final int position = parent.getChildAdapterPosition(view); + + if (position >= 0) { + // All columns get some spacing at the bottom and at the right side + outRect.right = gutter; + outRect.bottom = gutter; + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java new file mode 100644 index 000000000..ddb1236b6 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java @@ -0,0 +1,29 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Consumer; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.util.ViewUtil; + +public class AssigneeViewHolder extends RecyclerView.ViewHolder { + private ItemAssigneeBinding binding; + + @SuppressWarnings("WeakerAccess") + public AssigneeViewHolder(ItemAssigneeBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Account account, @NonNull User user, @Nullable Consumer<User> onClickListener) { + ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), R.drawable.ic_person_grey600_24dp); + if(onClickListener != null) { + itemView.setOnClickListener((v) -> onClickListener.accept(user)); + } + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java index 3182fffa2..2c697de08 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java @@ -2,25 +2,25 @@ package it.niedermann.nextcloud.deck.ui.card.details; import android.content.Context; import android.content.res.ColorStateList; -import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.LinearLayout; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; import com.google.android.material.chip.Chip; import com.google.android.material.snackbar.Snackbar; @@ -31,18 +31,21 @@ import com.wdullaer.materialdatetimepicker.time.TimePickerDialog.OnTimeSetListen import com.yydcdut.markdown.MarkdownProcessor; import com.yydcdut.markdown.syntax.edit.EditFactory; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.Locale; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import it.niedermann.android.util.ColorUtil; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabDetailsBinding; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.branding.BrandedDatePickerDialog; import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; @@ -51,26 +54,23 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedTimePickerDialog; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; import it.niedermann.nextcloud.deck.ui.card.LabelAutoCompleteAdapter; import it.niedermann.nextcloud.deck.ui.card.UserAutoCompleteAdapter; +import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeDialog; +import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeListener; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import it.niedermann.nextcloud.deck.util.ColorUtil; import it.niedermann.nextcloud.deck.util.MarkDownUtil; -import it.niedermann.nextcloud.deck.util.ViewUtil; -import static android.text.format.DateFormat.getDateFormat; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; -public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener { +public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener, CardAssigneeListener { private FragmentCardEditTabDetailsBinding binding; private EditCardViewModel viewModel; - private SyncManager syncManager; - private DateFormat dateFormat; - private DateFormat dueTime = new SimpleDateFormat("HH:mm", Locale.ROOT); - @Px - private int avatarSize; - private LinearLayout.LayoutParams avatarLayoutParams; + private AssigneeAdapter adapter; + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); + private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); private AppCompatActivity activity; @Override @@ -92,8 +92,6 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis ViewGroup container, Bundle savedInstanceState) { binding = FragmentCardEditTabDetailsBinding.inflate(inflater, container, false); - dateFormat = getDateFormat(activity); - viewModel = new ViewModelProvider(activity).get(EditCardViewModel.class); // This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance @@ -103,22 +101,20 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis return binding.getRoot(); } - syncManager = new SyncManager(requireContext()); - - avatarSize = dpToPx(requireContext(), R.dimen.avatar_size); - avatarLayoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize); - avatarLayoutParams.setMargins(0, 0, dpToPx(requireContext(), R.dimen.spacer_1x), 0); + @Px final int avatarSize = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size); + final LinearLayout.LayoutParams avatarLayoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize); + avatarLayoutParams.setMargins(0, 0, DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x), 0); - setupPeople(); + setupAssignees(); setupLabels(); setupDueDate(); setupDescription(); + setupProjects(); binding.description.setText(viewModel.getFullCard().getCard().getDescription()); return binding.getRoot(); } - @Override public void onResume() { super.onResume(); @@ -173,49 +169,14 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis } } - private TimePickerDialog createTimePickerDialogFromDate( - @Nullable OnTimeSetListener listener, - @Nullable Date date - ) { - int hourOfDay = 0; - int minutes = 0; - - if (date != null) { - hourOfDay = date.getHours(); - minutes = date.getMinutes(); - } - return BrandedTimePickerDialog.newInstance(listener, hourOfDay, minutes, true); - } - - private DatePickerDialog createDatePickerDialogFromDate( - @Nullable OnDateSetListener listener, - @Nullable Date date - ) { - int year; - int month; - int day; - - Calendar cal = Calendar.getInstance(); - if (date != null) { - cal.setTime(date); - year = cal.get(Calendar.YEAR); - month = cal.get(Calendar.MONTH); - day = cal.get(Calendar.DAY_OF_MONTH); - } else { - year = cal.get(Calendar.YEAR); - month = cal.get(Calendar.MONTH); - day = cal.get(Calendar.DAY_OF_MONTH); - } - return BrandedDatePickerDialog.newInstance(listener, year, month, day); - } - private void setupDueDate() { if (this.viewModel.getFullCard().getCard().getDueDate() != null) { - binding.dueDateDate.setText(dateFormat.format(this.viewModel.getFullCard().getCard().getDueDate())); - binding.dueDateTime.setText(dueTime.format(this.viewModel.getFullCard().getCard().getDueDate())); - binding.clearDueDate.setVisibility(View.VISIBLE); + final ZonedDateTime dueDate = this.viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()); + binding.dueDateDate.setText(dueDate == null ? null : dueDate.format(dateFormatter)); + binding.dueDateTime.setText(dueDate == null ? null : dueDate.format(timeFormatter)); + binding.clearDueDate.setVisibility(VISIBLE); } else { - binding.clearDueDate.setVisibility(View.GONE); + binding.clearDueDate.setVisibility(GONE); binding.dueDateDate.setText(null); binding.dueDateTime.setText(null); } @@ -223,31 +184,37 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis if (viewModel.canEdit()) { binding.dueDateDate.setOnClickListener(v -> { - if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null) { - createDatePickerDialogFromDate(this, viewModel.getFullCard().getCard().getDueDate()).show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName()); + final LocalDate date; + if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null && viewModel.getFullCard().getCard().getDueDate() != null) { + date = viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()).toLocalDate(); } else { - createDatePickerDialogFromDate(this, null).show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName()); + date = LocalDate.now(); } + BrandedDatePickerDialog.newInstance(this, date.getYear(), date.getMonthValue(), date.getDayOfMonth()) + .show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName()); }); binding.dueDateTime.setOnClickListener(v -> { - if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null) { - createTimePickerDialogFromDate(this, viewModel.getFullCard().getCard().getDueDate()).show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName()); + final LocalTime time; + if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null && viewModel.getFullCard().getCard().getDueDate() != null) { + time = viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()).toLocalTime(); } else { - createTimePickerDialogFromDate(this, null).show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName()); + time = LocalTime.now(); } + BrandedTimePickerDialog.newInstance(this, time.getHour(), time.getMinute(), true) + .show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName()); }); binding.clearDueDate.setOnClickListener(v -> { binding.dueDateDate.setText(null); binding.dueDateTime.setText(null); viewModel.getFullCard().getCard().setDueDate(null); - binding.clearDueDate.setVisibility(View.GONE); + binding.clearDueDate.setVisibility(GONE); }); } else { binding.dueDateDate.setEnabled(false); binding.dueDateTime.setEnabled(false); - binding.clearDueDate.setVisibility(View.GONE); + binding.clearDueDate.setVisibility(GONE); } } @@ -266,7 +233,7 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis newLabel.setBoardId(boardId); newLabel.setTitle(((LabelAutoCompleteAdapter) binding.labels.getAdapter()).getLastFilterText()); newLabel.setLocalId(null); - WrappedLiveData<Label> createLabelLiveData = syncManager.createLabel(accountId, newLabel, boardId); + WrappedLiveData<Label> createLabelLiveData = viewModel.createLabel(accountId, newLabel, boardId); observeOnce(createLabelLiveData, CardDetailsFragment.this, createdLabel -> { if (createLabelLiveData.hasError()) { DeckLog.logError(createLabelLiveData.getError()); @@ -277,14 +244,14 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(createdLabel); viewModel.getFullCard().getLabels().add(createdLabel); binding.labelsGroup.addView(createChipFromLabel(newLabel)); - binding.labelsGroup.setVisibility(View.VISIBLE); + binding.labelsGroup.setVisibility(VISIBLE); } }); } else { ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(label); viewModel.getFullCard().getLabels().add(label); binding.labelsGroup.addView(createChipFromLabel(label)); - binding.labelsGroup.setVisibility(View.VISIBLE); + binding.labelsGroup.setVisibility(VISIBLE); } binding.labels.setText(""); @@ -296,18 +263,17 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis for (Label label : viewModel.getFullCard().getLabels()) { binding.labelsGroup.addView(createChipFromLabel(label)); } - binding.labelsGroup.setVisibility(View.VISIBLE); + binding.labelsGroup.setVisibility(VISIBLE); } else { binding.labelsGroup.setVisibility(View.INVISIBLE); } } - private Chip createChipFromLabel(Label label) { final Chip chip = new Chip(activity); chip.setText(label.getTitle()); if (viewModel.canEdit()) { - chip.setCloseIcon(getResources().getDrawable(R.drawable.ic_close_circle_grey600)); + chip.setCloseIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_close_circle_grey600)); chip.setCloseIconVisible(true); chip.setOnCloseIconClickListener(v -> { binding.labelsGroup.removeView(chip); @@ -316,9 +282,9 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis }); } try { - final int labelColor = Color.parseColor("#" + label.getColor()); + final int labelColor = label.getColor(); chip.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); - final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor); + final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); chip.setTextColor(color); if (chip.getCloseIcon() != null) { @@ -331,7 +297,15 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis return chip; } - private void setupPeople() { + private void setupAssignees() { + adapter = new AssigneeAdapter((user) -> CardAssigneeDialog.newInstance(user).show(getChildFragmentManager(), CardAssigneeDialog.class.getSimpleName()), viewModel.getAccount()); + binding.assignees.setAdapter(adapter); + binding.assignees.post(() -> { + @Px final int gutter = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x); + final int spanCount = (int) (float) binding.assignees.getWidth() / (DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size) + gutter); + binding.assignees.setLayoutManager(new GridLayoutManager(getContext(), spanCount)); + binding.assignees.addItemDecoration(new AssigneeDecoration(gutter)); + }); if (viewModel.canEdit()) { Long localCardId = viewModel.getFullCard().getCard().getLocalId(); localCardId = localCardId == null ? -1 : localCardId; @@ -340,81 +314,90 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis User user = (User) adapterView.getItemAtPosition(position); viewModel.getFullCard().getAssignedUsers().add(user); ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user); - addAvatar(viewModel.getAccount().getUrl(), user); + adapter.addUser(user); binding.people.setText(""); }); if (this.viewModel.getFullCard().getAssignedUsers() != null) { - binding.peopleList.removeAllViews(); - for (User user : this.viewModel.getFullCard().getAssignedUsers()) { - addAvatar(viewModel.getAccount().getUrl(), user); - } + adapter.setUsers(this.viewModel.getFullCard().getAssignedUsers()); } } else { binding.people.setEnabled(false); } } - private void addAvatar(String baseUrl, User user) { - ImageView avatar = new ImageView(activity); - avatar.setLayoutParams(avatarLayoutParams); - if (viewModel.canEdit()) { - avatar.setOnClickListener(v -> { - viewModel.getFullCard().getAssignedUsers().remove(user); - binding.peopleList.removeView(avatar); - ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user); - BrandedSnackbar.make( - requireView(), getString(R.string.unassigned_user, user.getDisplayname()), - Snackbar.LENGTH_LONG) - .setAction(R.string.simple_undo, v1 -> { - viewModel.getFullCard().getAssignedUsers().add(user); - ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user); - addAvatar(baseUrl, user); - }).show(); - }); - } - binding.peopleList.addView(avatar); - avatar.requestLayout(); - ViewUtil.addAvatar(avatar, baseUrl, user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp); - } - @Override - public void onDateSet(com.wdullaer.materialdatetimepicker.date.DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { - Calendar c = Calendar.getInstance(); + public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { int hourOfDay; int minute; - if (binding.dueDateTime.getText() != null && binding.dueDateTime.length() > 0) { - hourOfDay = this.viewModel.getFullCard().getCard().getDueDate().getHours(); - minute = this.viewModel.getFullCard().getCard().getDueDate().getMinutes(); - } else { + final CharSequence selectedTime = binding.dueDateTime.getText(); + if (TextUtils.isEmpty(selectedTime)) { hourOfDay = 0; minute = 0; + } else { + final LocalTime oldTime = LocalTime.from(this.viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault())); + hourOfDay = oldTime.getHour(); + minute = oldTime.getMinute(); } - c.set(year, monthOfYear, dayOfMonth, hourOfDay, minute); - this.viewModel.getFullCard().getCard().setDueDate(c.getTime()); - binding.dueDateDate.setText(dateFormat.format(c.getTime())); + final ZonedDateTime newDateTime = ZonedDateTime.of( + LocalDate.of(year, monthOfYear + 1, dayOfMonth), + LocalTime.of(hourOfDay, minute), + ZoneId.systemDefault() + ); + this.viewModel.getFullCard().getCard().setDueDate(newDateTime.toInstant()); + binding.dueDateDate.setText(newDateTime.format(dateFormatter)); - if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().getTime() == 0) { - binding.clearDueDate.setVisibility(View.GONE); + if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().toEpochMilli() == 0) { + binding.clearDueDate.setVisibility(GONE); } else { - binding.clearDueDate.setVisibility(View.VISIBLE); + binding.clearDueDate.setVisibility(VISIBLE); } } @Override - public void onTimeSet(com.wdullaer.materialdatetimepicker.time.TimePickerDialog view, int hourOfDay, int minute, int second) { - if (this.viewModel.getFullCard().getCard().getDueDate() == null) { - this.viewModel.getFullCard().getCard().setDueDate(new Date()); + public void onTimeSet(TimePickerDialog view, int hourOfDay, int minute, int second) { + final Instant oldInstant = this.viewModel.getFullCard().getCard().getDueDate(); + final ZonedDateTime oldDateTime = oldInstant == null ? ZonedDateTime.now() : oldInstant.atZone(ZoneId.systemDefault()); + final ZonedDateTime newDateTime = oldDateTime.with( + LocalTime.of(hourOfDay, minute) + ); + + this.viewModel.getFullCard().getCard().setDueDate(newDateTime.toInstant()); + binding.dueDateTime.setText(newDateTime.format(timeFormatter)); + if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().toEpochMilli() == 0) { + binding.clearDueDate.setVisibility(GONE); + } else { + binding.clearDueDate.setVisibility(VISIBLE); } - this.viewModel.getFullCard().getCard().getDueDate().setHours(hourOfDay); - this.viewModel.getFullCard().getCard().getDueDate().setMinutes(minute); - binding.dueDateTime.setText(dueTime.format(this.viewModel.getFullCard().getCard().getDueDate().getTime())); - if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().getTime() == 0) { - binding.clearDueDate.setVisibility(View.GONE); + } + + private void setupProjects() { + if (viewModel.getFullCard().getProjects().size() > 0) { + binding.projectsTitle.setVisibility(VISIBLE); + binding.projects.setNestedScrollingEnabled(false); + final CardProjectsAdapter adapter = new CardProjectsAdapter(viewModel.getFullCard().getProjects(), getChildFragmentManager()); + binding.projects.setAdapter(adapter); + binding.projects.setVisibility(VISIBLE); } else { - binding.clearDueDate.setVisibility(View.VISIBLE); + binding.projectsTitle.setVisibility(GONE); + binding.projects.setVisibility(GONE); } } + + @Override + public void onUnassignUser(@NonNull User user) { + viewModel.getFullCard().getAssignedUsers().remove(user); + adapter.removeUser(user); + ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user); + BrandedSnackbar.make( + requireView(), getString(R.string.unassigned_user, user.getDisplayname()), + Snackbar.LENGTH_LONG) + .setAction(R.string.simple_undo, v1 -> { + viewModel.getFullCard().getAssignedUsers().add(user); + ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user); + adapter.addUser(user); + }).show(); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java deleted file mode 100644 index 2efbab789..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package it.niedermann.nextcloud.deck.ui.card.details; - -import java.util.Date; - -import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.model.User; - -public interface CardDetailsListener { - - void onDescriptionChanged(String toString); - - void onDueDateChanged(Date dueDate); - - void onUserAdded(User user); - - void onUserRemoved(User user); - - void onLabelRemoved(Label label); - - void onLabelAdded(Label createdLabel); -}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java new file mode 100644 index 000000000..0c2d63d74 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java @@ -0,0 +1,52 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.databinding.ItemProjectBinding; +import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources; +import it.niedermann.nextcloud.deck.ui.card.projectresources.CardProjectResourcesDialog; + +public class CardProjectsAdapter extends RecyclerView.Adapter<CardProjectsViewHolder> { + + @NonNull + private final List<OcsProjectWithResources> projects; + @NonNull + private final FragmentManager fragmentManager; + + public CardProjectsAdapter(@NonNull List<OcsProjectWithResources> projects, @NonNull FragmentManager fragmentManager) { + this.projects = new ArrayList<>(projects.size()); + this.projects.addAll(projects); + this.fragmentManager = fragmentManager; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return projects.get(position).getLocalId(); + } + + @NonNull + @Override + public CardProjectsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new CardProjectsViewHolder(ItemProjectBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull CardProjectsViewHolder holder, int position) { + final OcsProjectWithResources project = projects.get(position); + holder.bind(project, (v) -> CardProjectResourcesDialog.newInstance(project.getName(), project.getResources()).show(fragmentManager, CardProjectResourcesDialog.class.getSimpleName())); + } + + @Override + public int getItemCount() { + return projects.size(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java new file mode 100644 index 000000000..9c5494af7 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java @@ -0,0 +1,32 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import android.view.View.OnClickListener; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemProjectBinding; +import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources; + +public class CardProjectsViewHolder extends RecyclerView.ViewHolder { + + private ItemProjectBinding binding; + + public CardProjectsViewHolder(@NonNull ItemProjectBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull OcsProjectWithResources project, @Nullable OnClickListener onClickListener) { + binding.projectName.setText(project.getName()); + final int resourcesCount = project.getResources().size(); + binding.resourcesCount.setText(itemView.getContext().getResources().getQuantityString(R.plurals.resources_count, resourcesCount, resourcesCount)); + if (resourcesCount > 0) { + binding.getRoot().setOnClickListener(onClickListener); + } else { + binding.getRoot().setOnClickListener(null); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java new file mode 100644 index 000000000..4c95574c3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java @@ -0,0 +1,54 @@ +package it.niedermann.nextcloud.deck.ui.card.projectresources; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.databinding.ItemProjectResourceBinding; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; + +public class CardProjectResourceAdapter extends RecyclerView.Adapter<CardProjectResourceViewHolder> { + + @NonNull + private final EditCardViewModel viewModel; + @NonNull + private final List<OcsProjectResource> resources; + @NonNull + private final LifecycleOwner owner; + + public CardProjectResourceAdapter(@NonNull EditCardViewModel viewModel, @NonNull List<OcsProjectResource> resources, @NonNull LifecycleOwner owner) { + this.viewModel = viewModel; + this.resources = new ArrayList<>(resources.size()); + this.resources.addAll(resources); + this.owner = owner; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return resources.get(position).getLocalId(); + } + + @NonNull + @Override + public CardProjectResourceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new CardProjectResourceViewHolder(ItemProjectResourceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull CardProjectResourceViewHolder holder, int position) { + holder.bind(viewModel, resources.get(position), owner); + } + + @Override + public int getItemCount() { + return this.resources.size(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java new file mode 100644 index 000000000..272945e45 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java @@ -0,0 +1,110 @@ +package it.niedermann.nextcloud.deck.ui.card.projectresources; + +import android.content.Intent; +import android.content.res.Resources; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemProjectResourceBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; +import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; +import it.niedermann.nextcloud.deck.util.ProjectUtil; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static it.niedermann.nextcloud.deck.util.ProjectUtil.getResourceUri; + +public class CardProjectResourceViewHolder extends RecyclerView.ViewHolder { + @NonNull + private final ItemProjectResourceBinding binding; + + public CardProjectResourceViewHolder(@NonNull ItemProjectResourceBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull EditCardViewModel viewModel, @NonNull OcsProjectResource resource, @NonNull LifecycleOwner owner) { + final Account account = viewModel.getAccount(); + final Resources resources = itemView.getResources(); + binding.name.setText(resource.getName()); + final @Nullable String link = resource.getLink(); + binding.type.setVisibility(VISIBLE); + if (resource.getType() != null) { + switch (resource.getType()) { + case "deck": { + // TODO https://github.com/stefan-niedermann/nextcloud-deck/issues/671 + linkifyViewHolder(account, link); + binding.type.setText(resources.getString(R.string.project_type_deck_board)); + binding.image.setImageResource(R.drawable.project_deck_36dp); + break; + } + case "deck-card": { + try { + long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link); + if (ids.length == 2) { + viewModel.getCardByRemoteID(account.getId(), ids[1]).observe(owner, (fullCard) -> { + if (fullCard != null) { + viewModel.getBoardByRemoteId(account.getId(), ids[0]).observe(owner, (board) -> { + if (board != null) { + binding.getRoot().setOnClickListener((v) -> itemView.getContext().startActivity(EditActivity.createEditCardIntent(itemView.getContext(), account, board.getLocalId(), fullCard.getLocalId()))); + } else { + linkifyViewHolder(account, link); + } + }); + } else { + linkifyViewHolder(account, link); + } + }); + } else { + linkifyViewHolder(account, link); + } + } catch (IllegalArgumentException e) { + DeckLog.logError(e); + linkifyViewHolder(account, link); + } + binding.type.setText(resources.getString(R.string.project_type_deck_card)); + binding.image.setImageResource(R.drawable.project_deck_36dp); + break; + } + case "file": { + binding.type.setText(resources.getString(R.string.project_type_file)); + linkifyViewHolder(account, link); + binding.image.setImageResource(R.drawable.project_file_36dp); + break; + } + case "room": { + binding.type.setText(resources.getString(R.string.project_type_room)); + linkifyViewHolder(account, link); + binding.image.setImageResource(R.drawable.project_talk_36dp); + break; + } + default: { + DeckLog.info("Unknown resource type for " + resource.getName() + ": " + resource.getType()); + binding.type.setVisibility(GONE); + linkifyViewHolder(account, link); + break; + } + } + } else { + DeckLog.warn("Resource type for " + resource.getName() + " is null"); + binding.type.setVisibility(GONE); + } + } + + private void linkifyViewHolder(@NonNull Account account, @Nullable String link) { + if (link != null) { + try { + binding.getRoot().setOnClickListener((v) -> itemView.getContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(getResourceUri(account, link)))); + } catch (IllegalArgumentException e) { + DeckLog.logError(e); + } + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java new file mode 100644 index 000000000..46195b309 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java @@ -0,0 +1,83 @@ +package it.niedermann.nextcloud.deck.ui.card.projectresources; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.DialogProjectResourcesBinding; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; +import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; + +public class CardProjectResourcesDialog extends BrandedDialogFragment { + + private static final String KEY_RESOURCES = "resources"; + private static final String KEY_PROJECT_NAME = "projectName"; + private DialogProjectResourcesBinding binding; + private EditCardViewModel viewModel; + + private String projectName; + @NonNull + private List<OcsProjectResource> resources = new ArrayList<>(); + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + final Bundle args = requireArguments(); + if (!args.containsKey(KEY_RESOURCES)) { + throw new IllegalArgumentException("Provide at least " + KEY_RESOURCES); + } + //noinspection unchecked + this.resources.addAll((ArrayList<OcsProjectResource>) Objects.requireNonNull(args.getSerializable(KEY_RESOURCES))); + this.projectName = args.getString(KEY_PROJECT_NAME); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + binding = DialogProjectResourcesBinding.inflate(LayoutInflater.from(requireContext())); + viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + + AlertDialog.Builder dialogBuilder = new BrandedAlertDialogBuilder(requireContext()); + + return dialogBuilder + .setTitle(projectName) + .setView(binding.getRoot()) + .setNeutralButton(R.string.simple_close, null) + .create(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + final CardProjectResourceAdapter adapter = new CardProjectResourceAdapter(viewModel, resources, requireActivity()); + binding.getRoot().setAdapter(adapter); + super.onActivityCreated(savedInstanceState); + } + + @Override + public void applyBrand(int mainColor) { + + } + + public static DialogFragment newInstance(@Nullable String projectName, @NonNull List<OcsProjectResource> resources) { + final DialogFragment fragment = new CardProjectResourcesDialog(); + final Bundle args = new Bundle(); + args.putString(KEY_PROJECT_NAME, projectName); + args.putSerializable(KEY_RESOURCES, new ArrayList<>(resources)); + fragment.setArguments(args); + return fragment; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java index 9eef878c3..ac6335b90 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java @@ -8,13 +8,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.android.util.ClipboardUtil; +import it.niedermann.nextcloud.deck.BuildConfig; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ActivityExceptionBinding; import it.niedermann.nextcloud.deck.ui.exception.tips.TipsAdapter; -import it.niedermann.nextcloud.deck.util.ExceptionUtil; - -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; +import it.niedermann.nextcloud.exception.ExceptionUtil; public class ExceptionActivity extends AppCompatActivity { @@ -22,9 +21,12 @@ public class ExceptionActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final ActivityExceptionBinding binding = ActivityExceptionBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); - super.onCreate(savedInstanceState); + setSupportActionBar(binding.toolbar); Throwable throwable = ((Throwable) getIntent().getSerializableExtra(KEY_THROWABLE)); @@ -32,23 +34,18 @@ public class ExceptionActivity extends AppCompatActivity { throwable = new Exception("Could not get exception"); } - DeckLog.logError(throwable); + final TipsAdapter adapter = new TipsAdapter(this::startActivity); + final String debugInfo = "Full Crash:\n\n" + ExceptionUtil.INSTANCE.getDebugInfos(this, throwable, BuildConfig.FLAVOR); - setSupportActionBar(binding.toolbar); + binding.tips.setAdapter(adapter); + binding.tips.setNestedScrollingEnabled(false); binding.toolbar.setTitle(R.string.error); binding.message.setText(throwable.getMessage()); - - final String debugInfo = ExceptionUtil.getDebugInfos(this, throwable, null); - binding.stacktrace.setText(debugInfo); + binding.copy.setOnClickListener((v) -> ClipboardUtil.INSTANCE.copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfo + "\n```")); + binding.close.setOnClickListener((v) -> finish()); - final TipsAdapter adapter = new TipsAdapter(this::startActivity); - binding.tips.setAdapter(adapter); - binding.tips.setNestedScrollingEnabled(false); adapter.setThrowable(this, null, throwable); - - binding.copy.setOnClickListener((v) -> copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfo + "\n```")); - binding.close.setOnClickListener((v) -> finish()); } @NonNull diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java index 6c0d0ba79..7a84ce0b6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java @@ -11,14 +11,14 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDialogFragment; import androidx.fragment.app.DialogFragment; +import it.niedermann.android.util.ClipboardUtil; +import it.niedermann.nextcloud.deck.BuildConfig; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogExceptionBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.ui.exception.tips.TipsAdapter; -import it.niedermann.nextcloud.deck.util.ExceptionUtil; - -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; +import it.niedermann.nextcloud.exception.ExceptionUtil; public class ExceptionDialogFragment extends AppCompatDialogFragment { @@ -52,7 +52,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment { final TipsAdapter adapter = new TipsAdapter((actionIntent) -> requireActivity().startActivity(actionIntent)); - final String debugInfos = ExceptionUtil.getDebugInfos(requireContext(), throwable, account); + final String debugInfos = ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), throwable, BuildConfig.FLAVOR, account == null ? null : account.getServerDeckVersion()); binding.tips.setAdapter(adapter); binding.stacktrace.setText(debugInfos); @@ -65,7 +65,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment { .setView(binding.getRoot()) .setTitle(R.string.error_dialog_title) .setPositiveButton(android.R.string.copy, (a, b) -> { - copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```"); + ClipboardUtil.INSTANCE.copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```"); a.dismiss(); }) .setNegativeButton(R.string.simple_close, null) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java index 8f0bfce33..c62b23e51 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java @@ -2,21 +2,24 @@ package it.niedermann.nextcloud.deck.ui.exception; import android.app.Activity; -import org.jetbrains.annotations.NotNull; +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.DeckLog; public class ExceptionHandler implements Thread.UncaughtExceptionHandler { - private Activity context; + @NonNull + private final Activity activity; - public ExceptionHandler(Activity context) { - super(); - this.context = context; + public ExceptionHandler(@NonNull Activity activity) { + this.activity = activity; } @Override - public void uncaughtException(@NotNull Thread t, Throwable e) { - context.getApplicationContext().startActivity(ExceptionActivity.createIntent(context, e)); - context.finish(); + public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { + DeckLog.logError(e); + activity.getApplicationContext().startActivity(ExceptionActivity.createIntent(activity, e)); + activity.finish(); Runtime.getRuntime().exit(0); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java index a059b2956..6bfd82b13 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java @@ -39,6 +39,10 @@ import static it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment. public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { + private static final Intent INTENT_APP_INFO = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)) + .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info); + @NonNull private Consumer<Intent> actionButtonClickedListener; @NonNull @@ -68,11 +72,8 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { public void setThrowable(@NonNull Context context, @Nullable Account account, @NonNull Throwable throwable) { if (throwable instanceof TokenMismatchException) { add(R.string.error_dialog_tip_token_mismatch_retry); - add(R.string.error_dialog_tip_token_mismatch_clear_storage); - Intent intent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)) - .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info); - add(R.string.error_dialog_tip_clear_storage, intent); + add(R.string.error_dialog_tip_clear_storage_might_help); + add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO); } else if (throwable instanceof NextcloudFilesAppNotSupportedException) { add(R.string.error_dialog_tip_files_outdated); } else if (throwable instanceof NextcloudApiNotRespondingException) { @@ -122,6 +123,10 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { } else { add(R.string.error_dialog_version_not_parsable); } + add(R.string.error_dialog_account_might_not_be_authorized); + break; + case UNKNOWN_ACCOUNT_USER_ID: + add(R.string.error_dialog_user_not_found_in_database); break; case CAPABILITIES_NOT_PARSABLE: default: @@ -133,15 +138,15 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { add(R.string.error_dialog_capabilities_not_parsable); } } + // Files app might no longer be authenticated: https://github.com/stefan-niedermann/nextcloud-deck/issues/621#issuecomment-665533567 + add(R.string.error_dialog_tip_clear_storage_might_help); + add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO); } else if (throwable instanceof RuntimeException) { if (throwable.getMessage() != null && throwable.getMessage().contains("database")) { Intent reportIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.url_report_bug))) .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_report_issue); add(R.string.error_dialog_tip_database_upgrade_failed, reportIntent); - Intent clearIntent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)) - .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info); - add(R.string.error_dialog_tip_clear_storage, clearIntent); + add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO); } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java index aa6f59d04..6aa03b811 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java @@ -8,6 +8,7 @@ import android.os.Bundle; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -19,6 +20,7 @@ import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayoutMediator; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogFilterBinding; import it.niedermann.nextcloud.deck.model.enums.EDueType; @@ -45,8 +47,9 @@ public class FilterDialogFragment extends BrandedDialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - indicator = getResources().getDrawable(R.drawable.circle_grey600_8dp); - indicator.setColorFilter(getResources().getColor(R.color.primary), PorterDuff.Mode.SRC_ATOP); + indicator = ContextCompat.getDrawable(requireContext(), R.drawable.circle_grey600_8dp); + assert indicator != null; + indicator.setColorFilter(getResources().getColor(R.color.defaultBrand), PorterDuff.Mode.SRC_ATOP); filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); @@ -61,10 +64,10 @@ public class FilterDialogFragment extends BrandedDialogFragment { filterInformationDraft.observe(this, (draft) -> { switch (position) { case 0: - tab.setIcon(draft.getLabels().size() > 0 ? indicator : null); + tab.setIcon(draft.getLabels().size() > 0 || draft.isNoAssignedLabel() ? indicator : null); break; case 1: - tab.setIcon(draft.getUsers().size() > 0 ? indicator : null); + tab.setIcon(draft.getUsers().size() > 0 || draft.isNoAssignedUser() ? indicator : null); break; case 2: tab.setIcon(draft.getDueType() != EDueType.NO_FILTER ? indicator : null); @@ -103,9 +106,10 @@ public class FilterDialogFragment extends BrandedDialogFragment { @Override public void applyBrand(int mainColor) { - @ColorInt int finalMainColor = getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor); - binding.tabLayout.setSelectedTabIndicatorColor(finalMainColor); - indicator.setColorFilter(finalMainColor, PorterDuff.Mode.SRC_ATOP); + @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(binding.tabLayout.getContext(), mainColor); + final boolean contrastRatioIsSufficient = ColorUtil.INSTANCE.getContrastRatio(mainColor, ContextCompat.getColor(binding.tabLayout.getContext(), R.color.primary)) > 1.7d; + binding.tabLayout.setSelectedTabIndicatorColor(contrastRatioIsSufficient ? mainColor : finalMainColor); + indicator.setColorFilter(contrastRatioIsSufficient ? mainColor : finalMainColor, PorterDuff.Mode.SRC_ATOP); } private static class TabsPagerAdapter extends FragmentStateAdapter { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java index 096f0db9c..39fb791be 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java @@ -1,20 +1,21 @@ package it.niedermann.nextcloud.deck.ui.filter; import android.content.res.ColorStateList; -import android.graphics.Color; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; +import it.niedermann.android.util.ColorUtil; +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemFilterLabelBinding; import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.util.ColorUtil; @SuppressWarnings("WeakerAccess") public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapter.LabelViewHolder> { @@ -23,11 +24,17 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte @NonNull private final List<Label> selectedLabels = new ArrayList<>(); @Nullable + private static final Label NOT_ASSIGNED = null; + @Nullable private final SelectionListener<Label> selectionListener; - public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, @Nullable SelectionListener<Label> selectionListener) { + public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, boolean noAssignedLabel, @Nullable SelectionListener<Label> selectionListener) { super(); + this.labels.add(NOT_ASSIGNED); this.labels.addAll(labels); + if (noAssignedLabel) { + this.selectedLabels.add(NOT_ASSIGNED); + } this.selectedLabels.addAll(selectedLabels); this.selectionListener = selectionListener; setHasStableIds(true); @@ -36,7 +43,8 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte @Override public long getItemId(int position) { - return labels.get(position).getLocalId(); + @Nullable final Label label = labels.get(position); + return label == null ? -1L : label.getLocalId(); } @NonNull @@ -47,7 +55,11 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte @Override public void onBindViewHolder(@NonNull LabelViewHolder viewHolder, int position) { - viewHolder.bind(labels.get(position)); + if (position == 0) { + viewHolder.bindNotAssigned(); + } else { + viewHolder.bind(labels.get(position)); + } } @Override @@ -55,26 +67,36 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte return labels.size(); } - public List<Label> getSelected() { - return selectedLabels; - } - class LabelViewHolder extends RecyclerView.ViewHolder { private ItemFilterLabelBinding binding; LabelViewHolder(@NonNull ItemFilterLabelBinding binding) { super(binding.getRoot()); this.binding = binding; + this.binding.label.setClickable(false); } void bind(final Label label) { binding.label.setText(label.getTitle()); - final int labelColor = Color.parseColor("#" + label.getColor()); + final int labelColor = label.getColor(); binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); - final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor); + final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); binding.label.setTextColor(color); itemView.setSelected(selectedLabels.contains(label)); + bindClickListener(label); + } + + public void bindNotAssigned() { + binding.label.setText(itemView.getContext().getString(R.string.no_assigned_label)); + binding.label.setTextColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.accent))); + binding.label.setChipIcon(ContextCompat.getDrawable(itemView.getContext(), R.drawable.ic_baseline_block_24)); + binding.label.setChipBackgroundColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.primary))); + binding.label.setRippleColor(null); + itemView.setSelected(selectedLabels.contains(NOT_ASSIGNED)); + bindClickListener(NOT_ASSIGNED); + } + private void bindClickListener(@Nullable Label label) { itemView.setOnClickListener(view -> { if (selectedLabels.contains(label)) { selectedLabels.remove(label); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java index 357f93cf9..e7d693185 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java @@ -12,7 +12,6 @@ import androidx.lifecycle.ViewModelProvider; import it.niedermann.nextcloud.deck.databinding.DialogFilterLabelsBinding; import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.MainViewModel; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; @@ -31,21 +30,33 @@ public class FilterLabelsFragment extends Fragment implements SelectionListener< filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); - observeOnce(new SyncManager(requireContext()).findProposalsForLabelsToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (labels) -> { + observeOnce(filterViewModel.findProposalsForLabelsToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (labels) -> { binding.labels.setNestedScrollingEnabled(false); - binding.labels.setAdapter(new FilterLabelsAdapter(labels, requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(), this)); + binding.labels.setAdapter(new FilterLabelsAdapter( + labels, + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(), + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedLabel(), + this)); }); return binding.getRoot(); } @Override - public void onItemSelected(Label item) { - filterViewModel.addFilterInformationDraftLabel(item); + public void onItemSelected(@Nullable Label item) { + if (item == null) { + filterViewModel.setNotAssignedLabel(true); + } else { + filterViewModel.addFilterInformationDraftLabel(item); + } } @Override - public void onItemDeselected(Label item) { - filterViewModel.removeFilterInformationLabel(item); + public void onItemDeselected(@Nullable Label item) { + if (item == null) { + filterViewModel.setNotAssignedLabel(false); + } else { + filterViewModel.removeFilterInformationLabel(item); + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java index b4ae8f679..4b75b985f 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java @@ -8,6 +8,8 @@ import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; + import java.util.ArrayList; import java.util.List; @@ -23,6 +25,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us final int avatarSize; @NonNull private final Account account; + @Nullable + private static final User NOT_ASSIGNED = null; @NonNull private final List<User> users = new ArrayList<>(); @NonNull @@ -30,11 +34,15 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us @Nullable private final SelectionListener<User> selectionListener; - public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, @Nullable SelectionListener selectionListener) { + public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, boolean noAssignedUser, @Nullable SelectionListener<User> selectionListener) { super(); this.avatarSize = avatarSize; this.account = account; + this.users.add(NOT_ASSIGNED); this.users.addAll(users); + if (noAssignedUser) { + this.selectedUsers.add(NOT_ASSIGNED); + } this.selectedUsers.addAll(selectedUsers); this.selectionListener = selectionListener; setHasStableIds(true); @@ -43,7 +51,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us @Override public long getItemId(int position) { - return users.get(position).getLocalId(); + @Nullable final User user = users.get(position); + return user == null ? -1L : user.getLocalId(); } @NonNull @@ -54,7 +63,11 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us @Override public void onBindViewHolder(@NonNull UserViewHolder viewHolder, int position) { - viewHolder.bind(users.get(position)); + if (position == 0) { + viewHolder.bindNotAssigned(); + } else { + viewHolder.bind(users.get(position)); + } } @Override @@ -62,10 +75,6 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us return users.size(); } - public List<User> getSelected() { - return selectedUsers; - } - class UserViewHolder extends RecyclerView.ViewHolder { private ItemFilterUserBinding binding; @@ -74,22 +83,34 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us this.binding = binding; } - void bind(final User user) { - binding.displayName.setText(user.getDisplayname()); + void bind(@NonNull final User user) { + binding.title.setText(user.getDisplayname()); ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp); itemView.setSelected(selectedUsers.contains(user)); + bindClickListener(user); + } + + public void bindNotAssigned() { + binding.title.setText(itemView.getContext().getString(R.string.simple_unassigned)); + Glide.with(itemView.getContext()) + .load(R.drawable.ic_baseline_block_24) + .into(binding.avatar); + itemView.setSelected(selectedUsers.contains(NOT_ASSIGNED)); + bindClickListener(NOT_ASSIGNED); + } + private void bindClickListener(@Nullable User user) { itemView.setOnClickListener(view -> { if (selectedUsers.contains(user)) { selectedUsers.remove(user); itemView.setSelected(false); - if(selectionListener != null) { + if (selectionListener != null) { selectionListener.onItemDeselected(user); } } else { selectedUsers.add(user); itemView.setSelected(true); - if(selectionListener != null) { + if (selectionListener != null) { selectionListener.onItemSelected(user); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java index 64bc1db1f..6ffaec6a6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java @@ -10,14 +10,13 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogFilterAssigneesBinding; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.MainViewModel; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; import static java.util.Objects.requireNonNull; public class FilterUserFragment extends Fragment implements SelectionListener<User> { @@ -33,21 +32,35 @@ public class FilterUserFragment extends Fragment implements SelectionListener<Us filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); - observeOnce(new SyncManager(requireContext()).findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> { + observeOnce(filterViewModel.findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> { binding.users.setNestedScrollingEnabled(false); - binding.users.setAdapter(new FilterUserAdapter(dpToPx(requireContext(), R.dimen.avatar_size), mainViewModel.getCurrentAccount(), users, requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(), this)); + binding.users.setAdapter(new FilterUserAdapter( + DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size), + mainViewModel.getCurrentAccount(), + users, + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(), + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedUser(), + this)); }); return binding.getRoot(); } @Override - public void onItemSelected(User item) { - filterViewModel.addFilterInformationUser(item); + public void onItemSelected(@Nullable User item) { + if (item == null) { + filterViewModel.setNotAssignedUser(true); + } else { + filterViewModel.addFilterInformationUser(item); + } } @Override - public void onItemDeselected(User item) { - filterViewModel.removeFilterInformationUser(item); + public void onItemDeselected(@Nullable User item) { + if (item == null) { + filterViewModel.setNotAssignedUser(false); + } else { + filterViewModel.removeFilterInformationUser(item); + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java index cf8dc1754..c42a61ebd 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java @@ -1,28 +1,40 @@ package it.niedermann.nextcloud.deck.ui.filter; +import android.app.Application; + import androidx.annotation.IntRange; import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; + +import java.util.List; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.enums.EDueType; import it.niedermann.nextcloud.deck.model.internal.FilterInformation; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import static it.niedermann.nextcloud.deck.model.internal.FilterInformation.hasActiveFilter; @SuppressWarnings("WeakerAccess") -public class FilterViewModel extends ViewModel { +public class FilterViewModel extends AndroidViewModel { + + private final SyncManager syncManager; @IntRange(from = 0, to = 2) private int currentFilterTab = 0; @NonNull - private MutableLiveData<FilterInformation> filterInformationDraft = new MutableLiveData<>(new FilterInformation()); + private final MutableLiveData<FilterInformation> filterInformationDraft = new MutableLiveData<>(new FilterInformation()); @NonNull - private MutableLiveData<FilterInformation> filterInformation = new MutableLiveData<>(); + private final MutableLiveData<FilterInformation> filterInformation = new MutableLiveData<>(); + + public FilterViewModel(@NonNull Application application) { + super(application); + this.syncManager = new SyncManager(application); + } public void publishFilterInformationDraft() { this.filterInformation.postValue(hasActiveFilter(filterInformationDraft.getValue()) ? filterInformationDraft.getValue() : null); @@ -66,6 +78,18 @@ public class FilterViewModel extends ViewModel { this.filterInformationDraft.postValue(newDraft); } + public void setNotAssignedUser(boolean notAssignedUser) { + FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue()); + newDraft.setNoAssignedUser(notAssignedUser); + this.filterInformationDraft.postValue(newDraft); + } + + public void setNotAssignedLabel(boolean notAssignedLabel) { + FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue()); + newDraft.setNoAssignedLabel(notAssignedLabel); + this.filterInformationDraft.postValue(newDraft); + } + public void removeFilterInformationLabel(@NonNull Label label) { FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue()); newDraft.removeLabel(label); @@ -86,4 +110,12 @@ public class FilterViewModel extends ViewModel { public int getCurrentFilterTab() { return this.currentFilterTab; } + + public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId) { + return syncManager.findProposalsForUsersToAssign(accountId, boardId, -1L, -1); + } + + public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) { + return syncManager.findProposalsForLabelsToAssign(accountId, boardId, -1L); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java index d2635a860..3fad71773 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java @@ -1,9 +1,11 @@ package it.niedermann.nextcloud.deck.ui.filter; +import androidx.annotation.Nullable; + public interface SelectionListener<T> { - void onItemSelected(T item); + void onItemSelected(@Nullable T item); - default void onItemDeselected(T item) { + default void onItemDeselected(@Nullable T item) { // Deselecting is optional } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java index 4b43cbed6..0892eb437 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java @@ -11,14 +11,14 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import it.niedermann.android.glidesso.SingleSignOnUrl; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding; import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; import static android.view.View.GONE; import static android.view.View.VISIBLE; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; public class ManageAccountViewHolder extends RecyclerView.ViewHolder { @@ -33,7 +33,7 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder { binding.accountName.setText(account.getUserName()); binding.accountHost.setText(Uri.parse(account.getUrl()).getHost()); Glide.with(itemView.getContext()) - .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) + .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java index 9d273cdcb..8aa45e39a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java @@ -5,15 +5,12 @@ import android.util.Log; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; - -import com.nextcloud.android.sso.helper.SingleAccountHelper; +import androidx.lifecycle.ViewModelProvider; import it.niedermann.nextcloud.deck.databinding.ActivityManageAccountsBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId; -import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; public class ManageAccountsActivity extends AppCompatActivity { @@ -21,44 +18,37 @@ public class ManageAccountsActivity extends AppCompatActivity { private static final String TAG = ManageAccountsActivity.class.getSimpleName(); private ActivityManageAccountsBinding binding; + private ManageAccountsViewModel viewModel; private ManageAccountAdapter adapter; - private SyncManager syncManager = null; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ActivityManageAccountsBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); + viewModel = new ViewModelProvider(this).get(ManageAccountsViewModel.class); + setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - syncManager = new SyncManager(this); - - adapter = new ManageAccountAdapter((account) -> { - SingleAccountHelper.setCurrentAccount(getApplicationContext(), account.getName()); - syncManager = new SyncManager(this); - saveCurrentAccountId(this, account.getId()); - }, (accountPair) -> { + adapter = new ManageAccountAdapter((account) -> viewModel.setNewAccount(account), (accountPair) -> { if (accountPair.first != null) { - syncManager.deleteAccount(accountPair.first.getId()); + viewModel.deleteAccount(accountPair.first.getId()); } else { throw new IllegalArgumentException("Could not delete account because given account was null."); } Account newAccount = accountPair.second; if (newAccount != null) { - SingleAccountHelper.setCurrentAccount(getApplicationContext(), newAccount.getName()); - saveCurrentAccountId(this, newAccount.getId()); - syncManager = new SyncManager(this); + viewModel.setNewAccount(newAccount); } else { Log.i(TAG, "Got delete account request, but new account is null. Maybe last account has been deleted?"); } }); binding.accounts.setAdapter(adapter); - observeOnce(syncManager.readAccount(readCurrentAccountId(this)), this, (account -> { + observeOnce(viewModel.readAccount(readCurrentAccountId(this)), this, (account -> { adapter.setCurrentAccount(account); - syncManager.readAccounts().observe(this, (localAccounts -> { + viewModel.readAccounts().observe(this, (localAccounts -> { if (localAccounts.size() == 0) { Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName()); setResult(AppCompatActivity.RESULT_FIRST_USER); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java new file mode 100644 index 000000000..66e9d3850 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java @@ -0,0 +1,45 @@ +package it.niedermann.nextcloud.deck.ui.manageaccounts; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.nextcloud.android.sso.helper.SingleAccountHelper; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; + +import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId; + +@SuppressWarnings("WeakerAccess") +public class ManageAccountsViewModel extends AndroidViewModel { + + private SyncManager syncManager; + + public ManageAccountsViewModel(@NonNull Application application) { + super(application); + this.syncManager = new SyncManager(application); + } + + public LiveData<Account> readAccount(long id) { + return syncManager.readAccount(id); + } + + public LiveData<List<Account>> readAccounts() { + return syncManager.readAccounts(); + } + + public void setNewAccount(@NonNull Account account) { + SingleAccountHelper.setCurrentAccount(getApplication(), account.getName()); + syncManager = new SyncManager(getApplication()); + saveCurrentAccountId(getApplication(), account.getId()); + } + + public void deleteAccount(long id) { + syncManager.deleteAccount(id); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java new file mode 100644 index 000000000..2b7eb52fe --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java @@ -0,0 +1,128 @@ +package it.niedermann.nextcloud.deck.ui.movecard; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.DialogMoveCardBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; +import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackFragment; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +public class MoveCardDialogFragment extends BrandedDialogFragment implements PickStackListener { + + private static final String KEY_ORIGIN_ACCOUNT_ID = "account_id"; + private static final String KEY_ORIGIN_BOARD_LOCAL_ID = "board_local_id"; + private static final String KEY_ORIGIN_CARD_TITLE = "card_title"; + private static final String KEY_ORIGIN_CARD_LOCAL_ID = "card_local_id"; + private Long originAccountId; + private Long originBoardLocalId; + private String originCardTitle; + private Long originCardLocalId; + + private DialogMoveCardBinding binding; + private PickStackViewModel viewModel; + private MoveCardListener moveCardListener; + + private Account selectedAccount; + private Board selectedBoard; + private Stack selectedStack; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getParentFragment() instanceof MoveCardListener) { + this.moveCardListener = (MoveCardListener) getParentFragment(); + } else if (context instanceof MoveCardListener) { + this.moveCardListener = (MoveCardListener) context; + } else { + throw new IllegalArgumentException("Caller must implement " + MoveCardListener.class.getSimpleName()); + } + + final Bundle args = requireArguments(); + originAccountId = args.getLong(KEY_ORIGIN_ACCOUNT_ID, -1L); + if (originAccountId < 0) { + throw new IllegalArgumentException("Missing " + KEY_ORIGIN_ACCOUNT_ID); + } + originCardLocalId = args.getLong(KEY_ORIGIN_CARD_LOCAL_ID, -1L); + if (originCardLocalId < 0) { + throw new IllegalArgumentException("Missing " + KEY_ORIGIN_CARD_LOCAL_ID); + } + originBoardLocalId = args.getLong(KEY_ORIGIN_BOARD_LOCAL_ID, -1L); + if (originBoardLocalId < 0) { + throw new IllegalArgumentException("Missing " + KEY_ORIGIN_BOARD_LOCAL_ID); + } + originCardTitle = args.getString(KEY_ORIGIN_CARD_TITLE); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = DialogMoveCardBinding.inflate(inflater); + binding.title.setText(getString(R.string.action_card_move_title, originCardTitle)); + binding.submit.setOnClickListener((v) -> { + DeckLog.verbose("[Move card] Attempt to move to " + Stack.class.getSimpleName() + " #" + selectedStack.getLocalId()); + this.moveCardListener.move(originAccountId, originCardLocalId, selectedAccount.getId(), selectedBoard.getLocalId(), selectedStack.getLocalId()); + dismiss(); + }); + binding.cancel.setOnClickListener((v) -> dismiss()); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getChildFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, PickStackFragment.newInstance(false)) + .commit(); + } + + @Override + public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) { + this.selectedAccount = account; + this.selectedBoard = board; + this.selectedStack = stack; + if (board == null || stack == null) { + binding.submit.setEnabled(false); + binding.moveWarning.setVisibility(GONE); + } else { + binding.submit.setEnabled(true); + binding.moveWarning.setVisibility(board.getLocalId().equals(originBoardLocalId) ? GONE : VISIBLE); + } + } + + @Override + public void applyBrand(int mainColor) { + final ColorStateList mainColorStateList = ColorStateList.valueOf(BrandingUtil.getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor)); + binding.cancel.setTextColor(mainColorStateList); + binding.submit.setTextColor(mainColorStateList); + } + + public static DialogFragment newInstance(long originAccountId, long originBoardLocalId, String originCardTitle, Long originCardLocalId) { + final DialogFragment dialogFragment = new MoveCardDialogFragment(); + final Bundle args = new Bundle(); + args.putLong(KEY_ORIGIN_ACCOUNT_ID, originAccountId); + args.putLong(KEY_ORIGIN_BOARD_LOCAL_ID, originBoardLocalId); + args.putString(KEY_ORIGIN_CARD_TITLE, originCardTitle); + args.putLong(KEY_ORIGIN_CARD_LOCAL_ID, originCardLocalId); + dialogFragment.setArguments(args); + return dialogFragment; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java new file mode 100644 index 000000000..f6f7a7a1f --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java @@ -0,0 +1,5 @@ +package it.niedermann.nextcloud.deck.ui.movecard; + +public interface MoveCardListener { + void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java new file mode 100644 index 000000000..d65971cf4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java @@ -0,0 +1,204 @@ +package it.niedermann.nextcloud.deck.ui.pickstack; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; + +import java.util.List; + +import it.niedermann.nextcloud.deck.databinding.FragmentPickStackBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.ui.ImportAccountActivity; +import it.niedermann.nextcloud.deck.ui.preparecreate.AccountAdapter; +import it.niedermann.nextcloud.deck.ui.preparecreate.BoardAdapter; +import it.niedermann.nextcloud.deck.ui.preparecreate.SelectedListener; +import it.niedermann.nextcloud.deck.ui.preparecreate.StackAdapter; + +import static androidx.lifecycle.Transformations.switchMap; +import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId; +import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentBoardId; +import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentStackId; + +public class PickStackFragment extends Fragment { + + private FragmentPickStackBinding binding; + private PickStackViewModel viewModel; + + private static final String KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION = "show_boards_without_edit_permission"; + + private PickStackListener pickStackListener; + + private boolean showBoardsWithoutEditPermission = false; + private long lastAccountId; + private long lastBoardId; + private long lastStackId; + + private ArrayAdapter<Account> accountAdapter; + private ArrayAdapter<Board> boardAdapter; + private ArrayAdapter<Stack> stackAdapter; + + @Nullable + private LiveData<List<Board>> boardsLiveData; + @NonNull + private Observer<List<Board>> boardsObserver = (boards) -> { + boardAdapter.clear(); + boardAdapter.addAll(boards); + binding.boardSelect.setEnabled(true); + + if (boards.size() > 0) { + binding.boardSelect.setEnabled(true); + + Board boardToSelect = null; + for (Board board : boards) { + if (board.getLocalId() == lastBoardId) { + boardToSelect = board; + break; + } + } + if (boardToSelect == null) { + boardToSelect = boards.get(0); + } + binding.boardSelect.setSelection(boardAdapter.getPosition(boardToSelect)); + } else { + binding.boardSelect.setEnabled(false); + pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), null, null); + } + }; + + @Nullable + private LiveData<List<Stack>> stacksLiveData; + @NonNull + private Observer<List<Stack>> stacksObserver = (stacks) -> { + stackAdapter.clear(); + stackAdapter.addAll(stacks); + + if (stacks.size() > 0) { + binding.stackSelect.setEnabled(true); + + Stack stackToSelect = null; + for (Stack stack : stacks) { + if (stack.getLocalId() == lastStackId) { + stackToSelect = stack; + break; + } + } + if (stackToSelect == null) { + stackToSelect = stacks.get(0); + } + binding.stackSelect.setSelection(stackAdapter.getPosition(stackToSelect)); + } else { + binding.stackSelect.setEnabled(false); + pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), null); + } + }; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getParentFragment() instanceof PickStackListener) { + this.pickStackListener = (PickStackListener) getParentFragment(); + } else if (context instanceof PickStackListener) { + this.pickStackListener = (PickStackListener) context; + } else { + throw new IllegalArgumentException("Caller must implement " + PickStackListener.class.getSimpleName()); + } + final Bundle args = getArguments(); + if (args != null) { + this.showBoardsWithoutEditPermission = args.getBoolean(KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION, false); + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = FragmentPickStackBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(requireActivity()).get(PickStackViewModel.class); + + accountAdapter = new AccountAdapter(requireContext()); + binding.accountSelect.setAdapter(accountAdapter); + binding.accountSelect.setEnabled(false); + boardAdapter = new BoardAdapter(requireContext()); + binding.boardSelect.setAdapter(boardAdapter); + binding.stackSelect.setEnabled(false); + stackAdapter = new StackAdapter(requireContext()); + binding.stackSelect.setAdapter(stackAdapter); + binding.stackSelect.setEnabled(false); + + switchMap(viewModel.hasAccounts(), hasAccounts -> { + if (hasAccounts) { + return viewModel.readAccounts(); + } else { + startActivityForResult(new Intent(requireActivity(), ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + return null; + } + }).observe(getViewLifecycleOwner(), (List<Account> accounts) -> { + if (accounts == null || accounts.size() == 0) { + throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry"); + } + + lastAccountId = readCurrentAccountId(requireContext()); + lastBoardId = readCurrentBoardId(requireContext(), lastAccountId); + lastStackId = readCurrentStackId(requireContext(), lastAccountId, lastBoardId); + + accountAdapter.clear(); + accountAdapter.addAll(accounts); + binding.accountSelect.setEnabled(true); + + for (Account account : accounts) { + if (account.getId() == lastAccountId) { + binding.accountSelect.setSelection(accountAdapter.getPosition(account)); + break; + } + } + }); + + binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { + updateLiveDataSource(boardsLiveData, boardsObserver, showBoardsWithoutEditPermission + ? viewModel.getBoards(parent.getSelectedItemId()) + : viewModel.getBoardsWithEditPermission(parent.getSelectedItemId())); + }); + + binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { + updateLiveDataSource(stacksLiveData, stacksObserver, viewModel.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId())); + }); + + binding.stackSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { + pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), (Stack) parent.getSelectedItem()); + }); + + return binding.getRoot(); + } + + /** + * Updates the source of the given liveData and de- and reregisters the given observer. + */ + private <T> void updateLiveDataSource(@Nullable LiveData<T> liveData, Observer<T> observer, LiveData<T> newSource) { + if (liveData != null) { + liveData.removeObserver(observer); + } + liveData = newSource; + liveData.observe(getViewLifecycleOwner(), observer); + } + + public static PickStackFragment newInstance(boolean showBoardsWithoutEditPermission) { + final PickStackFragment fragment = new PickStackFragment(); + final Bundle args = new Bundle(); + args.putBoolean(KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION, showBoardsWithoutEditPermission); + fragment.setArguments(args); + return fragment; + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java new file mode 100644 index 000000000..ce227746a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java @@ -0,0 +1,12 @@ +package it.niedermann.nextcloud.deck.ui.pickstack; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; + +public interface PickStackListener { + void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java new file mode 100644 index 000000000..cc9fc2259 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java @@ -0,0 +1,45 @@ +package it.niedermann.nextcloud.deck.ui.pickstack; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; + +@SuppressWarnings("WeakerAccess") +public class PickStackViewModel extends AndroidViewModel { + + private final SyncManager syncManager; + + public PickStackViewModel(@NonNull Application application) { + super(application); + this.syncManager = new SyncManager(application); + } + + public LiveData<Boolean> hasAccounts() { + return syncManager.hasAccounts(); + } + + public LiveData<List<Account>> readAccounts() { + return syncManager.readAccounts(); + } + + public LiveData<List<Board>> getBoards(long accountId) { + return syncManager.getBoards(accountId); + } + + public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) { + return syncManager.getBoardsWithEditPermission(accountId); + } + + public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { + return syncManager.getStacksForBoard(accountId, localBoardId); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java index 35a98efd7..f537c9fb4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java @@ -6,14 +6,17 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; -import org.jetbrains.annotations.NotNull; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import java.net.URL; + +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateAccountBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.util.DimensionUtil; -import it.niedermann.nextcloud.deck.util.ViewUtil; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; public class AccountAdapter extends AbstractAdapter<Account> { @@ -27,9 +30,9 @@ public class AccountAdapter extends AbstractAdapter<Account> { return item.getId(); } - @NotNull + @NonNull @Override - public View getView(int position, View convertView, @NotNull ViewGroup parent) { + public View getView(int position, View convertView, @NonNull ViewGroup parent) { final ItemPrepareCreateAccountBinding binding; if (convertView == null) { binding = ItemPrepareCreateAccountBinding.inflate(inflater, parent, false); @@ -40,8 +43,18 @@ public class AccountAdapter extends AbstractAdapter<Account> { final Account item = getItem(position); if (item != null) { binding.username.setText(item.getUserName()); - binding.instance.setText(item.getUrl()); - ViewUtil.addAvatar(binding.avatar, item.getUrl(), item.getUserName(), DimensionUtil.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp); + try { + binding.instance.setText(new URL(item.getUrl()).getHost()); + } catch (Throwable t) { + binding.instance.setText(item.getUrl()); + } + + Glide.with(getContext()) + .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details)))) + .placeholder(R.drawable.ic_baseline_account_circle_24) + .error(R.drawable.ic_baseline_account_circle_24) + .apply(RequestOptions.circleCropTransform()) + .into(binding.avatar); } else { DeckLog.logError(new IllegalArgumentException("No item for position " + position)); } 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 190c55e4b..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 @@ -6,8 +6,6 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; -import org.jetbrains.annotations.NotNull; - import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateBoardBinding; @@ -26,9 +24,9 @@ public class BoardAdapter extends AbstractAdapter<Board> { return item.getLocalId(); } - @NotNull + @NonNull @Override - public View getView(int position, View convertView, @NotNull ViewGroup parent) { + public View getView(int position, View convertView, @NonNull ViewGroup parent) { final ItemPrepareCreateBoardBinding binding; if (convertView == null) { binding = ItemPrepareCreateBoardBinding.inflate(inflater, parent, false); @@ -39,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, "#" + 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/preparecreate/PrepareCreateActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java index ab0cc4816..18317e078 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java @@ -2,214 +2,52 @@ package it.niedermann.nextcloud.deck.ui.preparecreate; import android.content.ClipData; import android.content.Intent; -import android.content.res.ColorStateList; -import android.graphics.Color; import android.os.Bundle; import android.text.TextUtils; -import android.widget.ArrayAdapter; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.Observer; +import androidx.appcompat.app.ActionBar; -import java.util.List; - -import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.databinding.ActivityPrepareCreateBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.model.Board; -import it.niedermann.nextcloud.deck.model.full.FullStack; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; -import it.niedermann.nextcloud.deck.ui.ImportAccountActivity; -import it.niedermann.nextcloud.deck.ui.branding.Branded; +import it.niedermann.nextcloud.deck.ui.PickStackActivity; import it.niedermann.nextcloud.deck.ui.card.EditActivity; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; -import it.niedermann.nextcloud.deck.util.ColorUtil; -import static android.graphics.Color.parseColor; -import static androidx.lifecycle.Transformations.switchMap; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentBoardId; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentStackId; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardId; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentStackId; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas; - -public class PrepareCreateActivity extends AppCompatActivity implements Branded { - - private ActivityPrepareCreateBinding binding; - - private SyncManager syncManager; - - private boolean brandingEnabled; - - private long lastAccountId; - private long lastBoardId; - private long lastStackId; - private ArrayAdapter<Account> accountAdapter; - private ArrayAdapter<Board> boardAdapter; - private ArrayAdapter<FullStack> stackAdapter; - - @Nullable - private LiveData<List<Board>> boardsLiveData; - @NonNull - private Observer<List<Board>> boardsObserver = (boards) -> { - boardAdapter.clear(); - boardAdapter.addAll(boards); - binding.boardSelect.setEnabled(true); - - if (boards.size() > 0) { - binding.boardSelect.setEnabled(true); - - for (Board board : boards) { - if (board.getLocalId() == lastBoardId) { - binding.boardSelect.setSelection(boardAdapter.getPosition(board)); - applyBrand(Color.parseColor('#' + board.getColor())); - break; - } - } - } else { - applyBrand(ContextCompat.getColor(this, R.color.defaultBrand)); - binding.boardSelect.setEnabled(false); - binding.submit.setEnabled(false); - } - }; - - @Nullable - private LiveData<List<FullStack>> stacksLiveData; - @NonNull - private Observer<List<FullStack>> stacksObserver = (fullStacks) -> { - stackAdapter.clear(); - stackAdapter.addAll(fullStacks); - - if (fullStacks.size() > 0) { - binding.stackSelect.setEnabled(true); - binding.submit.setEnabled(true); - - for (FullStack fullStack : fullStacks) { - if (fullStack.getLocalId() == lastStackId) { - binding.stackSelect.setSelection(stackAdapter.getPosition(fullStack)); - break; - } - } - } else { - binding.stackSelect.setEnabled(false); - binding.submit.setEnabled(false); - } - }; +public class PrepareCreateActivity extends PickStackActivity { @Override - protected void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); - - brandingEnabled = isBrandingEnabled(this); - - binding = ActivityPrepareCreateBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - - accountAdapter = new AccountAdapter(this); - binding.accountSelect.setAdapter(accountAdapter); - binding.accountSelect.setEnabled(false); - boardAdapter = new BoardAdapter(this); - binding.boardSelect.setAdapter(boardAdapter); - binding.stackSelect.setEnabled(false); - stackAdapter = new StackAdapter(this); - binding.stackSelect.setAdapter(stackAdapter); - binding.stackSelect.setEnabled(false); - - syncManager = new SyncManager(this); - - switchMap(syncManager.hasAccounts(), hasAccounts -> { - if (hasAccounts) { - return syncManager.readAccounts(); - } else { - startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); - return null; - } - }).observe(this, (List<Account> accounts) -> { - if (accounts == null || accounts.size() == 0) { - throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry"); - } - - lastAccountId = readCurrentAccountId(this); - lastBoardId = readCurrentBoardId(this, lastAccountId); - lastStackId = readCurrentStackId(this, lastAccountId, lastBoardId); - - accountAdapter.clear(); - accountAdapter.addAll(accounts); - binding.accountSelect.setEnabled(true); - - for (Account account : accounts) { - if (account.getId() == lastAccountId) { - binding.accountSelect.setSelection(accountAdapter.getPosition(account)); - break; - } - } - }); - - binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { - updateLiveDataSource(boardsLiveData, boardsObserver, syncManager.getBoardsWithEditPermission(parent.getSelectedItemId())); - }); - - binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { - applyBrand(Color.parseColor('#' + ((Board) binding.boardSelect.getSelectedItem()).getColor())); - updateLiveDataSource(stacksLiveData, stacksObserver, syncManager.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId())); - }); - - binding.cancel.setOnClickListener((v) -> finish()); - binding.submit.setOnClickListener((v) -> onSubmit()); + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.add_card); + } } - /** - * Updates the source of the given liveData and de- and reregisters the given observer. - */ - private <T> void updateLiveDataSource(@Nullable LiveData<T> liveData, Observer<T> observer, LiveData<T> newSource) { - if (liveData != null) { - liveData.removeObserver(observer); + @Override + protected void onSubmit(Account account, long boardId, long stackId) { + final String receivedClipData = getReceivedClipData(getIntent()); + if (receivedClipData == null) { + startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId)); + } else { + startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData)); } - liveData = newSource; - liveData.observe(PrepareCreateActivity.this, observer); - } - /** - * Starts EditActivity and passes parameters. - */ - private void onSubmit() { - final Account account = accountAdapter.getItem(binding.accountSelect.getSelectedItemPosition()); - if (account != null) { - final long boardId = binding.boardSelect.getSelectedItemId(); - final long stackId = binding.stackSelect.getSelectedItemId(); - final String receivedClipData = getReceivedClipData(getIntent()); - if (receivedClipData == null) { - startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId)); - } else { - startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData)); - } + saveCurrentAccountId(this, account.getId()); + saveCurrentBoardId(this, account.getId(), boardId); + saveCurrentStackId(this, account.getId(), boardId, stackId); + applyBrand(account.getColor()); - saveCurrentAccountId(this, account.getId()); - saveCurrentBoardId(this, account.getId(), boardId); - saveCurrentStackId(this, account.getId(), boardId, stackId); - applyBrand(parseColor(account.getColor())); + finish(); + } - finish(); - } else { - ExceptionDialogFragment.newInstance(new IllegalStateException("Selected account at position " + binding.accountSelect.getSelectedItemPosition() + " is null."), null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } + @Override + protected boolean showBoardsWithoutEditPermission() { + return false; } @Nullable @@ -232,20 +70,4 @@ public class PrepareCreateActivity extends AppCompatActivity implements Branded final CharSequence text = item.getText(); return TextUtils.isEmpty(text) ? null : text.toString(); } - - @Override - public void applyBrand(int mainColor) { - try { - if (brandingEnabled) { - @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(this, R.color.primary)) - ? mainColor - : isDarkTheme(this) ? Color.WHITE : Color.BLACK; - DrawableCompat.setTintList(binding.submit.getBackground(), ColorStateList.valueOf(finalMainColor)); - binding.submit.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor)); - binding.cancel.setTextColor(getSecondaryForegroundColorDependingOnTheme(this, mainColor)); - } - } catch (Throwable t) { - DeckLog.logError(t); - } - } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java index 89c702075..e3e0ad5b5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java @@ -6,14 +6,12 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; -import org.jetbrains.annotations.NotNull; - import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding; -import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.Stack; -public class StackAdapter extends AbstractAdapter<FullStack> { +public class StackAdapter extends AbstractAdapter<Stack> { @SuppressWarnings("WeakerAccess") public StackAdapter(@NonNull Context context) { @@ -21,13 +19,13 @@ public class StackAdapter extends AbstractAdapter<FullStack> { } @Override - protected long getItemId(@NonNull FullStack item) { + protected long getItemId(@NonNull Stack item) { return item.getLocalId(); } - @NotNull + @NonNull @Override - public View getView(int position, View convertView, @NotNull ViewGroup parent) { + public View getView(int position, View convertView, @NonNull ViewGroup parent) { final ItemPrepareCreateStackBinding binding; if (convertView == null) { binding = ItemPrepareCreateStackBinding.inflate(inflater, parent, false); @@ -35,9 +33,9 @@ public class StackAdapter extends AbstractAdapter<FullStack> { binding = ItemPrepareCreateStackBinding.bind(convertView); } - final FullStack item = getItem(position); + final Stack item = getItem(position); if (item != null) { - binding.stackTitle.setText(item.getStack().getTitle()); + binding.stackTitle.setText(item.getTitle()); } else { DeckLog.logError(new IllegalArgumentException("No item for position " + position)); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java index ea8d90f22..04429f225 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java @@ -23,6 +23,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande private BrandedSwitchPreference wifiOnlyPref; private BrandedSwitchPreference themePref; private BrandedSwitchPreference brandingPref; + private BrandedSwitchPreference compactPref; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -67,6 +68,8 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande DeckLog.error("Could not find preference with key: \"" + getString(R.string.pref_key_dark_theme) + "\""); } + compactPref = findPreference(getString(R.string.pref_key_compact)); + final ListPreference backgroundSyncPref = findPreference(getString(R.string.pref_key_background_sync)); if (backgroundSyncPref != null) { backgroundSyncPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { @@ -92,5 +95,6 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande wifiOnlyPref.applyBrand(mainColor); themePref.applyBrand(mainColor); brandingPref.applyBrand(mainColor); + compactPref.applyBrand(mainColor); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java index 271b60489..3028ea952 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java @@ -14,15 +14,16 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.ViewModelProvider; +import it.niedermann.nextcloud.deck.BuildConfig; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogShareProgressBinding; import it.niedermann.nextcloud.deck.exceptions.UploadAttachmentFailedException; import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; +import it.niedermann.nextcloud.exception.ExceptionUtil; import static android.graphics.PorterDuff.Mode; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.nextcloud.deck.util.ExceptionUtil.getDebugInfos; public class ShareProgressDialogFragment extends BrandedDialogFragment { @@ -70,7 +71,7 @@ public class ShareProgressDialogFragment extends BrandedDialogFragment { binding.errorReportButton.setOnClickListener((v) -> { final StringBuilder debugInfos = new StringBuilder(exceptionsCount + " attachments failed to upload:"); for (Throwable t : exceptions) { - debugInfos.append(getDebugInfos(requireContext(), t, null)); + debugInfos.append(ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), t, BuildConfig.FLAVOR)); } ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException(debugInfos.toString()), null) .show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java index a64629bd7..3a0005fb1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java @@ -16,9 +16,9 @@ import androidx.lifecycle.ViewModelProvider; 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.List; import it.niedermann.nextcloud.deck.DeckLog; @@ -126,7 +126,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe throw new IllegalArgumentException("MimeType of uri is null. [" + uri + "]"); } runOnUiThread(() -> { - final WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(fullCard.getAccountId(), fullCard.getCard().getLocalId(), mimeType, tempFile); + final WrappedLiveData<Attachment> liveData = mainViewModel.addAttachmentToCard(fullCard.getAccountId(), fullCard.getCard().getLocalId(), mimeType, tempFile); liveData.observe(ShareTargetActivity.this, (next) -> { if (liveData.hasError()) { if (liveData.getError() instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) liveData.getError()).getStatusCode() == HTTP_CONFLICT) { @@ -160,7 +160,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe ? receivedText : oldDescription + "\n\n" + receivedText ); - WrappedLiveData<FullCard> liveData = syncManager.updateCard(fullCard); + WrappedLiveData<FullCard> liveData = mainViewModel.updateCard(fullCard); observeOnce(liveData, this, (next) -> { if (liveData.hasError()) { cardSelected = false; @@ -173,8 +173,8 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe break; case 1: final Account currentAccount = mainViewModel.getCurrentAccount(); - final DeckComment comment = new DeckComment(receivedText.trim(), currentAccount.getUserName(), new Date()); - syncManager.addCommentToCard(currentAccount.getId(), fullCard.getLocalId(), comment); + final DeckComment comment = new DeckComment(receivedText.trim(), currentAccount.getUserName(), Instant.now()); + mainViewModel.addCommentToCard(currentAccount.getId(), fullCard.getLocalId(), comment); Toast.makeText(getApplicationContext(), getString(R.string.share_success, "\"" + receivedText + "\"", "\"" + fullCard.getCard().getTitle() + "\""), Toast.LENGTH_LONG).show(); finish(); break; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java index e2cc83372..8fc56759f 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java @@ -8,11 +8,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter; import java.util.ArrayList; import java.util.List; -import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.Stack; public class StackAdapter extends FragmentStateAdapter { @NonNull - private List<FullStack> stackList = new ArrayList<>(); + private final List<Stack> stackList = new ArrayList<>(); public StackAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); @@ -23,7 +23,7 @@ public class StackAdapter extends FragmentStateAdapter { return stackList.size(); } - public FullStack getItem(int position) { + public Stack getItem(int position) { return stackList.get(position); } @@ -34,7 +34,7 @@ public class StackAdapter extends FragmentStateAdapter { @Override public boolean containsItem(long itemId) { - for (FullStack stack : stackList) { + for (Stack stack : stackList) { if (stack.getLocalId() == itemId) { return true; } @@ -48,9 +48,9 @@ public class StackAdapter extends FragmentStateAdapter { return StackFragment.newInstance(stackList.get(position).getLocalId()); } - public void setStacks(@NonNull List<FullStack> fullStacks) { + public void setStacks(@NonNull List<Stack> stacks) { this.stackList.clear(); - this.stackList.addAll(fullStacks); + this.stackList.addAll(stacks); notifyDataSetChanged(); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java index 726a74184..313e6e821 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java @@ -20,20 +20,27 @@ import java.util.List; import it.niedermann.android.crosstabdnd.DragAndDropTab; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.databinding.FragmentStackBinding; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; import it.niedermann.nextcloud.deck.ui.card.CardAdapter; import it.niedermann.nextcloud.deck.ui.card.SelectCardListener; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel; +import it.niedermann.nextcloud.deck.ui.movecard.MoveCardListener; -public class StackFragment extends BrandedFragment implements DragAndDropTab<CardAdapter> { +import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; + +public class StackFragment extends BrandedFragment implements DragAndDropTab<CardAdapter>, MoveCardListener { private static final String KEY_STACK_ID = "stackId"; private FragmentStackBinding binding; - private SyncManager syncManager; + private MainViewModel mainViewModel; private FragmentActivity activity; private OnScrollListener onScrollListener; @@ -61,10 +68,10 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentStackBinding.inflate(inflater, container, false); activity = requireActivity(); + binding = FragmentStackBinding.inflate(inflater, container, false); + mainViewModel = new ViewModelProvider(activity).get(MainViewModel.class); - final MainViewModel mainViewModel = new ViewModelProvider(activity).get(MainViewModel.class); final FilterViewModel filterViewModel = new ViewModelProvider(activity).get(FilterViewModel.class); // This might be a zombie fragment with an empty MainViewModel after Android killed the activity (but not the fragment instance @@ -74,19 +81,10 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car return binding.getRoot(); } - syncManager = new SyncManager(activity); - - adapter = new CardAdapter( - requireContext(), - getChildFragmentManager(), - mainViewModel.getCurrentAccount(), - mainViewModel.getCurrentBoardLocalId(), - mainViewModel.getCurrentBoardRemoteId(), - stackId, - mainViewModel.currentBoardHasEditPermission(), - syncManager, - this, - (requireActivity() instanceof SelectCardListener) ? (SelectCardListener) requireActivity() : null); + adapter = new CardAdapter(requireContext(), getChildFragmentManager(), stackId, mainViewModel, this, + (requireActivity() instanceof SelectCardListener) + ? (SelectCardListener) requireActivity() + : null); binding.recyclerView.setAdapter(adapter); if (onScrollListener != null) { @@ -114,12 +112,12 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car } }); - cardsLiveData = syncManager.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterViewModel.getFilterInformation().getValue()); + cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterViewModel.getFilterInformation().getValue()); cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver); filterViewModel.getFilterInformation().observe(getViewLifecycleOwner(), (filterInformation -> { cardsLiveData.removeObserver(cardsObserver); - cardsLiveData = syncManager.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterInformation); + cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterInformation); cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver); })); @@ -153,4 +151,17 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car return fragment; } + + @Override + public void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) { + final WrappedLiveData<Void> liveData = mainViewModel.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId); + observeOnce(liveData, requireActivity(), (next) -> { + if (liveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(liveData.getError())) { + ExceptionDialogFragment.newInstance(liveData.getError(), null).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } else { + DeckLog.log("Moved " + Card.class.getSimpleName() + " \"" + originCardLocalId + "\" to " + Stack.class.getSimpleName() + " \"" + targetStackLocalId + "\""); + } + }); + } + }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java new file mode 100644 index 000000000..af17464dc --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java @@ -0,0 +1,182 @@ +package it.niedermann.nextcloud.deck.ui.takephoto; + +import android.content.Context; +import android.content.Intent; +import android.content.res.ColorStateList; +import android.net.Uri; +import android.os.Bundle; +import android.util.Size; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.camera.core.Camera; +import androidx.camera.core.ImageCapture; +import androidx.camera.core.ImageCaptureException; +import androidx.camera.core.Preview; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.io.File; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.ExecutionException; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.databinding.ActivityTakePhotoBinding; +import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; + +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.IMAGE_JPEG; + +@RequiresApi(LOLLIPOP) +public class TakePhotoActivity extends BrandedActivity { + + private ActivityTakePhotoBinding binding; + private TakePhotoViewModel viewModel; + + private View[] brandedViews; + + private ListenableFuture<ProcessCameraProvider> cameraProviderFuture; + private OrientationEventListener orientationEventListener; + + private final DateTimeFormatter fileNameFromCameraFormatter = DateTimeFormatter.ofPattern("'JPG_'yyyyMMdd'_'HHmmss'.jpg'"); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + + binding = ActivityTakePhotoBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(TakePhotoViewModel.class); + + setContentView(binding.getRoot()); + + cameraProviderFuture = ProcessCameraProvider.getInstance(this); + cameraProviderFuture.addListener(() -> { + try { + final ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); + final Preview previewUseCase = getPreviewUseCase(); + final ImageCapture captureUseCase = getCaptureUseCase(); + final Camera camera = cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase); + + viewModel.getCameraSelectorToggleButtonImageResource().observe(this, res -> binding.switchCamera.setImageDrawable(ContextCompat.getDrawable(this, res))); + viewModel.getTorchToggleButtonImageResource().observe(this, res -> binding.toggleTorch.setImageDrawable(ContextCompat.getDrawable(this, res))); + viewModel.isTorchEnabled().observe(this, enabled -> camera.getCameraControl().enableTorch(enabled)); + + binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled()); + binding.switchCamera.setOnClickListener((v) -> { + viewModel.toggleCameraSelector(); + cameraProvider.unbindAll(); + cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase); + }); + } catch (ExecutionException | InterruptedException e) { + DeckLog.logError(e); + finish(); + } + }, ContextCompat.getMainExecutor(this)); + + brandedViews = new View[]{binding.takePhoto, binding.switchCamera, binding.toggleTorch}; + } + + private ImageCapture getCaptureUseCase() { + final ImageCapture captureUseCase = new ImageCapture.Builder().setTargetResolution(new Size(720, 1280)).build(); + + orientationEventListener = new OrientationEventListener(this) { + @Override + public void onOrientationChanged(int orientation) { + int rotation; + + // Monitors orientation values to determine the target rotation value + if (orientation >= 45 && orientation < 135) { + rotation = Surface.ROTATION_270; + } else if (orientation >= 135 && orientation < 225) { + rotation = Surface.ROTATION_180; + } else if (orientation >= 225 && orientation < 315) { + rotation = Surface.ROTATION_90; + } else { + rotation = Surface.ROTATION_0; + } + + captureUseCase.setTargetRotation(rotation); + } + }; + orientationEventListener.enable(); + + binding.takePhoto.setOnClickListener((v) -> { + binding.takePhoto.setEnabled(false); + final String photoFileName = Instant.now().atZone(ZoneId.systemDefault()).format(fileNameFromCameraFormatter); + try { + final File photoFile = AttachmentUtil.getTempCacheFile(this, "photos/" + photoFileName); + final ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(photoFile).build(); + captureUseCase.takePicture(options, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() { + @Override + public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) { + final Uri savedUri = Uri.fromFile(photoFile); + DeckLog.info("onImageSaved - savedUri: " + savedUri.toString()); + setResult(RESULT_OK, new Intent().setDataAndType(savedUri, IMAGE_JPEG)); + finish(); + } + + @Override + public void onError(@NonNull ImageCaptureException e) { + e.printStackTrace(); + //noinspection ResultOfMethodCallIgnored + photoFile.delete(); + binding.takePhoto.setEnabled(true); + } + }); + } catch (Exception e) { + ExceptionDialogFragment.newInstance(e, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + + return captureUseCase; + } + + private Preview getPreviewUseCase() { + Preview previewUseCase = new Preview.Builder().build(); + previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider()); + return previewUseCase; + } + + @Override + protected void onPause() { + if (this.orientationEventListener != null) { + this.orientationEventListener.disable(); + } + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + if (this.orientationEventListener != null) { + this.orientationEventListener.enable(); + } + } + + @RequiresApi(LOLLIPOP) + public static Intent createIntent(@NonNull Context context) { + return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + } + + @Override + public void applyBrand(int mainColor) { + final ColorStateList colorStateList = ColorStateList.valueOf(mainColor); + for (View v : brandedViews) { + v.setBackgroundTintList(colorStateList); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java new file mode 100644 index 000000000..a71291ff2 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java @@ -0,0 +1,57 @@ +package it.niedermann.nextcloud.deck.ui.takephoto; + +import androidx.annotation.NonNull; +import androidx.camera.core.CameraSelector; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; +import androidx.lifecycle.ViewModel; + +import it.niedermann.nextcloud.deck.R; + +import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA; +import static androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA; + +public class TakePhotoViewModel extends ViewModel { + + @NonNull + private CameraSelector cameraSelector = DEFAULT_BACK_CAMERA; + @NonNull + private final MutableLiveData<Integer> cameraSelectorToggleButtonImageResource = new MutableLiveData<>(R.drawable.ic_baseline_camera_front_24); + @NonNull + private final MutableLiveData<Boolean> torchEnabled = new MutableLiveData<>(false); + + @NonNull + public CameraSelector getCameraSelector() { + return this.cameraSelector; + } + + public LiveData<Integer> getCameraSelectorToggleButtonImageResource() { + return this.cameraSelectorToggleButtonImageResource; + } + + public void toggleCameraSelector() { + if (this.cameraSelector == DEFAULT_BACK_CAMERA) { + this.cameraSelector = DEFAULT_FRONT_CAMERA; + this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_rear_24); + } else { + this.cameraSelector = DEFAULT_BACK_CAMERA; + this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_front_24); + } + } + + public void toggleTorchEnabled() { + //noinspection ConstantConditions + this.torchEnabled.postValue(!this.torchEnabled.getValue()); + } + + public LiveData<Boolean> isTorchEnabled() { + return this.torchEnabled; + } + + public LiveData<Integer> getTorchToggleButtonImageResource() { + return Transformations.map(isTorchEnabled(), enabled -> enabled + ? R.drawable.ic_baseline_flash_off_24 + : R.drawable.ic_baseline_flash_on_24); + } +} 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 30dc0ada4..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,31 +10,31 @@ 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; import it.niedermann.nextcloud.deck.util.ViewUtil; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; - public class ColorChooser extends LinearLayout { - private WidgetColorChooserBinding binding; - - private final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ); + private final WidgetColorChooserBinding binding; - private Context context; - private String[] colors; + private final Context context; + private final int[] colors; - private String selectedColor; - private String previouslySelectedColor; + @ColorInt + private int selectedColor; + @ColorInt + private int previouslySelectedColor; @Nullable private ImageView previouslySelectedImageView; @@ -41,17 +42,22 @@ public class ColorChooser extends LinearLayout { super(context, attrs); this.context = context; - params.setMargins(0, dpToPx(context, R.dimen.spacer_1x), 0, 0); + final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + 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 @@ -61,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); }); @@ -84,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; @@ -107,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/view/OverlappingAvatars.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java index 501d33106..0facc385c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java @@ -10,6 +10,7 @@ import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.annotation.Px; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import com.bumptech.glide.Glide; @@ -17,12 +18,11 @@ import com.bumptech.glide.request.RequestOptions; import java.util.List; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; - public class OverlappingAvatars extends RelativeLayout { final int maxAvatarCount; @Px @@ -44,11 +44,12 @@ public class OverlappingAvatars extends RelativeLayout { public OverlappingAvatars(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); maxAvatarCount = context.getResources().getInteger(R.integer.max_avatar_count); - avatarBorderSize = dpToPx(context, R.dimen.avatar_size_small_overlapping_border); - avatarSize = dpToPx(context, R.dimen.avatar_size_small) + avatarBorderSize * 2; - overlapPx = dpToPx(context, R.dimen.avatar_size_small_overlapping); - borderDrawable = getResources().getDrawable(R.drawable.avatar_border); - DrawableCompat.setTint(borderDrawable, getResources().getColor(R.color.bg_card)); + avatarBorderSize = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small_overlapping_border); + avatarSize = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small) + avatarBorderSize * 2; + overlapPx = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small_overlapping); + borderDrawable = ContextCompat.getDrawable(context, R.drawable.avatar_border); + assert borderDrawable != null; + DrawableCompat.setTint(borderDrawable, ContextCompat.getColor(context, R.color.bg_card)); } public void setAvatars(@NonNull Account account, @NonNull List<User> assignedUsers) { @@ -70,6 +71,7 @@ public class OverlappingAvatars extends RelativeLayout { avatar.requestLayout(); Glide.with(context) .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(assignedUsers.get(avatarCount).getUid()) + "/" + avatarSize) + .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) .apply(RequestOptions.circleCropTransform()) .into(avatar); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java new file mode 100644 index 000000000..0912a07dd --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java @@ -0,0 +1,35 @@ +package it.niedermann.nextcloud.deck.ui.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; + +import androidx.constraintlayout.widget.ConstraintLayout; + +public class SquareConstraintLayout extends ConstraintLayout { + + public SquareConstraintLayout(Context context) { + super(context); + } + + public SquareConstraintLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Set a square layout. + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } + +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java new file mode 100644 index 000000000..a2a50430c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.ui.view.labelchip; + +import android.annotation.SuppressLint; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; + +import it.niedermann.android.util.DimensionUtil; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Label; + +@SuppressLint("ViewConstructor") +public class CompactLabelChip extends LabelChip { + + public CompactLabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) { + super(context, label, gutter); + params.setFlexBasisPercent(1 / 6.5f); + setHeight(DimensionUtil.INSTANCE.dpToPx(context, R.dimen.compact_label_height)); + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java new file mode 100644 index 000000000..80e44d7e0 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.ui.view.labelchip; + +import android.annotation.SuppressLint; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; + +import it.niedermann.nextcloud.deck.model.Label; + +import static android.text.TextUtils.TruncateAt.MIDDLE; + +@SuppressLint("ViewConstructor") +public class DefaultLabelChip extends LabelChip { + + public DefaultLabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) { + super(context, label, gutter); + setText(label.getTitle()); + setEllipsize(MIDDLE); + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java index db4e123d3..83853f0b2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java @@ -1,9 +1,8 @@ -package it.niedermann.nextcloud.deck.ui.view; +package it.niedermann.nextcloud.deck.ui.view.labelchip; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; -import android.graphics.Color; import android.view.ViewGroup; import androidx.annotation.NonNull; @@ -12,26 +11,24 @@ import androidx.annotation.Px; import com.google.android.flexbox.FlexboxLayout; import com.google.android.material.chip.Chip; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.util.ColorUtil; - -import static android.text.TextUtils.TruncateAt.MIDDLE; @SuppressLint("ViewConstructor") public class LabelChip extends Chip { private final Label label; + protected final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + public LabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) { super(context); this.label = label; - FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ); - params.setMargins(0, 0, gutter, 0); setLayoutParams(params); setEnsureMinTouchTargetSize(false); @@ -42,15 +39,13 @@ public class LabelChip extends Chip { setTextStartPadding(gutter); setTextEndPadding(gutter); setChipEndPadding(gutter); - - setText(label.getTitle()); - setEllipsize(MIDDLE); + setClickable(false); try { - int labelColor = Color.parseColor("#" + label.getColor()); + int labelColor = label.getColor(); ColorStateList c = ColorStateList.valueOf(labelColor); setChipBackgroundColor(c); - setTextColor(ColorUtil.getForegroundColorForBackgroundColor(labelColor)); + setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor)); } catch (IllegalArgumentException e) { DeckLog.logError(e); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java new file mode 100644 index 000000000..1c5e35d97 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java @@ -0,0 +1,22 @@ +package it.niedermann.nextcloud.deck.ui.view.labellayout; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.ui.view.labelchip.CompactLabelChip; +import it.niedermann.nextcloud.deck.ui.view.labelchip.LabelChip; + +public class CompactLabelLayout extends LabelLayout { + + public CompactLabelLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected LabelChip createLabelChip(@NonNull Label label) { + return new CompactLabelChip(getContext(), label, gutter); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java new file mode 100644 index 000000000..f2d6d0752 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.ui.view.labellayout; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.ui.view.labelchip.DefaultLabelChip; + +public class DefaultLabelLayout extends LabelLayout { + + public DefaultLabelLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected DefaultLabelChip createLabelChip(@NonNull Label label) { + return new DefaultLabelChip(getContext(), label, gutter); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java index 814c63ce1..3db539e53 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java @@ -1,4 +1,4 @@ -package it.niedermann.nextcloud.deck.ui.view; +package it.niedermann.nextcloud.deck.ui.view.labellayout; import android.content.Context; import android.util.AttributeSet; @@ -11,21 +11,22 @@ import com.google.android.flexbox.FlexboxLayout; import java.util.LinkedList; import java.util.List; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.ui.view.labelchip.LabelChip; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; - -public class LabelLayout extends FlexboxLayout { +public abstract class LabelLayout extends FlexboxLayout { @Px - private int gutter; - private List<LabelChip> chipList = new LinkedList<>(); + final protected int gutter; + @NonNull + final private List<LabelChip> chipList = new LinkedList<>(); public LabelLayout(Context context, AttributeSet attrs) { super(context, attrs); - this.gutter = dpToPx(context, R.dimen.spacer_1hx); + this.gutter = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1hx); } /** @@ -82,9 +83,11 @@ public class LabelLayout extends FlexboxLayout { continue labelList; } } - LabelChip chip = new LabelChip(getContext(), label, gutter); + final LabelChip chip = createLabelChip(label); addView(chip); chipList.add(chip); } } + + protected abstract LabelChip createLabelChip(@NonNull Label label); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java index 8c1fbe1fa..fe6e969e7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java @@ -45,7 +45,7 @@ public class SelectCardForWidgetActivity extends MainActivity implements SelectC @Override public void onCardSelected(FullCard fullCard) { - syncManager.addOrUpdateSingleCardWidget(appWidgetId, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId()); + mainViewModel.addOrUpdateSingleCardWidget(appWidgetId, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId()); final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, getApplicationContext(), SingleCardWidget.class) .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java index 783f98e00..b44b80d1b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java @@ -43,7 +43,7 @@ public class SingleCardWidget extends AppWidgetProvider { views.setTextViewText(R.id.description, fullModel.getFullCard().getCard().getDescription()); if (fullModel.getFullCard().getCard().getDueDate() != null) { - views.setTextViewText(R.id.card_due_date, DateUtil.getRelativeDateTimeString(context, fullModel.getFullCard().getCard().getDueDate().getTime())); + views.setTextViewText(R.id.card_due_date, DateUtil.getRelativeDateTimeString(context, fullModel.getFullCard().getCard().getDueDate().toEpochMilli())); // TODO Use multiple views for background colors and only set the necessary to View.VISIBLE // https://stackoverflow.com/a/3376537 // Because otherwise using Reflection is the only way @@ -141,13 +141,10 @@ public class SingleCardWidget extends AppWidgetProvider { super.onDeleted(context, appWidgetIds); } - /** * Updates UI data of all {@link SingleCardWidget} instances */ public static void notifyDatasetChanged(Context context) { - Intent intent = new Intent(context, SingleCardWidget.class); - intent.setAction("android.appwidget.action.APPWIDGET_UPDATE"); - context.sendBroadcast(intent); + context.sendBroadcast(new Intent(context, SingleCardWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java new file mode 100644 index 000000000..cb179953c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java @@ -0,0 +1,121 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.RemoteViews; + +import java.util.NoSuchElementException; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.MainActivity; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; + +import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE; + +public class StackWidget extends AppWidgetProvider { + public static final String ACCOUNT_ID_KEY = "stack_widget_account_id"; + public static final String ACCOUNT_KEY = "stack_widget_account"; + public static final String STACK_ID_KEY = "stack_widget_stack_id"; + public static final String BUNDLE_KEY = "stack_widget_bundle"; + private static final int PENDING_INTENT_OPEN_APP_RQ = 0; + private static final int PENDING_INTENT_EDIT_CARD_RQ = 1; + + static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds, Account account) { + final SyncManager syncManager = new SyncManager(context); + + for (int appWidgetId : appWidgetIds) { + new Thread(() -> { + try { + final StackWidgetModel model = syncManager.getStackWidgetModelDirectly(appWidgetId); + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack); + Intent serviceIntent = new Intent(context, StackWidgetService.class); + + serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + serviceIntent.putExtra(ACCOUNT_ID_KEY + appWidgetId, model.getAccountId()); + serviceIntent.putExtra(STACK_ID_KEY + appWidgetId, model.getStackId()); + if (account != null) { + Bundle extras = new Bundle(); + extras.putSerializable(StackWidget.ACCOUNT_KEY + appWidgetId, account); + serviceIntent.putExtra(BUNDLE_KEY + appWidgetId, extras); + } + serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); + + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(new ComponentName(context.getPackageName(), MainActivity.class.getName())); + PendingIntent pendingIntent = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_APP_RQ, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(R.id.widget_stack_header_rl, pendingIntent); + + PendingIntent templatePI = PendingIntent.getActivity(context, PENDING_INTENT_EDIT_CARD_RQ, + new Intent(context, EditActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + + views.setPendingIntentTemplate(R.id.stack_widget_lv, templatePI); + views.setRemoteAdapter(R.id.stack_widget_lv, serviceIntent); + views.setEmptyView(R.id.stack_widget_lv, R.id.widget_stack_placeholder_iv); + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.stack_widget_lv); + awm.updateAppWidget(appWidgetId, views); + } catch (NoSuchElementException e) { + // onUpdate has been triggered before the user finished configuring the widget + } + }).start(); + } + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.onUpdate(context, appWidgetManager, appWidgetIds); + updateAppWidget(context, appWidgetManager, appWidgetIds, null); + } + + @Override + public void onReceive(Context context, Intent intent) { + final Account account; + + super.onReceive(context, intent); + + AppWidgetManager awm = AppWidgetManager.getInstance(context); + + if (intent.getAction() != null) { + if (intent.getAction().equals(ACTION_APPWIDGET_UPDATE)) { + if (intent.hasExtra(BUNDLE_KEY)) { + Bundle extras = intent.getBundleExtra(StackWidget.BUNDLE_KEY); + account = (Account) extras.getSerializable(ACCOUNT_KEY); + + if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) { + if (intent.getExtras() != null) { + updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)}, account); + } + } else { + updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, StackWidget.class)), account); + } + } + } + } + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + super.onDeleted(context, appWidgetIds); + final SyncManager syncManager = new SyncManager(context); + + for (int appWidgetId : appWidgetIds) { + syncManager.deleteStackWidgetModel(appWidgetId); + } + } + + /** + * Updates UI data of all {@link StackWidget} instances + */ + public static void notifyDatasetChanged(Context context) { + context.sendBroadcast(new Intent(context, StackWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); + } +} 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 new file mode 100644 index 000000000..96b5cf672 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java @@ -0,0 +1,68 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.ActionBar; +import androidx.lifecycle.ViewModelProvider; + +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; + +public class StackWidgetConfigurationActivity extends PickStackActivity { + private int appWidgetId; + private StackWidgetConfigurationViewModel stackWidgetConfigurationViewModel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + stackWidgetConfigurationViewModel = new ViewModelProvider(this).get(StackWidgetConfigurationViewModel.class); + + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.add_stack_widget); + } + + setResult(RESULT_CANCELED); + final Bundle extras = getIntent().getExtras(); + + if (extras != null) { + appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + } + + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + DeckLog.error("INVALID_APPWIDGET_ID"); + finish(); + } + } + + @Override + protected void onSubmit(Account account, long boardId, long stackId) { + final Bundle extras = new Bundle(); + + stackWidgetConfigurationViewModel.addStackWidget(appWidgetId, account.getId(), stackId, false); + Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, + getApplicationContext(), StackWidget.class); + extras.putSerializable(StackWidget.ACCOUNT_KEY, account); + extras.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + + // The `extras` bundle is added to the intent this way because using putExtras(extras) + // would have the OS attempt to reassemle the data and cause a crash + // when it finds classes that are only known to this application. + updateIntent.putExtra(StackWidget.BUNDLE_KEY, extras); + setResult(RESULT_OK, updateIntent); + getApplicationContext().sendBroadcast(updateIntent); + + finish(); + } + + @Override + protected boolean showBoardsWithoutEditPermission() { + return true; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java new file mode 100644 index 000000000..cc669accf --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java @@ -0,0 +1,23 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; + +@SuppressWarnings("WeakerAccess") +public class StackWidgetConfigurationViewModel extends AndroidViewModel { + + private final SyncManager syncManager; + + public StackWidgetConfigurationViewModel(@NonNull Application application) { + super(application); + this.syncManager = new SyncManager(application); + } + + public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { + syncManager.addStackWidget(appWidgetId, accountId, stackId, darkTheme); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java new file mode 100644 index 000000000..87f357e20 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java @@ -0,0 +1,134 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import androidx.lifecycle.LiveData; + +import java.util.List; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; + +public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory { + private final Context context; + private final int appWidgetId; + private final long accountId; + private final long stackId; + + private Account account; + private FullStack stack; + private List<FullCard> cardList; + + StackWidgetFactory(Context context, Intent intent) { + this.context = context; + appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + accountId = intent.getLongExtra(StackWidget.ACCOUNT_ID_KEY + appWidgetId, -1); + stackId = intent.getLongExtra(StackWidget.STACK_ID_KEY + appWidgetId, -1); + if (intent.hasExtra(StackWidget.BUNDLE_KEY + appWidgetId)) { + account = (Account) intent.getBundleExtra(StackWidget.BUNDLE_KEY + appWidgetId).getSerializable(StackWidget.ACCOUNT_KEY + appWidgetId); + } + } + + @Override + public void onCreate() { + SyncManager syncManager = new SyncManager(context); + + LiveData<FullStack> stackLiveData = syncManager.getStack(accountId, stackId); + stackLiveData.observeForever((FullStack fullStack) -> { + if (fullStack != null) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack); + stack = fullStack; + views.setTextViewText(R.id.widget_stack_title_tv, stack.getStack().getTitle()); + + LiveData<FullBoard> fullBoardLiveData = syncManager.getFullBoardById(accountId, stack.getStack().getBoardId()); + fullBoardLiveData.observeForever((FullBoard fullBoard) -> { + if (fullBoard != null) { + views.setInt(R.id.widget_stack_header_icon, "setColorFilter", fullBoard.getBoard().getColor()); + notifyAppWidgetUpdate(views); + } + }); + + LiveData<List<FullCard>> fullCardData = syncManager.getFullCardsForStack(accountId, stackId, null); + fullCardData.observeForever((List<FullCard> fullCards) -> cardList = fullCards); + notifyAppWidgetUpdate(views); + } + }); + } + + @Override + public void onDataSetChanged() { + + } + + + @Override + public void onDestroy() { + + } + + @Override + public int getCount() { + return stack == null ? 0 : stack.getCards().size(); + } + + @Override + public RemoteViews getViewAt(int i) { + RemoteViews widget_entry; + + if (cardList == null || i > (cardList.size() - 1) || cardList.get(i) == null) { + DeckLog.error("Card not found at position " + i); + return null; + } + + FullCard card = cardList.get(i); + + widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_stack_entry); + widget_entry.setTextViewText(R.id.widget_entry_content_tv, card.card.getTitle()); + + final Intent intent = EditActivity.createEditCardIntent(context, account, stack.getStack().getBoardId(), card.getCard().getLocalId()); + intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); + widget_entry.setOnClickFillInIntent(R.id.widget_stack_entry, intent); + + return widget_entry; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public boolean hasStableIds() { + return true; + } + + private void notifyAppWidgetUpdate(RemoteViews views) { + AppWidgetManager awm = AppWidgetManager.getInstance(context); + int[] appWidgetIds = awm.getAppWidgetIds(new ComponentName(context, StackWidget.class)); + awm.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stack_widget_lv); + awm.updateAppWidget(appWidgetId, views); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java new file mode 100644 index 000000000..9299a96e2 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java @@ -0,0 +1,11 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class StackWidgetService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new StackWidgetFactory(this.getApplicationContext(), intent); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java index 6b2d6925f..ef3e14b37 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java @@ -1,9 +1,16 @@ package it.niedermann.nextcloud.deck.util; import android.content.Context; +import android.content.Intent; import android.net.Uri; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; +import android.widget.Toast; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; import java.io.File; import java.io.FileNotFoundException; @@ -12,6 +19,9 @@ import java.io.IOException; import java.io.InputStream; import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.model.ocs.Version; /** * Created by stefan on 07.03.20. @@ -22,38 +32,121 @@ public class AttachmentUtil { private AttachmentUtil() { } - public static String getRemoteUrl(String accountUrl, long cardRemoteId, long attachmentRemoteId) { + /** + * @return a link to the thumbnail of the given {@link Attachment}. + * If a thumbnail is not available (see {@link Version#supportsFileAttachments()}), a link to + * the {@link Attachment} itself will be returned instead. + */ + public static String getThumbnailUrl(@NonNull Version version, @NonNull String accountUrl, @NonNull Long cardRemoteId, @NonNull Attachment attachment, @Px int previewSize) { + return version.supportsFileAttachments() && !TextUtils.isEmpty(String.valueOf(attachment.getFileId())) + ? accountUrl + "/index.php/core/preview?fileId=" + attachment.getFileId() + "&x=" + previewSize + "&y=" + previewSize + : getRemoteOrLocalUrl(accountUrl, cardRemoteId, attachment); + } + + /** + * @return {@link AttachmentUtil#getRemoteUrl} or {@link Attachment#getLocalPath()} as fallback + * in case this {@param attachment} has not yet been synced. + */ + @Nullable + public static String getRemoteOrLocalUrl(@NonNull String accountUrl, @Nullable Long cardRemoteId, @NonNull Attachment attachment) { + return (attachment.getId() == null || cardRemoteId == null) + ? attachment.getLocalPath() + : getRemoteUrl(accountUrl, cardRemoteId, attachment.getId()); + } + + /** + * Tries to open the given {@link Attachment} in web browser. Displays a toast on failure. + */ + public static void openAttachmentInBrowser(@NonNull Context context, @NonNull String accountUrl, Long cardRemoteId, Long attachmentRemoteId) { + if (cardRemoteId == null) { + Toast.makeText(context, R.string.card_does_not_yet_exist, Toast.LENGTH_LONG).show(); + DeckLog.logError(new IllegalArgumentException("cardRemoteId must not be null.")); + return; + } + if (attachmentRemoteId == null) { + Toast.makeText(context, R.string.attachment_does_not_yet_exist, Toast.LENGTH_LONG).show(); + DeckLog.logError(new IllegalArgumentException("attachmentRemoteId must not be null.")); + return; + } + context.startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(AttachmentUtil.getRemoteUrl(accountUrl, cardRemoteId, attachmentRemoteId)))); + } + + private static String getRemoteUrl(@NonNull String accountUrl, @NonNull Long cardRemoteId, @NonNull Long attachmentRemoteId) { return accountUrl + "/index.php/apps/deck/cards/" + cardRemoteId + "/attachment/" + attachmentRemoteId; } - public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localId) throws IOException, IllegalArgumentException { - String fullTempPath = context.getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + accountId + "/card-" + (localId == null ? "pending-creation" : localId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context); - DeckLog.verbose("----- fullTempPath: " + fullTempPath); - InputStream inputStream = context.getContentResolver().openInputStream(currentUri); + public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localCardId) throws IOException, IllegalArgumentException { + final InputStream inputStream = context.getContentResolver().openInputStream(currentUri); if (inputStream == null) { throw new IOException("Could not open input stream for " + currentUri.getPath()); } - File cacheFile = new File(fullTempPath); - File tempDir = cacheFile.getParentFile(); + final File cacheFile = getTempCacheFile(context, "attachments/account-" + accountId + "/card-" + (localCardId == null ? "pending-creation" : localCardId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context)); + final FileOutputStream outputStream = new FileOutputStream(cacheFile); + byte[] buffer = new byte[4096]; + + int count; + while ((count = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + } + DeckLog.verbose("----- wrote"); + return cacheFile; + } + + /** + * Creates a new {@link File} + */ + public static File getTempCacheFile(@NonNull Context context, String fileName) throws IOException { + File cacheFile = new File(context.getApplicationContext().getFilesDir().getAbsolutePath() + "/" + fileName); + + DeckLog.verbose("- Full path for new cache file: " + cacheFile.getAbsolutePath()); + + final File tempDir = cacheFile.getParentFile(); if (tempDir == null) { - throw new FileNotFoundException("could not cacheFile.getPranetFile()"); + throw new FileNotFoundException("could not cacheFile.getParentFile()"); } if (!tempDir.exists()) { - if (!tempDir.mkdirs()) { + DeckLog.verbose("-- The folder in which the new file should be created does not exist yet. Trying to create it..."); + if (tempDir.mkdirs()) { + DeckLog.verbose("--- Creation successful"); + } else { throw new IOException("Directory for temporary file does not exist and could not be created."); } } - if (!cacheFile.createNewFile()) { + + DeckLog.verbose("- Try to create actual cache file"); + if (cacheFile.createNewFile()) { + DeckLog.verbose("-- Successfully created cache file"); + } else { throw new IOException("Failed to create cacheFile"); } - FileOutputStream outputStream = new FileOutputStream(fullTempPath); - byte[] buffer = new byte[4096]; - int count; - while ((count = inputStream.read(buffer)) > 0) { - outputStream.write(buffer, 0, count); - } - DeckLog.verbose("----- wrote"); return cacheFile; } + + @DrawableRes + public static int getIconForMimeType(@NonNull String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + return R.drawable.ic_attach_file_grey600_24dp; + } else if (MimeTypeUtil.isAudio(mimeType)) { + return R.drawable.ic_music_note_grey600_24dp; + } else if (MimeTypeUtil.isVideo(mimeType)) { + return R.drawable.ic_local_movies_grey600_24dp; + } else if (MimeTypeUtil.isPdf(mimeType)) { + return R.drawable.ic_baseline_picture_as_pdf_24; + } else if (MimeTypeUtil.isContact(mimeType)) { + return R.drawable.ic_baseline_contact_mail_24; + } else { + return R.drawable.ic_attach_file_grey600_24dp; + } + } + + public static String getMimeType(@Nullable String url) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java deleted file mode 100644 index 1dad224e9..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java +++ /dev/null @@ -1,38 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.R; - -import static android.content.Context.CLIPBOARD_SERVICE; - -public class ClipboardUtil { - - private ClipboardUtil() { - } - - public static boolean copyToClipboard(@NonNull Context context, @Nullable String text) { - return copyToClipboard(context, text, text); - } - - public static boolean copyToClipboard(@NonNull Context context, @Nullable String label, @Nullable String text) { - final ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); - if (clipboardManager == null) { - DeckLog.error("ClipboardManager is null"); - Toast.makeText(context, R.string.could_not_copy_to_clipboard, Toast.LENGTH_LONG).show(); - return false; - } - final ClipData clipData = ClipData.newPlainText(label, text); - clipboardManager.setPrimaryClip(clipData); - DeckLog.info("Copied to clipboard: [" + label + "] \"" + text + "\""); - Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - return true; - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java deleted file mode 100644 index 08354c5fb..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java +++ /dev/null @@ -1,165 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.graphics.Color; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; - -import java.util.HashMap; -import java.util.Map; - -public final class ColorUtil { - - private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>(); - private static final Map<Integer, Integer> FOREGROUND_CACHE = new HashMap<>(); - private static final Map<Integer, Boolean> IS_DARK_COLOR_CACHE = new HashMap<>(); - - private ColorUtil() { - } - - @ColorInt - public static int getForegroundColorForBackgroundColor(@ColorInt int color) { - Integer ret = FOREGROUND_CACHE.get(color); - if (ret == null) { - if (Color.TRANSPARENT == color) - ret = Color.BLACK; - else if (isColorDark(color)) - ret = Color.WHITE; - else - ret = Color.BLACK; - - FOREGROUND_CACHE.put(color, ret); - } - return ret; - } - - /** - * @return well formatted string starting with a hash followed by 6 hex numbers that is parsable by {@link Color#parseColor(String)}. - */ - public static String formatColorToParsableHexString(String input) { - if (input == null) { - throw new IllegalArgumentException("input color string is null"); - } - if (isParsableValidHexColorString(input)) { - return input; - } - final char[] chars = input.replaceAll("#", "").toCharArray(); - final StringBuilder sb = new StringBuilder(7).append("#"); - if (chars.length == 6) { - sb.append(chars); - } else if (chars.length == 3) { - for (char c : chars) { - sb.append(c).append(c); - } - } else { - throw new IllegalArgumentException("unparsable color string: \"" + input + "\""); - } - final String formattedHexColor = sb.toString(); - if (isParsableValidHexColorString(formattedHexColor)) { - return formattedHexColor; - } else { - throw new IllegalArgumentException("\"" + input + "\" is not a valid color string. Result of tried normalizing: " + formattedHexColor); - } - } - - /** - * Checking for {@link Color#parseColor(String)} being able to parse the input is the important part because we don't know the implementation and rely on it to be able to parse the color. - * - * @return true, if the input starts with a hash followed by 6 characters of hex numbers and is parsable by {@link Color#parseColor(String)}. - */ - private static boolean isParsableValidHexColorString(@NonNull String input) { - try { - Color.parseColor(input); - return input.matches("#[a-fA-F0-9]{6}"); - } catch (Exception e) { - return false; - } - } - - public static boolean isColorDark(@ColorInt int color) { - Boolean ret = IS_DARK_COLOR_CACHE.get(color); - if (ret == null) { - ret = getBrightness(color) < 200; - IS_DARK_COLOR_CACHE.put(color, ret); - } - return ret; - } - - private static int getBrightness(@ColorInt int color) { - final int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)}; - - return (int) Math.sqrt(rgb[0] * rgb[0] * .241 + rgb[1] - * rgb[1] * .691 + rgb[2] * rgb[2] * .068); - } - - // --------------------------------------------------- - // Based on https://github.com/LeaVerou/contrast-ratio - // --------------------------------------------------- - - public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) { - ColorPair key = new ColorPair(colorOne, colorTwo); - Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key); - if (ret == null) { - ret = getContrastRatio(colorOne, colorTwo) > 3d; - CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret); - return ret; - } - return ret; - } - - public static boolean contrastRatioIsSufficientBigAreas(@ColorInt int colorOne, @ColorInt int colorTwo) { - ColorPair key = new ColorPair(colorOne, colorTwo); - Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key); - if (ret == null) { - ret = getContrastRatio(colorOne, colorTwo) > 1.47d; - CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret); - return ret; - } - return ret; - } - - public static double getContrastRatio(@ColorInt int colorOne, @ColorInt int colorTwo) { - final double lum1 = getLuminanace(colorOne); - final double lum2 = getLuminanace(colorTwo); - final double brightest = Math.max(lum1, lum2); - final double darkest = Math.min(lum1, lum2); - return (brightest + 0.05) / (darkest + 0.05); - } - - private static double getLuminanace(@ColorInt int color) { - final int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)}; - return getSubcolorLuminance(rgb[0]) * 0.2126 + getSubcolorLuminance(rgb[1]) * 0.7152 + getSubcolorLuminance(rgb[2]) * 0.0722; - } - - private static double getSubcolorLuminance(@ColorInt int color) { - final double value = color / 255d; - return value <= 0.03928 - ? value / 12.92 - : Math.pow((value + 0.055) / 1.055, 2.4); - } - - private static class ColorPair extends Pair<Integer, Integer> { - - private ColorPair(@Nullable Integer first, @Nullable Integer second) { - super(first, second); - } - - @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "NumberEquality"}) - @Override - public boolean equals(Object o) { - final ColorPair colorPair = (ColorPair) o; - if (first != colorPair.first) return false; - return second == colorPair.second; - } - - @SuppressWarnings("ConstantConditions") - @Override - public int hashCode() { - int result = first; - result = 31 * result + second; - return result; - } - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java index 5c2ffd76c..ac88519ff 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java @@ -3,9 +3,9 @@ package it.niedermann.nextcloud.deck.util; import android.content.Context; import android.text.format.DateUtils; -import java.util.Date; -import java.util.TimeZone; -import java.util.concurrent.TimeUnit; +import androidx.annotation.NonNull; + +import java.time.ZonedDateTime; import it.niedermann.nextcloud.deck.R; @@ -15,48 +15,9 @@ public final class DateUtil { private DateUtil() { } - public static Date nowInGMT() { - return convertToGMT(new Date()); - } - - private static Date convertToGMT(Date date ){ - TimeZone tz = TimeZone.getDefault(); - Date ret = new Date( date.getTime() - tz.getRawOffset() ); - - // if we are now in DST, back off by the delta. Note that we are checking the GMT date, this is the KEY. - if ( tz.inDaylightTime( ret )){ - Date dstDate = new Date( ret.getTime() - tz.getDSTSavings() ); - - // check to make sure we have not crossed back into standard time - // this happens when we are on the cusp of DST (7pm the day before the change for PDT) - if ( tz.inDaylightTime( dstDate )){ - ret = dstDate; - } - } - return ret; - } - - /** - * Get difference between 2 dates in days (hours, minutes will be set to zero). - * - * @param sourceDateFrom start date - * @param sourceDateUntil end date - * @return difference between the to dates in days. - */ - public static long getDayDifference(Date sourceDateFrom, Date sourceDateUntil) { - Date dateFrom = new Date(sourceDateFrom.getTime()); - dateFrom.setHours(0); - dateFrom.setMinutes(0); - - Date dateUntil = new Date(sourceDateUntil.getTime()); - dateUntil.setHours(0); - dateUntil.setMinutes(0); - - return TimeUnit.DAYS.convert(dateUntil.getTime() - dateFrom.getTime(), TimeUnit.MILLISECONDS); - } - - public static CharSequence getRelativeDateTimeString(Context context, long time) { - if ((System.currentTimeMillis() - time) < 60 * 1000 && System.currentTimeMillis() > time) { + public static CharSequence getRelativeDateTimeString(@NonNull Context context, long time) { + long now = ZonedDateTime.now().toInstant().toEpochMilli(); + if ((now - time) < 60 * 1000 && now > time) { // < 60 seconds -> seconds ago return context.getString(R.string.seconds_ago); } else { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java new file mode 100644 index 000000000..e01d0ec29 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java @@ -0,0 +1,59 @@ +package it.niedermann.nextcloud.deck.util; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; + +import java.util.HashMap; +import java.util.Map; + +import it.niedermann.android.util.ColorUtil; + +public final class DeckColorUtil { + + private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>(); + + public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) { + ColorPair key = new ColorPair(colorOne, colorTwo); + Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key); + if (ret == null) { + ret = ColorUtil.INSTANCE.getContrastRatio(colorOne, colorTwo) > 3d; + CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret); + return ret; + } + return ret; + } + + public static boolean contrastRatioIsSufficientBigAreas(@ColorInt int colorOne, @ColorInt int colorTwo) { + ColorPair key = new ColorPair(colorOne, colorTwo); + Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key); + if (ret == null) { + ret = ColorUtil.INSTANCE.getContrastRatio(colorOne, colorTwo) > 1.47d; + CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret); + return ret; + } + return ret; + } + + private static class ColorPair extends Pair<Integer, Integer> { + + private ColorPair(@Nullable Integer first, @Nullable Integer second) { + super(first, second); + } + + @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "NumberEquality"}) + @Override + public boolean equals(Object o) { + final ColorPair colorPair = (ColorPair) o; + if (first != colorPair.first) return false; + return second == colorPair.second; + } + + @Override + public int hashCode() { + int result = first; + result = 31 * result + second; + return result; + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java deleted file mode 100644 index 6ff22eace..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java +++ /dev/null @@ -1,17 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.content.Context; - -import androidx.annotation.DimenRes; -import androidx.annotation.NonNull; -import androidx.annotation.Px; - -public final class DimensionUtil { - private DimensionUtil() { - } - - @Px - public static int dpToPx(@NonNull Context context, @DimenRes int resource) { - return context.getResources().getDimensionPixelSize(resource); - } -} 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 7457c0df7..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, "#" + 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,25 +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.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(); }); @@ -79,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); } @@ -87,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/ExceptionUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java deleted file mode 100644 index 8599f0b6d..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java +++ /dev/null @@ -1,81 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; - -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; -import com.nextcloud.android.sso.helper.VersionCheckHelper; -import com.nextcloud.android.sso.ui.UiExceptionManager; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import it.niedermann.nextcloud.deck.BuildConfig; -import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.model.Account; - -public class ExceptionUtil { - - private ExceptionUtil() { - - } - - public static String getDebugInfos(@NonNull Context context, Throwable throwable, @Nullable Account account) { - return "" + - getAppVersions(context, account) + - "\n\n---\n" + - getDeviceInfos() + - "\n\n---" + - "\n\n" + - getStacktraceOf(throwable); - } - - private static String getAppVersions(Context context, @Nullable Account account) { - String versions = "" - + "App Version: " + BuildConfig.VERSION_NAME + "\n" - + "App Version Code: " + BuildConfig.VERSION_CODE + "\n" - + "App Flavor: " + BuildConfig.FLAVOR + "\n"; - - if (account != null) { - versions += "\n"; - versions += "Deck Server Version: " + account.getServerDeckVersion() + "\n"; - } - - versions += "\n"; - try { - versions += "Files App Version Code: " + VersionCheckHelper.getNextcloudFilesVersionCode(context); - } catch (PackageManager.NameNotFoundException e) { - versions += "Files App Version Code: " + e.getMessage(); - e.printStackTrace(); - } - return versions; - } - - private static String getDeviceInfos() { - return "" - + "\nOS Version: " + System.getProperty("os.version") + "(" + Build.VERSION.INCREMENTAL + ")" - + "\nOS API Level: " + Build.VERSION.SDK_INT - + "\nDevice: " + Build.DEVICE - + "\nManufacturer: " + Build.MANUFACTURER - + "\nModel (and Product): " + Build.MODEL + " (" + Build.PRODUCT + ")"; - } - - private static String getStacktraceOf(Throwable e) { - StringWriter sw = new StringWriter(); - e.printStackTrace(new PrintWriter(sw)); - return sw.toString(); - } - - @UiThread - public static void handleNextcloudFilesAppNotInstalledException(@NonNull Context context, @NonNull NextcloudFilesAppNotInstalledException exception) { - UiExceptionManager.showDialogForException(context, exception); - DeckLog.warn("============================================================="); - DeckLog.warn("Nextcloud app is not installed. Cannot choose account"); - exception.printStackTrace(); - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java index 0390bf96d..04694a058 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java @@ -6,6 +6,7 @@ import java.util.Locale; public class MimeTypeUtil { + public static final String IMAGE_JPEG = "image/jpeg"; public static final String TEXT_PLAIN = "text/plain"; public static final String TEXT_VCARD = "text/vcard"; public static final String APPLICATION_PDF = "application/pdf"; 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 new file mode 100644 index 000000000..58c465290 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java @@ -0,0 +1,90 @@ +package it.niedermann.nextcloud.deck.util; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.net.URL; + +import it.niedermann.nextcloud.deck.model.Account; + +public class ProjectUtil { + + private ProjectUtil() { + } + + @NonNull + public static Uri getResourceUri(@NonNull Account account, @NonNull String link) throws IllegalArgumentException { + try { + // Assume link contains a fully qualified Uri including host + final URL u = new URL(link); + return Uri.parse(u.toString()); + } catch (Throwable linkIsNotQualified) { + try { + // Assume link is a absolute path that needs to be concatenated with account url for a complete Uri + final URL u = new URL(account.getUrl() + link); + return Uri.parse(u.toString()); + } catch (Throwable throwable) { + throw new IllegalArgumentException("Could not parse " + Uri.class.getSimpleName() + ": " + link, throwable); + } + } + } + + /** + * extracts the values of board- and card-ID from url. + * 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] + * + * @param url to extract from + * @return extracted and parsed values as long[] with length 1-2 + */ + public static long[] extractBoardIdAndCardIdFromUrl(@Nullable String url) throws IllegalArgumentException { + if (url == null) { + throw new IllegalArgumentException("provided url is null"); + } + url = url.trim(); + if (url.length() == 0) { + throw new IllegalArgumentException("trimmed url is empty"); + } + // extract important part + 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 + "\""); + } + String[] splitBySeparator = splitByPrefix[1].split("/card/"); + + // remove any unexpected stuff + if (splitBySeparator.length > 1 && splitBySeparator[1].contains("/")) { + splitBySeparator[1] = splitBySeparator[1].split("/")[0]; + } + if (splitBySeparator.length > 0 && splitBySeparator[0].contains("/")) { + splitBySeparator[0] = splitBySeparator[0].split("/")[0]; + } + + if (splitBySeparator.length < 1) { + throw new IllegalArgumentException("This URL doesn't seem to be an URL containing the boardId: \"" + url + "\""); + } + + // return result + long boardId = Long.parseLong(splitBySeparator[0]); + if (boardId < 1) { + throw new IllegalArgumentException("Invalid boardId \"" + boardId + "\" for url \"" + url + "\"."); + } + if (splitBySeparator.length == 1) { + return new long[]{boardId}; + } else if (splitBySeparator.length == 2) { + long cardId = Long.parseLong(splitBySeparator[1]); + if (cardId > 0) { + return new long[]{boardId, cardId}; + } else { + return new long[]{boardId}; + } + } else { + throw new IllegalArgumentException("could not parse URL for boardId and/or cardId: \"" + url + "\""); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java index 54f3c8adc..c2ba6f46a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java @@ -21,28 +21,28 @@ import it.niedermann.nextcloud.deck.R; public class SpannableUtil { public static SpannableString strong(@NonNull CharSequence text) { - SpannableString span = new SpannableString(text); + final SpannableString span = new SpannableString(text); span.setSpan(new StyleSpan(Typeface.BOLD), 0, span.length(), 0); return span; } public static SpannableString disabled(@NonNull CharSequence text, @NonNull Context context) { - SpannableString span = new SpannableString(text); + final SpannableString span = new SpannableString(text); span.setSpan(new StyleSpan(Typeface.ITALIC), 0, span.length(), 0); span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.fg_secondary)), 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return span; } public static SpannableString url(@NonNull CharSequence text, @NonNull String target) { - SpannableString span = new SpannableString(text); + final SpannableString span = new SpannableString(text); span.setSpan(new URLSpan(target), 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return span; } public static void setTextWithURL(@NonNull TextView textView, @NonNull Resources resources, @StringRes int containerTextId, @StringRes int linkLabelId, @StringRes int urlId) { - String linkLabel = resources.getString(linkLabelId); - String finalText = resources.getString(containerTextId, linkLabel); - SpannableStringBuilder finalTextBuilder = new SpannableStringBuilder(finalText); + final String linkLabel = resources.getString(linkLabelId); + final String finalText = resources.getString(containerTextId, linkLabel); + final SpannableStringBuilder finalTextBuilder = new SpannableStringBuilder(finalText); finalTextBuilder.setSpan(new URLSpan(resources.getString(urlId)), finalText.indexOf(linkLabel), finalText.indexOf(linkLabel) + linkLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); textView.setText(finalTextBuilder); textView.setMovementMethod(new LinkMovementMethod()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java new file mode 100644 index 000000000..274af332d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java @@ -0,0 +1,42 @@ +package it.niedermann.nextcloud.deck.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Color; +import android.net.Uri; +import android.provider.ContactsContract; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + +import java.util.NoSuchElementException; +import java.util.Objects; + +import it.niedermann.nextcloud.deck.R; + +public class VCardUtil { + + private VCardUtil() { + // You shall not pass + } + + public static Uri getVCardContentUri(@NonNull Context context, @NonNull Uri contactUri) throws NoSuchElementException { + final ContentResolver cr = context.getContentResolver(); + try (final Cursor cursor = cr.query(contactUri, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + final String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)); + return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); + } else { + throw new NoSuchElementException("Cursor has zero entries"); + } + } + } + + @ColorInt + public static int getColorBasedOnDisplayName(@NonNull Context context, @NonNull String displayName) { + final String[] colors = context.getResources().getStringArray(R.array.board_default_colors); + final int hashCode = Objects.hashCode(displayName); + return Color.parseColor(colors[(hashCode < 0 ? hashCode * -1 : hashCode) % colors.length]); + } +} 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 6abbde557..fbc7fc3fc 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,10 +11,13 @@ 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; import androidx.annotation.Nullable; import androidx.annotation.Px; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.widget.TextViewCompat; @@ -24,34 +26,39 @@ import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; -import java.util.Date; +import java.time.LocalDate; import java.util.List; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.LOLLIPOP; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; +import static java.time.temporal.ChronoUnit.DAYS; public final class ViewUtil { private ViewUtil() { } public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @DrawableRes int errorResource) { - addAvatar(avatar, baseUrl, userId, DimensionUtil.dpToPx(avatar.getContext(), R.dimen.avatar_size), errorResource); + addAvatar(avatar, baseUrl, userId, DimensionUtil.INSTANCE.dpToPx(avatar.getContext(), R.dimen.avatar_size), errorResource); } public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @Px int avatarSizeInPx, @DrawableRes int errorResource) { final String uri = baseUrl + "/index.php/avatar/" + Uri.encode(userId) + "/" + avatarSizeInPx; Glide.with(avatar.getContext()) .load(uri) + .placeholder(errorResource) .error(errorResource) .apply(RequestOptions.circleCropTransform()) .into(avatar); } - public static void themeDueDate(Context context, TextView cardDueDate, Date dueDate) { - long diff = DateUtil.getDayDifference(new Date(), dueDate); + public static void themeDueDate(@NonNull Context context, @NonNull TextView cardDueDate, @NonNull LocalDate dueDate) { + long diff = DAYS.between(LocalDate.now(), dueDate); int backgroundDrawable = 0; int textColor = isDarkTheme(context) ? R.color.dark_fg_primary : R.color.grey600; @@ -69,21 +76,18 @@ public final class ViewUtil { } cardDueDate.setBackgroundResource(backgroundDrawable); - cardDueDate.setTextColor(context.getResources().getColor(textColor)); - TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(context.getResources().getColor(textColor))); + cardDueDate.setTextColor(ContextCompat.getColor(context, textColor)); + TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(ContextCompat.getColor(context, textColor))); } - public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @NonNull String color) { - final Drawable drawable = context.getResources().getDrawable(imageId); + 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 * @@ -118,7 +122,7 @@ public final class ViewUtil { Glide.with(context) .asBitmap() .placeholder(R.drawable.ic_person_grey600_24dp) - .load(account.getUrl() + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + DimensionUtil.dpToPx(context, R.dimen.icon_size_details)) + .load(account.getUrl() + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + DimensionUtil.INSTANCE.dpToPx(context, R.dimen.icon_size_details)) .apply(RequestOptions.circleCropTransform()) .into(new CustomTarget<Bitmap>() { @Override @@ -135,4 +139,12 @@ public final class ViewUtil { } textView.setText(messageBuilder); } + + public static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @ColorRes int colorRes) { + if (SDK_INT >= LOLLIPOP) { + imageView.setImageTintList(ColorStateList.valueOf(ContextCompat.getColor(context, colorRes))); + } else { + imageView.setColorFilter(ContextCompat.getColor(context, colorRes)); + } + } } |