diff options
author | Stefan Niedermann <info@niedermann.it> | 2021-03-25 22:01:25 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2021-03-25 22:01:25 +0300 |
commit | 7ae471d95d832d0f6f3460666ae7be76a9d714bc (patch) | |
tree | 656406b1dd1c8eeb7cf420fa87414f73f7c89d10 /app/src/main/java/it/niedermann/nextcloud | |
parent | 24360b8116a95d4a1e9bf49fa39b27a0ebf032bc (diff) | |
parent | dcd3f7140c15634330f7284e096f9de6ef467cd6 (diff) |
Merge branch 'master' into 587-compress-images
# Conflicts:
# app/build.gradle
# app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
# build.gradle
Diffstat (limited to 'app/src/main/java/it/niedermann/nextcloud')
165 files changed, 5399 insertions, 2163 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 cf30a0971..972c098d6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java @@ -2,14 +2,20 @@ package it.niedermann.nextcloud.deck; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Configuration; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.LiveData; import androidx.multidex.MultiDexApplication; import androidx.preference.PreferenceManager; -import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; -import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; +import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData; +import it.niedermann.nextcloud.deck.model.Account; + import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode; +import static androidx.lifecycle.Transformations.distinctUntilChanged; public class DeckApplication extends MultiDexApplication { @@ -17,46 +23,112 @@ public class DeckApplication extends MultiDexApplication { public static final long NO_BOARD_ID = -1L; public static final long NO_STACK_ID = -1L; + private static String PREF_KEY_THEME; + private static String PREF_KEY_DEBUGGING; + + private static LiveData<Integer> currentAccountColor$; + @Override public void onCreate() { - setAppTheme(isDarkTheme(getApplicationContext())); + PREF_KEY_THEME = getString(R.string.pref_key_dark_theme); + PREF_KEY_DEBUGGING = getString(R.string.pref_key_debugging); + setAppTheme(getAppTheme(this)); + DeckLog.enablePersistentLogs(isPersistentLoggingEnabled(this)); + currentAccountColor$ = distinctUntilChanged(new SharedPreferenceIntLiveData(PreferenceManager.getDefaultSharedPreferences(this), + getString(R.string.shared_preference_last_account_color), + ContextCompat.getColor(this, R.color.defaultBrand))); super.onCreate(); } + @Override + public void onLowMemory() { + super.onLowMemory(); + DeckLog.clearDebugLog(); + DeckLog.error("--- cleared log because of low memory ---"); + } + + // --------- + // Debugging + // --------- + + public static boolean isPersistentLoggingEnabled(@NonNull Context context) { + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean enabled = sharedPreferences.getBoolean(PREF_KEY_DEBUGGING, false); + DeckLog.log("--- Read:", PREF_KEY_DEBUGGING, "|", enabled); + return enabled; + } + // ----------------- // Day / Night theme // ----------------- - public static void setAppTheme(boolean darkTheme) { - setDefaultNightMode(darkTheme ? MODE_NIGHT_YES : MODE_NIGHT_NO); + public static void setAppTheme(int setting) { + setDefaultNightMode(setting); } - public static boolean isDarkTheme(@NonNull Context context) { + public static int getAppTheme(@NonNull Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.pref_key_dark_theme), false); + String mode; + try { + mode = prefs.getString(PREF_KEY_THEME, context.getString(R.string.pref_value_theme_system_default)); + } catch (ClassCastException e) { + boolean darkModeEnabled = prefs.getBoolean(PREF_KEY_THEME, false); + mode = darkModeEnabled ? context.getString(R.string.pref_value_theme_dark) : context.getString(R.string.pref_value_theme_light); + } + return Integer.parseInt(mode); + } + + public static boolean isDarkThemeActive(@NonNull Context context, int darkModeSetting) { + if (darkModeSetting == Integer.parseInt(context.getString(R.string.pref_value_theme_system_default))) { + return isDarkThemeActive(context); + } else { + return darkModeSetting == Integer.parseInt(context.getString(R.string.pref_value_theme_dark)); + } + } + + public static boolean isDarkThemeActive(@NonNull Context context) { + int uiMode = context.getResources().getConfiguration().uiMode; + return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } + + public static boolean isDarkTheme(@NonNull Context context) { + return isDarkThemeActive(context, getAppTheme(context)); } // -------------------------------------- // Current account / board / stack states // -------------------------------------- - public static void saveCurrentAccountId(@NonNull Context context, long accountId) { + public static void saveCurrentAccount(@NonNull Context context, @NonNull Account account) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write: shared_preference_last_account" + " | " + accountId); - editor.putLong(context.getString(R.string.shared_preference_last_account), accountId); + DeckLog.log("--- Write: shared_preference_last_account |", account.getId()); + editor.putLong(context.getString(R.string.shared_preference_last_account), account.getId()); + DeckLog.log("--- Write: shared_preference_last_account_color | ", account.getColor()); + editor.putInt(context.getString(R.string.shared_preference_last_account_color), account.getColor()); editor.apply(); } + public static LiveData<Integer> readCurrentAccountColor() { + return currentAccountColor$; + } + + @ColorInt + public static int readCurrentAccountColor(@NonNull Context context) { + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + DeckLog.log("--- Read: shared_preference_last_account_color"); + return sharedPreferences.getInt(context.getString(R.string.shared_preference_last_account_color), context.getApplicationContext().getResources().getColor(R.color.defaultBrand)); + } + public static long readCurrentAccountId(@NonNull Context context) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); long accountId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_account), NO_ACCOUNT_ID); - DeckLog.log("--- Read: shared_preference_last_account" + " | " + accountId); + DeckLog.log("--- Read: shared_preference_last_account |", accountId); return accountId; } public static void saveCurrentBoardId(@NonNull Context context, long accountId, long boardId) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write: shared_preference_last_board_for_account_" + accountId + " | " + boardId); + DeckLog.log("--- Write: shared_preference_last_board_for_account_" + accountId, "|", boardId); editor.putLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, boardId); editor.apply(); } @@ -64,13 +136,13 @@ public class DeckApplication extends MultiDexApplication { public static long readCurrentBoardId(@NonNull Context context, long accountId) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); long boardId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, NO_BOARD_ID); - DeckLog.log("--- Read: shared_preference_last_board_for_account_" + accountId + " | " + boardId); + DeckLog.log("--- Read: shared_preference_last_board_for_account_" + accountId, "|", boardId); return boardId; } public static void saveCurrentStackId(@NonNull Context context, long accountId, long boardId, long stackId) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write: shared_preference_last_stack_for_account_and_board_" + accountId + "_" + boardId + " | " + stackId); + DeckLog.log("--- Write: shared_preference_last_stack_for_account_and_board_" + accountId + "_" + boardId, "|", stackId); editor.putLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, stackId); editor.apply(); } @@ -78,7 +150,7 @@ public class DeckApplication extends MultiDexApplication { public static long readCurrentStackId(@NonNull Context context, long accountId, long boardId) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); long savedStackId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, NO_STACK_ID); - DeckLog.log("--- Read: shared_preference_last_stack_for_account_and_board" + accountId + "_" + boardId + " | " + savedStackId); + DeckLog.log("--- Read: shared_preference_last_stack_for_account_and_board" + accountId + "_" + boardId, "|", savedStackId); return savedStackId; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/DeckLog.java b/app/src/main/java/it/niedermann/nextcloud/deck/DeckLog.java index 47a0385e3..e857f0d56 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/DeckLog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/DeckLog.java @@ -1,55 +1,74 @@ package it.niedermann.nextcloud.deck; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; import android.util.Log; +import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import it.niedermann.nextcloud.deck.util.MimeTypeUtil; + public class DeckLog { + + private static final StringBuffer DEBUG_LOG = new StringBuffer(); + private static boolean PERSIST_LOGS = false; private static final String TAG = DeckLog.class.getSimpleName(); + public static void enablePersistentLogs(boolean persistLogs) { + PERSIST_LOGS = persistLogs; + if (!persistLogs) { + clearDebugLog(); + } + } + public enum Severity { VERBOSE, DEBUG, LOG, INFO, WARN, ERROR } - public static void verbose(String message) { - log(message, Severity.VERBOSE, 4); + public static void verbose(Object... message) { + log(Severity.VERBOSE, 4, message); } - public static void log(String message) { - log(message, Severity.DEBUG, 4); + public static void log(Object... message) { + log(Severity.DEBUG, 4, message); } - public static void info(String message) { - log(message, Severity.INFO, 4); + public static void info(Object... message) { + log(Severity.INFO, 4, message); } - public static void warn(String message) { - log(message, Severity.WARN, 4); + public static void warn(Object... message) { + log(Severity.WARN, 4, message); } - public static void error(String message) { - log(message, Severity.ERROR, 4); + public static void error(Object... message) { + log(Severity.ERROR, 4, message); } - public static void log(String message, Severity severity) { - log(message, severity, 3); + public static void log(@NonNull Severity severity, Object... message) { + log(severity, 3, message); } - private static void log(String message, Severity severity, int stackTracePosition) { - final String print; - if (BuildConfig.DEBUG) { - final StackTraceElement caller = Thread.currentThread().getStackTrace()[stackTracePosition]; - print = caller.getMethodName() + "() (" + caller.getFileName() + ":" + caller.getLineNumber() + ") → " + message; - } else { - print = message; + private static void log(@NonNull Severity severity, int stackTracePosition, Object... messages) { + if (!(PERSIST_LOGS || BuildConfig.DEBUG)) { + return; + } + final StackTraceElement caller = Thread.currentThread().getStackTrace()[stackTracePosition]; + final String print = "(" + caller.getFileName() + ":" + caller.getLineNumber() + ") " + caller.getMethodName() + "() → " + TextUtils.join(" ", messages); + if (PERSIST_LOGS) { + DEBUG_LOG.append(print).append("\n"); } switch (severity) { - case VERBOSE: - Log.v(TAG, print); - break; case DEBUG: Log.d(TAG, print); break; @@ -69,41 +88,46 @@ public class DeckLog { } public static void logError(@Nullable Throwable e) { + if (!(PERSIST_LOGS || BuildConfig.DEBUG)) { + return; + } if (e == null) { error("Could not log error because given error was null"); return; } final StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); - String stacktrace = sw.toString(); // stack trace as a string + final String stacktrace = sw.toString(); final StackTraceElement caller = Thread.currentThread().getStackTrace()[3]; - final String source = caller.getMethodName() + "() (" + caller.getFileName() + ":" + caller.getLineNumber() + ") -> "; - Log.e(TAG, source + stacktrace); + final String print = "(" + caller.getFileName() + ":" + caller.getLineNumber() + ") " + caller.getMethodName() + "() → " + stacktrace; + if (PERSIST_LOGS) { + DEBUG_LOG.append(print).append("\n"); + } + Log.e(TAG, print); } - public static void printCurrentStacktrace() { - log(getCurrentStacktrace(4)); + @NonNull + public static String getDebugLog() { + return DEBUG_LOG.toString(); } - public static String getCurrentStacktrace() { - return getCurrentStacktrace(4); + public static void clearDebugLog() { + DEBUG_LOG.setLength(0); } - private static String getCurrentStacktrace(@SuppressWarnings("SameParameterValue") int offset) { - final StackTraceElement[] elements = Thread.currentThread().getStackTrace(); - final StringBuilder buff = new StringBuilder(); - for (int i = offset; i < elements.length; i++) { - final StackTraceElement s = elements[i]; - buff.append("\tat "); - buff.append(s.getClassName()); - buff.append("."); - buff.append(s.getMethodName()); - buff.append("("); - buff.append(s.getFileName()); - buff.append(":"); - buff.append(s.getLineNumber()); - buff.append(")\n"); - } - return buff.toString(); + /** + * Writes the current log to a temporary file and starts a share intent. + */ + public static void shareLogAsFile(@NonNull Context context) throws IOException { + Toast.makeText(context, R.string.copying_logs_to_file, Toast.LENGTH_LONG).show(); + final File logFile = new File(context.getCacheDir().getAbsolutePath() + "/log.txt"); + final FileWriter writer = new FileWriter(logFile); + writer.write(DeckLog.getDebugLog()); + writer.close(); + context.startActivity(new Intent(Intent.ACTION_SEND) + .putExtra(Intent.EXTRA_TITLE, context.getString(R.string.log_file)) + .putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", logFile)) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setType(MimeTypeUtil.TEXT_PLAIN)); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java index e2cfda22e..a6e8da168 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java @@ -18,19 +18,18 @@ import retrofit2.NextcloudRetrofitApiBuilder; /** * Created by david on 26.05.17. */ - public class ApiProvider { - private static final String DECK_API_ENDPOINT = "/index.php/apps/deck/api/v1.0/"; + private static final String DECK_API_ENDPOINT = "/index.php/apps/deck/api/"; private static final String NC_API_ENDPOINT = "/ocs/v2.php/"; private DeckAPI deckAPI; private NextcloudServerAPI nextcloudAPI; @NonNull - private Context context; - private SingleSignOnAccount ssoAccount; + private final Context context; @Nullable - private String ssoAccountName; + private final String ssoAccountName; + private SingleSignOnAccount ssoAccount; public ApiProvider(Context context) { this(context, null); 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 1f0148b91..9e1b7d7f4 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 @@ -12,6 +12,7 @@ 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.enums.EAttachmentType; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.full.FullStack; @@ -32,129 +33,148 @@ import retrofit2.http.Part; import retrofit2.http.Path; import retrofit2.http.Query; +/** + * @link <a href="https://deck.readthedocs.io/en/latest/API/">Deck REST API</a> + */ public interface DeckAPI { String MODIFIED_SINCE_HEADER = "If-Modified-Since"; String IF_NONE_MATCH = "If-None-Match"; - // ### BOARDS - @POST("boards") + + // Boards + + @POST("v1.0/boards") Observable<FullBoard> createBoard(@Body Board board); - @GET("boards/{id}") + @GET("v1.0/boards/{id}") Observable<FullBoard> getBoard(@Path("id") long id, @Header(MODIFIED_SINCE_HEADER) String lastSync); - @PUT("boards/{id}") + @PUT("v1.0/boards/{id}") Observable<FullBoard> updateBoard(@Path("id") long id, @Body Board board); - @DELETE("boards/{id}") + @DELETE("v1.0/boards/{id}") Observable<Void> deleteBoard(@Path("id") long id); - @DELETE("boards/{id}/undo_delete") + @DELETE("v1.0/boards/{id}/undo_delete") Observable<FullBoard> restoreBoard(@Path("id") long id); - @GET("boards") - Observable<ParsedResponse<List<FullBoard>>> getBoards(@Query ("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync, @Header(IF_NONE_MATCH) String eTag); + @GET("v1.0/boards") + Observable<ParsedResponse<List<FullBoard>>> getBoards(@Query("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync, @Header(IF_NONE_MATCH) String eTag); + + @GET("v1.0/boards") + Observable<ParsedResponse<List<FullBoard>>> getBoards(@Query("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync); - // ### Stacks - @POST("boards/{boardId}/stacks") - Observable<FullStack> createStack(@Path("boardId") long boardId, @Body Stack stack); + // Stacks - @PUT("boards/{boardId}/stacks/{stackId}") + @POST("v1.0/boards/{boardId}/stacks") + Observable<FullStack> createStack(@Path("boardId") long boardId, @Body Stack stack); + + @PUT("v1.0/boards/{boardId}/stacks/{stackId}") Observable<FullStack> updateStack(@Path("boardId") long boardId, @Path("stackId") long id, @Body Stack stack); - @DELETE("boards/{boardId}/stacks/{stackId}") + @DELETE("v1.0/boards/{boardId}/stacks/{stackId}") Observable<Void> deleteStack(@Path("boardId") long boardId, @Path("stackId") long id); - @GET("boards/{boardId}/stacks/{stackId}") + @GET("v1.0/boards/{boardId}/stacks/{stackId}") Observable<FullStack> getStack(@Path("boardId") long boardId, @Path("stackId") long id, @Header(MODIFIED_SINCE_HEADER) String lastSync); - @GET("boards/{boardId}/stacks") + @GET("v1.0/boards/{boardId}/stacks") Observable<List<FullStack>> getStacks(@Path("boardId") long boardId, @Header(MODIFIED_SINCE_HEADER) String lastSync); - @GET("boards/{boardId}/stacks/archived") + @GET("v1.0/boards/{boardId}/stacks/archived") Observable<List<Stack>> getArchivedStacks(@Path("boardId") long boardId, @Header(MODIFIED_SINCE_HEADER) String lastSync); - // ### Cards - @POST("boards/{boardId}/stacks/{stackId}/cards") + // Cards + + @POST("v1.0/boards/{boardId}/stacks/{stackId}/cards") Observable<FullCard> createCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Body Card card); - @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}") + @PUT("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}") Observable<FullCard> updateCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Body CardUpdate card); @FormUrlEncoded - @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel") + @PUT("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel") 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") + @PUT("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/removeLabel") 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") + @PUT("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignUser") Observable<Void> assignUserToCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("userId") String userUID); @FormUrlEncoded - @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/unassignUser") - Observable<Void> unassignUserFromCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("userId") String userUID); + @PUT("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/unassignUser") + Observable<Void> unassignUserFromCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("userId") String userUID); - @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder") + @PUT("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder") Observable<List<FullCard>> moveCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Body Reorder reorder); - @DELETE("boards/{boardId}/stacks/{stackId}/cards/{cardId}") + @DELETE("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}") Observable<Void> deleteCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId); - @GET("boards/{boardId}/stacks/{stackId}/cards/{cardId}") - Observable<FullCard> getCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Header(MODIFIED_SINCE_HEADER) String lastSync); + /** + * @see <a href="https://github.com/nextcloud/deck/issues/2874">This endpoint does only return {@link Attachment}s of type {@link EAttachmentType.DECK_FILE}</a> + */ + @SuppressWarnings("JavadocReference") + @GET("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}") + Observable<FullCard> getCard_1_0(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Header(MODIFIED_SINCE_HEADER) String lastSync); + + @GET("v1.1/boards/{boardId}/stacks/{stackId}/cards/{cardId}") + Observable<FullCard> getCard_1_1(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Header(MODIFIED_SINCE_HEADER) String lastSync); - // ### LABELS - @GET("boards/{boardId}/labels/{labelId}") + // Labels + + @GET("v1.0/boards/{boardId}/labels/{labelId}") Observable<Label> getLabel(@Path("boardId") long boardId, @Path("labelId") long labelId, @Header(MODIFIED_SINCE_HEADER) String lastSync); - @PUT("boards/{boardId}/labels/{labelId}") + @PUT("v1.0/boards/{boardId}/labels/{labelId}") Observable<Label> updateLabel(@Path("boardId") long boardId, @Path("labelId") long labelId, @Body Label label); - @POST("boards/{boardId}/labels") + @POST("v1.0/boards/{boardId}/labels") Observable<Label> createLabel(@Path("boardId") long boardId, @Body Label label); - @DELETE("boards/{boardId}/labels/{labelId}") + @DELETE("v1.0/boards/{boardId}/labels/{labelId}") Observable<Void> deleteLabel(@Path("boardId") long boardId, @Path("labelId") long labelId); - // ### ATTACHMENTS - @GET("boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}") + // Attachments + + @GET("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}") Observable<ResponseBody> downloadAttachment(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Path("attachmentId") long attachmentId); - @GET("boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments") + @GET("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments") Observable<List<Attachment>> getAttachments(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId); @Multipart - @POST("boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments?type=deck_file") - Observable<Attachment> uploadAttachment(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Part MultipartBody.Part type, @Part MultipartBody.Part attachment); + @POST("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments") + Observable<Attachment> uploadAttachment(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Part MultipartBody.Part type, @Part MultipartBody.Part attachment); @Multipart - @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments?type=deck_file") - Observable<Attachment> updateAttachment(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Path("attachmentId") long attachmentId, @Part MultipartBody.Part type, @Part MultipartBody.Part attachment); + @PUT("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments") + Observable<Attachment> updateAttachment(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Path("attachmentId") long attachmentId, @Part MultipartBody.Part type, @Part MultipartBody.Part attachment); - @DELETE("boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}") + @DELETE("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}") Observable<Void> deleteAttachment(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Path("attachmentId") long attachmentId); - @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}/restore") + @PUT("v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId}/restore") Observable<Attachment> restoreAttachment(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Path("attachmentId") long attachmentId); - // ### ACL - @POST("boards/{boardId}/acl") + + // Access control lists + + @POST("v1.0/boards/{boardId}/acl") Observable<AccessControl> createAccessControl(@Path("boardId") long boardId, @Body AccessControl acl); - @PUT("boards/{boardId}/acl/{aclId}") + @PUT("v1.0/boards/{boardId}/acl/{aclId}") Observable<AccessControl> updateAccessControl(@Path("boardId") long boardId, @Path("aclId") long aclId, @Body AccessControl acl); - @DELETE("boards/{boardId}/acl/{aclId}") + @DELETE("v1.0/boards/{boardId}/acl/{aclId}") Observable<Void> deleteAccessControl(@Path("boardId") long boardId, @Path("aclId") long aclId, @Body AccessControl acl); - // ### COMMENTS - } 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 be3eaeb22..3134d45aa 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 @@ -1,44 +1,24 @@ package it.niedermann.nextcloud.deck.api; -import androidx.annotation.CallSuper; +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; import java.util.Collection; import java.util.List; -import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; -public abstract class IResponseCallback<T> { +public abstract class IResponseCallback<T> implements ResponseCallback<T> { + @NonNull protected Account account; - public IResponseCallback(Account account) { + public IResponseCallback(@NonNull Account account) { this.account = account; } - public abstract void onResponse(T response); - - @CallSuper - public void onError(Throwable throwable) { - 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 - } - - }; - } - public void fillAccountIDs(T response) { if (response != null) { if (isListOfRemoteEntity(response)) { @@ -71,7 +51,26 @@ public abstract class IResponseCallback<T> { return false; } + @NonNull public Account getAccount() { return account; } + + /** + * Forwards responses and errors to the given {@param callback} + */ + public static <T> IResponseCallback<T> from(@NonNull Account account, ResponseCallback<T> callback) { + return new IResponseCallback<T>(account) { + @Override + public void onResponse(T response) { + callback.onResponse(response); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + callback.onError(throwable); + } + }; + } }
\ No newline at end of file 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 e759f27d4..2443a919c 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 @@ -23,6 +23,7 @@ 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.ActivityType; +import it.niedermann.nextcloud.deck.model.enums.EAttachmentType; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.full.FullStack; @@ -75,7 +76,7 @@ public class JsonToEntityParser { } private static GroupMemberUIDs parseGroupMemberUIDs(JsonObject obj) { - DeckLog.verbose(obj.toString()); + DeckLog.verbose(obj); GroupMemberUIDs uids = new GroupMemberUIDs(); makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); @@ -93,7 +94,7 @@ public class JsonToEntityParser { } private static OcsUserList parseOcsUserList(JsonObject obj) { - DeckLog.verbose(obj.toString()); + DeckLog.verbose(obj); OcsUserList ocsUserList = new OcsUserList(); makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); @@ -118,7 +119,7 @@ public class JsonToEntityParser { } private static OcsUser parseSingleOcsUser(JsonObject obj) { - DeckLog.verbose(obj.toString()); + DeckLog.verbose(obj); OcsUser ocsUser = new OcsUser(); makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); @@ -137,7 +138,7 @@ public class JsonToEntityParser { } private static OcsProjectList parseOcsProjectList(JsonObject obj) { - DeckLog.verbose(obj.toString()); + DeckLog.verbose(obj); OcsProjectList projectList = new OcsProjectList(); makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); @@ -171,7 +172,7 @@ public class JsonToEntityParser { } private static OcsProjectResource parseOcsProjectResource(JsonObject obj) { - DeckLog.verbose(obj.toString()); + DeckLog.verbose(obj); OcsProjectResource resource = new OcsProjectResource(); makeTraceableIfFails(() -> { if (obj.has("id")) { @@ -213,7 +214,7 @@ public class JsonToEntityParser { } private static OcsComment parseOcsComment(JsonObject obj) { - DeckLog.verbose(obj.toString()); + DeckLog.verbose(obj); OcsComment comment = new OcsComment(); makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); @@ -229,7 +230,7 @@ public class JsonToEntityParser { } private static DeckComment parseDeckComment(JsonElement data) { - DeckLog.verbose(data.toString()); + DeckLog.verbose(data); DeckComment deckComment = new DeckComment(); makeTraceableIfFails(() -> { @@ -261,7 +262,7 @@ public class JsonToEntityParser { private static Mention parseMention(JsonElement mentionJson) { Mention mention = new Mention(); - DeckLog.verbose(mentionJson.toString()); + DeckLog.verbose(mentionJson); makeTraceableIfFails(() -> { @@ -278,7 +279,7 @@ public class JsonToEntityParser { protected static FullBoard parseBoard(JsonObject e) { FullBoard fullBoard = new FullBoard(); - DeckLog.verbose(e.toString()); + DeckLog.verbose(e); Board board = new Board(); makeTraceableIfFails(() -> { @@ -364,7 +365,7 @@ public class JsonToEntityParser { } protected static AccessControl parseAcl(JsonObject aclJson) { - DeckLog.verbose(aclJson.toString()); + DeckLog.verbose(aclJson); AccessControl acl = new AccessControl(); if (aclJson.has("participant") && !aclJson.get("participant").isJsonNull()) { @@ -387,7 +388,7 @@ public class JsonToEntityParser { } protected static FullCard parseCard(JsonObject e) { - DeckLog.verbose(e.toString()); + DeckLog.verbose(e); FullCard fullCard = new FullCard(); Card card = new Card(); fullCard.setCard(card); @@ -452,12 +453,12 @@ public class JsonToEntityParser { } protected static Attachment parseAttachment(JsonObject e) { - DeckLog.verbose(e.toString()); + DeckLog.verbose(e); Attachment a = new Attachment(); makeTraceableIfFails(() -> { a.setId(e.get("id").getAsLong()); a.setCardId(e.get("cardId").getAsLong()); - a.setType(e.get("type").getAsString()); + a.setType(EAttachmentType.findByValue(e.get("type").getAsString())); a.setEtag(getNullAsNull(e.get("ETag"))); a.setData(e.get("data").getAsString()); a.setLastModified(getTimestampFromLong(e.get("lastModified"))); @@ -468,6 +469,9 @@ public class JsonToEntityParser { JsonObject extendedData = e.getAsJsonObject("extendedData").getAsJsonObject(); a.setFilesize(extendedData.get("filesize").getAsLong()); a.setMimetype(extendedData.get("mimetype").getAsString()); + if (extendedData.has("fileid") && !extendedData.get("fileid").isJsonNull()) { + a.setFileId(extendedData.get("fileid").getAsLong()); + } if (extendedData.has("info") && !extendedData.get("info").isJsonNull()) { JsonObject info = extendedData.getAsJsonObject("info").getAsJsonObject(); a.setDirname(info.get("dirname").getAsString()); @@ -484,7 +488,7 @@ public class JsonToEntityParser { } protected static User parseUser(JsonElement userElement) { - DeckLog.verbose(userElement.toString()); + DeckLog.verbose(userElement); if (userElement.isJsonNull()) { return null; } @@ -509,7 +513,7 @@ public class JsonToEntityParser { protected static Capabilities parseCapabilities(JsonObject e) { - DeckLog.verbose(e.toString()); + DeckLog.verbose(e); Capabilities capabilities = new Capabilities(); // not traceable, we need the original DeckException for error handling if (e.has("ocs")) { @@ -575,7 +579,7 @@ public class JsonToEntityParser { } protected static FullStack parseStack(JsonObject e) { - DeckLog.verbose(e.toString()); + DeckLog.verbose(e); FullStack fullStack = new FullStack(); Stack stack = new Stack(); fullStack.setStack(stack); @@ -605,7 +609,7 @@ public class JsonToEntityParser { } protected static List<Activity> parseActivity(JsonObject e) { - DeckLog.verbose(e.toString()); + DeckLog.verbose(e); List<Activity> activityList = new ArrayList<>(); makeTraceableIfFails(() -> { @@ -633,7 +637,7 @@ public class JsonToEntityParser { } protected static Label parseLabel(JsonObject e) { - DeckLog.verbose(e.toString()); + DeckLog.verbose(e); Label label = new Label(); makeTraceableIfFails(() -> { label.setId(e.get("id").getAsLong()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudArrayDeserializer.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudArrayDeserializer.java index 820cfc653..1e99c8817 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudArrayDeserializer.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudArrayDeserializer.java @@ -35,8 +35,14 @@ public class NextcloudArrayDeserializer<T> implements JsonDeserializer<List<T>> JsonObject obj = jArr.get(i).getAsJsonObject(); items.add(JsonToEntityParser.parseJsonObject(obj, mType)); } + } else if (json.isJsonObject()) { + try { + items.add(JsonToEntityParser.parseJsonObject(json.getAsJsonObject(), mType)); + } catch (Exception e) { + throw new IllegalArgumentException("NextcloudArrayDeserializer got a Json Object, fallback parsing failed for input: " + json); + } } else { - throw new IllegalArgumentException("NextcloudArrayDeserializer got an non-array Json Object: " + json); + throw new IllegalArgumentException("NextcloudArrayDeserializer got a malformed Json Object: " + json); } return items; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudDeserializer.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudDeserializer.java index d23807957..0b84036a4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudDeserializer.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudDeserializer.java @@ -26,7 +26,7 @@ public class NextcloudDeserializer<T> implements JsonDeserializer<T> { @Override public T deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { - DeckLog.verbose(json.toString()); + DeckLog.verbose(json); return JsonToEntityParser.parseJsonObject(json.getAsJsonObject(), mType); } 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 7b93ace49..54e573d73 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 @@ -24,14 +24,27 @@ import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; + +/** + * @link <a href="https://deck.readthedocs.io/en/latest/API-Nextcloud/">Nextcloud REST API</a> + */ public interface NextcloudServerAPI { + + // Capabilities + @GET("cloud/capabilities?format=json") Observable<ParsedResponse<Capabilities>> getCapabilities(@Header("If-None-Match") String eTag); + + // Projects + @GET("collaboration/resources/deck-card/{cardId}?format=json") Observable<OcsProjectList> getProjectsForCard(@Path("cardId") long cardId); + + // Users + @GET("apps/files_sharing/api/v1/sharees?format=json&perPage=20&itemType=0%2C1%2C7") Observable<OcsUserList> searchUser(@Query("search") String searchTerm); @@ -41,9 +54,15 @@ public interface NextcloudServerAPI { @GET("cloud/users/{search}?format=json") Observable<OcsUser> getSingleUserData(@Path("search") String userUid); + + // Activities + @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); + + // Comments + @Headers({ "Accept: application/json", "Content-Type: application/json;charset=utf-8" diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/ResponseCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/ResponseCallback.java new file mode 100644 index 000000000..93b2bcc0d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/ResponseCallback.java @@ -0,0 +1,23 @@ +package it.niedermann.nextcloud.deck.api; + +import androidx.annotation.CallSuper; + +import it.niedermann.nextcloud.deck.DeckLog; + +public interface ResponseCallback<T> { + + void onResponse(T response); + + @CallSuper + default void onError(Throwable throwable) { + DeckLog.logError(throwable); + } + + /** + * @return a default {@link ResponseCallback} which does nothing {@link #onResponse(Object)} and the default action fo {@link #onError(Throwable)} + */ + static <T> ResponseCallback<T> empty() { + return response -> { + }; + } +} 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 7eabcbe70..eaa50dfa1 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 @@ -1,13 +1,14 @@ package it.niedermann.nextcloud.deck.model; +import androidx.annotation.Nullable; import androidx.room.Entity; import androidx.room.ForeignKey; -import androidx.room.Ignore; import androidx.room.Index; import java.io.Serializable; import java.time.Instant; +import it.niedermann.nextcloud.deck.model.enums.EAttachmentType; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @Entity(inheritSuperIndices = true, @@ -24,7 +25,8 @@ import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; public class Attachment extends AbstractRemoteEntity implements Comparable<Attachment>, Serializable { private long cardId; - private String type = "deck_file"; + // TODO use EAttachmentType + private EAttachmentType type = EAttachmentType.DECK_FILE; private String data; private Instant createdAt; private String createdBy; @@ -36,9 +38,8 @@ 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; + @Nullable + private Long fileId; public long getCardId() { return cardId; @@ -48,11 +49,11 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac this.cardId = cardId; } - public String getType() { + public EAttachmentType getType() { return type; } - public void setType(String type) { + public void setType(EAttachmentType type) { this.type = type; } @@ -144,19 +145,12 @@ 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() { + @Nullable + public Long getFileId() { return this.fileId; } - /** - * TODO depends on https://github.com/nextcloud/deck/pull/2638 - */ - @Ignore - public void setFileId(String fileId) { + public void setFileId(@Nullable Long fileId) { this.fileId = fileId; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/ActivityType.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/ActivityType.java index afaeee69c..8b4246bf2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/ActivityType.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/ActivityType.java @@ -43,7 +43,7 @@ public enum ActivityType { return s; } } - DeckLog.error("unknown ActivityType path: " + id); + DeckLog.error("unknown ActivityType path:", id); return CHANGE; } @@ -56,7 +56,7 @@ public enum ActivityType { return s; } } - DeckLog.error("unknown ActivityType path: " + path); + DeckLog.error("unknown ActivityType path:", path); return CHANGE; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/EAttachmentType.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/EAttachmentType.java new file mode 100644 index 000000000..92c15d6e0 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/EAttachmentType.java @@ -0,0 +1,33 @@ +package it.niedermann.nextcloud.deck.model.enums; + +public enum EAttachmentType { + // Do not change values. They match the Deck server apps values. + DECK_FILE(1, "deck_file"), + FILE(2, "file"), + UNKNOWN(1337, "unknown"); + + private final int id; + private final String value; + + EAttachmentType(int id, String value) { + this.id = id; + this.value = value; + } + + public int getId() { + return id; + } + + public static EAttachmentType findByValue(String value) { + for (EAttachmentType s : EAttachmentType.values()) { + if (s.value.equals(value)) { + return s; + } + } + return UNKNOWN; + } + + public String getValue() { + return value; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/EDueType.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/EDueType.java index 5241ae763..da9193635 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/EDueType.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/EDueType.java @@ -8,17 +8,32 @@ import androidx.annotation.StringRes; import it.niedermann.nextcloud.deck.R; public enum EDueType { - NO_FILTER(R.string.filter_no_filter), - OVERDUE(R.string.filter_overdue), - TODAY(R.string.filter_today), - WEEK(R.string.filter_week), - MONTH(R.string.filter_month), - NO_DUE(R.string.filter_no_due); + NO_FILTER(1, R.string.filter_no_filter), + OVERDUE(2, R.string.filter_overdue), + TODAY(3, R.string.filter_today), + WEEK(4, R.string.filter_week), + MONTH(5, R.string.filter_month), + NO_DUE(6, R.string.filter_no_due); - private int value; + private final int value; + private final int id; - EDueType(@StringRes int value) { + EDueType(int id, @StringRes int value) { this.value = value; + this.id = id; + } + + public int getId() { + return id; + } + + public static EDueType findById(int id) { + for (EDueType s : EDueType.values()) { + if (s.getId() == id) { + return s; + } + } + throw new IllegalArgumentException("unknown " + EDueType.class.getSimpleName() + " key: " + id); } @NonNull diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/ESortCriteria.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/ESortCriteria.java new file mode 100644 index 000000000..5b1343302 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/enums/ESortCriteria.java @@ -0,0 +1,35 @@ +package it.niedermann.nextcloud.deck.model.enums; + +public enum ESortCriteria { + /** + * Account → Board → Stack + */ + LOCATION(1), + /** + * Modification date of the card including comments, attachments etc. + */ + MODIFIED(2), + LAST_COMMENTED(3), + DUE_DATE(4), + ASSIGNEE(5), + LABEL(6); + + private final int id; + + ESortCriteria(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ESortCriteria findById(int id) { + for (ESortCriteria s : ESortCriteria.values()) { + if (s.getId() == id) { + return s; + } + } + throw new IllegalArgumentException("unknown " + ESortCriteria.class.getSimpleName() + " key: " + id); + } +} 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 611baf3df..dc4cf1fef 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 @@ -110,7 +110,7 @@ public class FullCard implements IRemoteEntity, DragAndDropModel { } public List<Attachment> getAttachments() { - if (!isAttachmentsSorted) { + if (!isAttachmentsSorted && attachments != null) { Collections.sort(attachments); isAttachmentsSorted = true; } 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 1b5f0879d..81aa8cd80 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 @@ -10,6 +10,7 @@ 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.ocs.projects.OcsProject; public class FilterInformation implements Serializable { @@ -21,11 +22,14 @@ public class FilterInformation implements Serializable { private EDueType dueType = EDueType.NO_FILTER; private boolean noAssignedLabel = false; private boolean noAssignedUser = false; + private boolean noAssignedProject = false; @NonNull private List<User> users = new ArrayList<>(); @NonNull private List<Label> labels = new ArrayList<>(); @NonNull + private List<OcsProject> projects = new ArrayList<>(); + @NonNull private EArchiveStatus archiveStatus = EArchiveStatus.NON_ARCHIVED; public FilterInformation() { @@ -41,6 +45,8 @@ public class FilterInformation implements Serializable { this.noAssignedUser = filterInformation.isNoAssignedUser(); this.noAssignedLabel = filterInformation.isNoAssignedLabel(); this.archiveStatus = filterInformation.getArchiveStatus(); + this.noAssignedProject = filterInformation.isNoAssignedProject(); + this.projects = filterInformation.getProjects(); } } @@ -99,6 +105,31 @@ public class FilterInformation implements Serializable { this.archiveStatus = archiveStatus; } + public void setUsers(@NonNull List<User> users) { + this.users = users; + } + + public boolean isNoAssignedProject() { + return noAssignedProject; + } + + public void setNoAssignedProject(boolean noAssignedProject) { + this.noAssignedProject = noAssignedProject; + } + + public void setLabels(@NonNull List<Label> labels) { + this.labels = labels; + } + + @NonNull + public List<OcsProject> getProjects() { + return projects; + } + + public void setProjects(@NonNull List<OcsProject> projects) { + this.projects = projects; + } + @NonNull public EArchiveStatus getArchiveStatus() { return archiveStatus; @@ -126,8 +157,10 @@ public class FilterInformation implements Serializable { } return filterInformation.getDueType() != EDueType.NO_FILTER || filterInformation.getUsers().size() > 0 + || filterInformation.getProjects().size() > 0 || filterInformation.getLabels().size() > 0 || filterInformation.noAssignedUser + || filterInformation.noAssignedProject || filterInformation.noAssignedLabel; } } 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 cfd973831..91817b73e 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 @@ -1,9 +1,6 @@ package it.niedermann.nextcloud.deck.model.ocs; -import android.content.Context; - import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.StringRes; import java.util.regex.Matcher; @@ -15,11 +12,10 @@ 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_0_6_4 = new Version("0.6.4", 0, 6, 4); 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; private String originalVersion = "?"; private int major = 0; @@ -64,7 +60,7 @@ public class Version implements Comparable<Version> { public static Version of(String versionString) { int major = 0, minor = 0, micro = 0; if (versionString != null) { - String[] split = versionString.split("\\."); + final String[] split = versionString.split("\\."); if (split.length > 0) { major = extractNumber(split[0]); if (split.length > 1) { @@ -87,18 +83,12 @@ public class Version implements Comparable<Version> { } @NonNull - public static Version minimumSupported(@NonNull Context context) { - if (VERSION_MINIMUM_SUPPORTED == null) { - final int minimumServerAppMajor = context.getResources().getInteger(R.integer.minimum_server_app_major); - final int minimumServerAppMinor = context.getResources().getInteger(R.integer.minimum_server_app_minor); - final int minimumServerAppPatch = context.getResources().getInteger(R.integer.minimum_server_app_patch); - VERSION_MINIMUM_SUPPORTED = new Version(minimumServerAppMajor + "." + minimumServerAppMinor + "." + minimumServerAppPatch, minimumServerAppMajor, minimumServerAppMinor, minimumServerAppPatch); - } - return VERSION_MINIMUM_SUPPORTED; + public static Version minimumSupported() { + return VERSION_0_6_4; } - public boolean isSupported(@NonNull Context context) { - return isGreaterOrEqualTo(Version.minimumSupported(context)); + public boolean isSupported() { + return isGreaterOrEqualTo(minimumSupported()); } /** @@ -167,9 +157,7 @@ public class Version implements Comparable<Version> { * @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); + return isGreaterOrEqualTo(VERSION_1_3_0); } /** diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/EWidgetType.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/EWidgetType.java new file mode 100644 index 000000000..6121c7515 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/EWidgetType.java @@ -0,0 +1,45 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +import it.niedermann.nextcloud.deck.ui.widget.filter.FilterWidget; +import it.niedermann.nextcloud.deck.ui.widget.stack.StackWidget; +import it.niedermann.nextcloud.deck.ui.widget.upcoming.UpcomingWidget; + +public enum EWidgetType { + FILTER_WIDGET(1, FilterWidget.class), + UPCOMING_WIDGET(2, UpcomingWidget.class), + STACK_WIDGET(3, StackWidget.class); + + private final int id; + private final Class<?> widgetClass; + + EWidgetType(int id, Class<?> widgetClass) { + this.id = id; + this.widgetClass = widgetClass; + } + + public static EWidgetType findById(int id) { + for (EWidgetType s : EWidgetType.values()) { + if (s.getId() == id) { + return s; + } + } + throw new IllegalArgumentException("unknown " + EWidgetType.class.getSimpleName() + " key: " + id); + } + + public static EWidgetType findByClass(Class<?> clazz) { + for (EWidgetType s : EWidgetType.values()) { + if (s.getWidgetClass() == clazz) { + return s; + } + } + throw new IllegalArgumentException("unknown EWidgetType class"); + } + + public int getId() { + return id; + } + + public Class<?> getWidgetClass() { + return widgetClass; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidget.java new file mode 100644 index 000000000..d6e7d4b08 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidget.java @@ -0,0 +1,142 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.PrimaryKey; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.model.enums.EDueType; + +@Entity() +public class FilterWidget { + + @PrimaryKey() + private int id; + + @Nullable + private String title; + + @Nullable + private EDueType dueType; + + @NonNull + private EWidgetType widgetType = EWidgetType.FILTER_WIDGET; + + @Ignore + @NonNull + private final List<FilterWidgetAccount> accounts = new ArrayList<>(); + + @Ignore + @NonNull + private final List<FilterWidgetSort> sorts = new ArrayList<>(); + + @NonNull + public List<FilterWidgetAccount> getAccounts() { + return accounts; + } + + public FilterWidget() { + // Default constructor + } + + @Ignore + public FilterWidget(int appWidgetId, @NonNull EWidgetType widgetType) { + setId(appWidgetId); + setWidgetType(widgetType); + } + + public void setAccounts(@NonNull List<FilterWidgetAccount> accounts) { + this.accounts.clear(); + this.accounts.addAll(accounts); + } + + public Integer getId() { + return id; + } + + @NonNull + public List<FilterWidgetSort> getSorts() { + return sorts; + } + + @Ignore + public void setSorts(@NonNull FilterWidgetSort sorts) { + this.sorts.clear(); + this.sorts.add(sorts); + } + + public void setSorts(@NonNull List<FilterWidgetSort> sorts) { + this.sorts.clear(); + this.sorts.addAll(sorts); + } + + public void setId(int id) { + this.id = id; + } + + @Nullable + public EDueType getDueType() { + return dueType; + } + + public void setDueType(@Nullable EDueType dueType) { + this.dueType = dueType; + } + + @NonNull + public EWidgetType getWidgetType() { + return widgetType; + } + + public void setWidgetType(@NonNull EWidgetType widgetType) { + this.widgetType = widgetType; + } + + @Nullable + public String getTitle() { + return title; + } + + public void setTitle(@Nullable String title) { + this.title = title; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FilterWidget that = (FilterWidget) o; + + if (id != that.id) return false; + if (title != null ? !title.equals(that.title) : that.title != null) return false; + if (dueType != that.dueType) return false; + if (widgetType != that.widgetType) return false; + if (!accounts.equals(that.accounts)) return false; + return sorts.equals(that.sorts); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + (dueType != null ? dueType.hashCode() : 0); + result = 31 * result + widgetType.hashCode(); + result = 31 * result + accounts.hashCode(); + result = 31 * result + sorts.hashCode(); + return result; + } + + public enum EChangedEntityType { + ACCOUNT, + BOARD, + STACK, + LABEL, + USER, + PROJECT, + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetAccount.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetAccount.java new file mode 100644 index 000000000..caf4e5dd1 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetAccount.java @@ -0,0 +1,161 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Ignore; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import it.niedermann.nextcloud.deck.model.Account; + +@Entity( + indices = { + @Index(value = "filterWidgetId", name = "index_FilterWidgetAccount_filterWidgetId"), + @Index(value = "accountId", name = "idx_FilterWidgetAccount_accountId"), + }, + foreignKeys = { + @ForeignKey( + entity = Account.class, + parentColumns = "id", + childColumns = "accountId", onDelete = ForeignKey.CASCADE + ), + @ForeignKey( + entity = FilterWidget.class, + parentColumns = "id", + childColumns = "filterWidgetId", onDelete = ForeignKey.CASCADE + ), + } +) +public class FilterWidgetAccount { + + @PrimaryKey(autoGenerate = true) + private Long id; + private Long filterWidgetId; + private Long accountId; + private boolean includeNoUser = true; + private boolean includeNoProject = true; + + @Ignore + private List<FilterWidgetBoard> boards = new ArrayList<>(); + + @Ignore + private List<FilterWidgetUser> users = new ArrayList<>(); + + @Ignore + private List<FilterWidgetProject> projects = new ArrayList<>(); + + public FilterWidgetAccount() { + // Default constructor + } + + @Ignore + public FilterWidgetAccount(Long accountId, boolean includeNoUser) { + this.setAccountId(accountId); + this.setIncludeNoUser(includeNoUser); + } + + public void setBoards(List<FilterWidgetBoard> boards) { + this.boards = boards; + } + + public List<FilterWidgetBoard> getBoards() { + return boards; + } + + public List<FilterWidgetUser> getUsers() { + return users; + } + + public void setUsers(List<FilterWidgetUser> users) { + this.users = users; + } + + @Ignore + public void setUsers(FilterWidgetUser user) { + this.users = Collections.singletonList(user); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getFilterWidgetId() { + return filterWidgetId; + } + + public void setFilterWidgetId(Long filterWidgetId) { + this.filterWidgetId = filterWidgetId; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public boolean isIncludeNoUser() { + return includeNoUser; + } + + public void setIncludeNoUser(boolean includeNoUser) { + this.includeNoUser = includeNoUser; + } + + public boolean isIncludeNoProject() { + return includeNoProject; + } + + public void setIncludeNoProject(boolean includeNoProject) { + this.includeNoProject = includeNoProject; + } + + public List<FilterWidgetProject> getProjects() { + return projects; + } + + public void setProjects(List<FilterWidgetProject> projects) { + this.projects = projects; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FilterWidgetAccount that = (FilterWidgetAccount) o; + + if (includeNoUser != that.includeNoUser) return false; + if (includeNoProject != that.includeNoProject) return false; + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (filterWidgetId != null ? !filterWidgetId.equals(that.filterWidgetId) : that.filterWidgetId != null) + return false; + if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) + return false; + if (boards != null ? !boards.equals(that.boards) : that.boards != null) return false; + if (users != null ? !users.equals(that.users) : that.users != null) return false; + return projects != null ? projects.equals(that.projects) : that.projects == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (filterWidgetId != null ? filterWidgetId.hashCode() : 0); + result = 31 * result + (accountId != null ? accountId.hashCode() : 0); + result = 31 * result + (includeNoUser ? 1 : 0); + result = 31 * result + (includeNoProject ? 1 : 0); + result = 31 * result + (boards != null ? boards.hashCode() : 0); + result = 31 * result + (users != null ? users.hashCode() : 0); + result = 31 * result + (projects != null ? projects.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetBoard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetBoard.java new file mode 100644 index 000000000..b1697f9b7 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetBoard.java @@ -0,0 +1,130 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Ignore; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.model.Board; + +@Entity( + indices = { + @Index(value = "filterAccountId", name = "idx_FilterWidgetBoard_filterAccountId"), + @Index(value = "boardId", name = "idx_FilterWidgetBoard_boardId") + }, + foreignKeys = { + @ForeignKey( + entity = Board.class, + parentColumns = "localId", + childColumns = "boardId", onDelete = ForeignKey.CASCADE + ), + @ForeignKey( + entity = FilterWidgetAccount.class, + parentColumns = "id", + childColumns = "filterAccountId", onDelete = ForeignKey.CASCADE + ) + } +) +public class FilterWidgetBoard { + + @PrimaryKey(autoGenerate = true) + private Long id; + private Long filterAccountId; + private Long boardId; + private boolean includeNoLabel = true; + + public FilterWidgetBoard() { + // Default constructor + } + + @Ignore + public FilterWidgetBoard(Long boardId, List<FilterWidgetStack> stacks) { + this.boardId = boardId; + this.stacks = stacks; + } + + @Ignore + private List<FilterWidgetStack> stacks = new ArrayList<>(); + + @Ignore + private List<FilterWidgetLabel> labels = new ArrayList<>(); + + public List<FilterWidgetLabel> getLabels() { + return labels; + } + + public void setLabels(List<FilterWidgetLabel> labels) { + this.labels = labels; + } + + public List<FilterWidgetStack> getStacks() { + return stacks; + } + + public void setStacks(List<FilterWidgetStack> stacks) { + this.stacks = stacks; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getFilterAccountId() { + return filterAccountId; + } + + public void setFilterAccountId(Long filterAccountId) { + this.filterAccountId = filterAccountId; + } + + public Long getBoardId() { + return boardId; + } + + public void setBoardId(Long boardId) { + this.boardId = boardId; + } + + public boolean isIncludeNoLabel() { + return includeNoLabel; + } + + public void setIncludeNoLabel(boolean includeNoLabel) { + this.includeNoLabel = includeNoLabel; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FilterWidgetBoard that = (FilterWidgetBoard) o; + + if (includeNoLabel != that.includeNoLabel) return false; + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (filterAccountId != null ? !filterAccountId.equals(that.filterAccountId) : that.filterAccountId != null) + return false; + if (boardId != null ? !boardId.equals(that.boardId) : that.boardId != null) return false; + if (stacks != null ? !stacks.equals(that.stacks) : that.stacks != null) return false; + return labels != null ? labels.equals(that.labels) : that.labels == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (filterAccountId != null ? filterAccountId.hashCode() : 0); + result = 31 * result + (boardId != null ? boardId.hashCode() : 0); + result = 31 * result + (includeNoLabel ? 1 : 0); + result = 31 * result + (stacks != null ? stacks.hashCode() : 0); + result = 31 * result + (labels != null ? labels.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetLabel.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetLabel.java new file mode 100644 index 000000000..0df280c98 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetLabel.java @@ -0,0 +1,79 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import it.niedermann.nextcloud.deck.model.Label; + +@Entity( + indices = { + @Index(value = "filterBoardId", name = "idx_FilterWidgetLabel_filterBoardId"), + @Index(value = "labelId", name = "idx_FilterWidgetLabel_labelId") + }, + foreignKeys = { + @ForeignKey( + entity = Label.class, + parentColumns = "localId", + childColumns = "labelId", onDelete = ForeignKey.CASCADE + ), + @ForeignKey( + entity = FilterWidgetBoard.class, + parentColumns = "id", + childColumns = "filterBoardId", onDelete = ForeignKey.CASCADE + ) + } +) +public class FilterWidgetLabel { + + @PrimaryKey(autoGenerate = true) + private Long id; + private Long filterBoardId; + private Long labelId; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getFilterBoardId() { + return filterBoardId; + } + + public void setFilterBoardId(Long filterBoardId) { + this.filterBoardId = filterBoardId; + } + + public Long getLabelId() { + return labelId; + } + + public void setLabelId(Long labelId) { + this.labelId = labelId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FilterWidgetLabel that = (FilterWidgetLabel) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (filterBoardId != null ? !filterBoardId.equals(that.filterBoardId) : that.filterBoardId != null) + return false; + return labelId != null ? labelId.equals(that.labelId) : that.labelId == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (filterBoardId != null ? filterBoardId.hashCode() : 0); + result = 31 * result + (labelId != null ? labelId.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetProject.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetProject.java new file mode 100644 index 000000000..7ed3f956c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetProject.java @@ -0,0 +1,89 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Ignore; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; + +@Entity( + indices = { + @Index(value = "filterAccountId", name = "idx_FilterWidgetProject_filterAccountId"), + @Index(value = "projectId", name = "idx_FilterWidgetProject_projectId") + }, + foreignKeys = { + @ForeignKey( + entity = OcsProject.class, + parentColumns = "localId", + childColumns = "projectId", onDelete = ForeignKey.CASCADE + ), + @ForeignKey( + entity = FilterWidgetAccount.class, + parentColumns = "id", + childColumns = "filterAccountId", onDelete = ForeignKey.CASCADE + ) + } +) +public class FilterWidgetProject { + + @PrimaryKey(autoGenerate = true) + private Long id; + private Long filterAccountId; + private Long projectId; + + public FilterWidgetProject() { + // Default constructor + } + + @Ignore + public FilterWidgetProject(Long projectId) { + this.projectId = projectId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getFilterAccountId() { + return filterAccountId; + } + + public void setFilterAccountId(Long filterAccountId) { + this.filterAccountId = filterAccountId; + } + + public Long getProjectId() { + return projectId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FilterWidgetProject that = (FilterWidgetProject) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (filterAccountId != null ? !filterAccountId.equals(that.filterAccountId) : that.filterAccountId != null) + return false; + return projectId != null ? projectId.equals(that.projectId) : that.projectId == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (filterAccountId != null ? filterAccountId.hashCode() : 0); + result = 31 * result + (projectId != null ? projectId.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetSort.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetSort.java new file mode 100644 index 000000000..ae6516c94 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetSort.java @@ -0,0 +1,126 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +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 it.niedermann.nextcloud.deck.model.enums.ESortCriteria; + +@Entity( + indices = { + @Index(value = "filterWidgetId", name = "idx_FilterWidgetSort_filterWidgetId"), + @Index(value = {"filterWidgetId", "criteria"}, name = "unique_idx_FilterWidgetSort_filterWidgetId_criteria"), + @Index(value = {"filterWidgetId", "ruleOrder"}, name = "unique_idx_FilterWidgetSort_filterWidgetId_ruleOrder"), + }, + foreignKeys = { + @ForeignKey( + entity = FilterWidget.class, + parentColumns = "id", + childColumns = "filterWidgetId", onDelete = ForeignKey.CASCADE + ) + } +) +public class FilterWidgetSort { + + @PrimaryKey(autoGenerate = true) + private Long id; + private Long filterWidgetId; + private boolean direction = true; + @NonNull + private ESortCriteria criteria; + private int ruleOrder; + + public FilterWidgetSort() { + // Default constructor + } + + @Ignore + public FilterWidgetSort(ESortCriteria criteria, boolean ascending) { + setCriteria(criteria); + setDirection(ascending); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getFilterWidgetId() { + return filterWidgetId; + } + + public void setFilterWidgetId(Long filterWidgetId) { + this.filterWidgetId = filterWidgetId; + } + + public boolean isDirectionAscending() { + return direction; + } + + public boolean isDirectionDescending() { + return !direction; + } + + public void setDirectionAscending() { + direction = true; + } + + public void setDirectionDescending() { + direction = false; + } + + public ESortCriteria getCriteria() { + return criteria; + } + + public void setCriteria(ESortCriteria criteria) { + this.criteria = criteria; + } + + public int getRuleOrder() { + return ruleOrder; + } + + public void setRuleOrder(int ruleOrder) { + this.ruleOrder = ruleOrder; + } + + public boolean isDirection() { + return direction; + } + + public void setDirection(boolean direction) { + this.direction = direction; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FilterWidgetSort)) return false; + + FilterWidgetSort that = (FilterWidgetSort) o; + + if (direction != that.direction) return false; + if (ruleOrder != that.ruleOrder) return false; + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (filterWidgetId != null ? !filterWidgetId.equals(that.filterWidgetId) : that.filterWidgetId != null) + return false; + return criteria == that.criteria; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (filterWidgetId != null ? filterWidgetId.hashCode() : 0); + result = 31 * result + (direction ? 1 : 0); + result = 31 * result + (criteria != null ? criteria.hashCode() : 0); + result = 31 * result + ruleOrder; + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetStack.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetStack.java new file mode 100644 index 000000000..2da1c0b79 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetStack.java @@ -0,0 +1,89 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Ignore; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import it.niedermann.nextcloud.deck.model.Stack; + +@Entity( + indices = { + @Index(value = "filterBoardId", name = "idx_FilterWidgetStack_filterBoardId"), + @Index(value = "stackId", name = "idx_FilterWidgetStack_stackId") + }, + foreignKeys = { + @ForeignKey( + entity = Stack.class, + parentColumns = "localId", + childColumns = "stackId", onDelete = ForeignKey.CASCADE + ), + @ForeignKey( + entity = FilterWidgetBoard.class, + parentColumns = "id", + childColumns = "filterBoardId", onDelete = ForeignKey.CASCADE + ) + } +) +public class FilterWidgetStack { + + @PrimaryKey(autoGenerate = true) + private Long id; + private Long filterBoardId; + private Long stackId; + + public FilterWidgetStack() { + // Default constructor + } + + @Ignore + public FilterWidgetStack(Long stackId) { + this.stackId = stackId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getFilterBoardId() { + return filterBoardId; + } + + public void setFilterBoardId(Long filterBoardId) { + this.filterBoardId = filterBoardId; + } + + public Long getStackId() { + return stackId; + } + + public void setStackId(Long stackId) { + this.stackId = stackId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FilterWidgetStack that = (FilterWidgetStack) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (filterBoardId != null ? !filterBoardId.equals(that.filterBoardId) : that.filterBoardId != null) + return false; + return stackId != null ? stackId.equals(that.stackId) : that.stackId == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (filterBoardId != null ? filterBoardId.hashCode() : 0); + result = 31 * result + (stackId != null ? stackId.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetUser.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetUser.java new file mode 100644 index 000000000..9fd69aa85 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/FilterWidgetUser.java @@ -0,0 +1,89 @@ +package it.niedermann.nextcloud.deck.model.widget.filter; + +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Ignore; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import it.niedermann.nextcloud.deck.model.User; + +@Entity( + indices = { + @Index(value = "filterAccountId", name = "idx_FilterWidgetUser_filterAccountId"), + @Index(value = "userId", name = "idx_FilterWidgetUser_userId") + }, + foreignKeys = { + @ForeignKey( + entity = User.class, + parentColumns = "localId", + childColumns = "userId", onDelete = ForeignKey.CASCADE + ), + @ForeignKey( + entity = FilterWidgetAccount.class, + parentColumns = "id", + childColumns = "filterAccountId", onDelete = ForeignKey.CASCADE + ) + } +) +public class FilterWidgetUser { + + @PrimaryKey(autoGenerate = true) + private Long id; + private Long filterAccountId; + private Long userId; + + public FilterWidgetUser() { + // Default constructor + } + + @Ignore + public FilterWidgetUser(Long userId) { + this.userId = userId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getFilterAccountId() { + return filterAccountId; + } + + public void setFilterAccountId(Long filterAccountId) { + this.filterAccountId = filterAccountId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FilterWidgetUser that = (FilterWidgetUser) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + if (filterAccountId != null ? !filterAccountId.equals(that.filterAccountId) : that.filterAccountId != null) + return false; + return userId != null ? userId.equals(that.userId) : that.userId == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (filterAccountId != null ? filterAccountId.hashCode() : 0); + result = 31 * result + (userId != null ? userId.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/dto/FilterWidgetCard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/dto/FilterWidgetCard.java new file mode 100644 index 000000000..2aaebb129 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/widget/filter/dto/FilterWidgetCard.java @@ -0,0 +1,45 @@ +package it.niedermann.nextcloud.deck.model.widget.filter.dto; + +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.full.FullCard; + +public class FilterWidgetCard { + private FullCard card; + private Stack stack; + private Board board; + + public FilterWidgetCard() { + // Default constructor + } + + public FilterWidgetCard(FullCard card, Stack stack, Board board) { + this.card = card; + this.stack = stack; + this.board = board; + } + + public FullCard getCard() { + return card; + } + + public void setCard(FullCard card) { + this.card = card; + } + + public Stack getStack() { + return stack; + } + + public void setStack(Stack stack) { + this.stack = stack; + } + + public Board getBoard() { + return board; + } + + public void setBoard(Board board) { + this.board = board; + } +} 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 d93835423..de0acc53b 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 @@ -37,6 +37,7 @@ 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.api.ResponseCallback; import it.niedermann.nextcloud.deck.exceptions.DeckException; import it.niedermann.nextcloud.deck.exceptions.OfflineException; import it.niedermann.nextcloud.deck.model.AccessControl; @@ -61,6 +62,8 @@ 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.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; +import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard; 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; @@ -103,10 +106,14 @@ public class SyncManager { @AnyThread public SyncManager(@NonNull Context context, @Nullable String ssoAccountName) { - appContext = context.getApplicationContext(); - LastSyncUtil.init(appContext); - this.dataBaseAdapter = new DataBaseAdapter(appContext); - this.serverAdapter = new ServerAdapter(appContext, ssoAccountName); + this(context, new DataBaseAdapter(context.getApplicationContext()), new ServerAdapter(context.getApplicationContext(), ssoAccountName)); + LastSyncUtil.init(context.getApplicationContext()); + } + + private SyncManager(@NonNull Context context, @NonNull DataBaseAdapter databaseAdapter, @NonNull ServerAdapter serverAdapter) { + this.appContext = context.getApplicationContext(); + this.dataBaseAdapter = databaseAdapter; + this.serverAdapter = serverAdapter; } @AnyThread @@ -208,8 +215,13 @@ public class SyncManager { refreshCapabilities(new IResponseCallback<Capabilities>(responseCallback.getAccount()) { @Override public void onResponse(Capabilities response) { + if (response != null) { + // When importing an account, the create account color can only be applied after the initial capabilities refresh + callbackAccount.setColor(response.getColor()); + } + if (response != null && !response.isMaintenanceEnabled()) { - if (response.getDeckVersion().isSupported(appContext)) { + if (response.getDeckVersion().isSupported()) { long accountId = callbackAccountId; Instant lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId); @@ -260,12 +272,12 @@ public class SyncManager { } } else { respondCallbacksAfterSync(callbacksQueueForSync, Boolean.FALSE, null); - DeckLog.warn("No sync. Server version not supported: " + response.getDeckVersion().getOriginalVersion()); + DeckLog.warn("No sync. Server version not supported:", response.getDeckVersion().getOriginalVersion()); } } else { respondCallbacksAfterSync(callbacksQueueForSync, Boolean.FALSE, null); if (response != null) { - DeckLog.warn("No sync. Status maintenance mode: " + response.isMaintenanceEnabled()); + DeckLog.warn("No sync. Status maintenance mode:", response.isMaintenanceEnabled()); } } } @@ -369,6 +381,11 @@ public class SyncManager { return dataBaseAdapter.readAccount(id); } + @WorkerThread + public Account readAccountDirectly(long id) { + return dataBaseAdapter.readAccountDirectly(id); + } + @AnyThread public LiveData<Account> readAccount(@Nullable String name) { return dataBaseAdapter.readAccount(name); @@ -379,6 +396,11 @@ public class SyncManager { return dataBaseAdapter.readAccounts(); } + @WorkerThread + public List<Account> readAccountsDirectly() { + return dataBaseAdapter.getAllAccountsDirectly(); + } + /** * <p> * Since the return value is a {@link LiveData}, it should immediately return the available values from the database @@ -423,6 +445,7 @@ public class SyncManager { Account acc = dataBaseAdapter.getAccountByIdDirectly(account.getId()); acc.applyCapabilities(response.getResponse(), response.getHeaders().get("ETag")); dataBaseAdapter.updateAccount(acc); + callback.getAccount().setServerDeckVersion(acc.getServerDeckVersion()); callback.onResponse(response.getResponse()); } @@ -434,14 +457,14 @@ 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."); + DeckLog.verbose("HTTP Status", HTTP_UNAVAILABLE + ": This server seems to be in maintenance mode."); if (capabilities.isMaintenanceEnabled()) { 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."); + 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()); @@ -524,14 +547,12 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<FullBoard> createBoard(long accountId, @NonNull Board board) { - WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); + public void createBoard(long accountId, @NonNull Board board, @NonNull ResponseCallback<FullBoard> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); User owner = dataBaseAdapter.getUserByUidDirectly(accountId, account.getUserName()); if (owner == null) { - DeckLog.log("owner is null - this can be the case if the Deck app has never before been opened in the webinterface"); - liveData.postValue(null); + callback.onError(new Exception("Owner is null. This can be the case if the Deck app has never before been opened in the webinterface")); } else { FullBoard fullBoard = new FullBoard(); board.setOwnerId(owner.getLocalId()); @@ -539,21 +560,9 @@ public class SyncManager { fullBoard.setBoard(board); board.setAccountId(accountId); fullBoard.setAccountId(accountId); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new BoardDataProvider(), fullBoard, new IResponseCallback<FullBoard>(account) { - @Override - public void onResponse(FullBoard response) { - liveData.postValue(response); - } - - @SuppressLint("MissingSuperCall") - @Override - public void onError(Throwable throwable, FullBoard entity) { - liveData.postError(throwable, entity); - } - }); + new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new BoardDataProvider(), fullBoard, IResponseCallback.from(account, callback)); } }); - return liveData; } /** @@ -562,18 +571,14 @@ public class SyncManager { * * @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, @ColorInt int targetBoardColor, boolean cloneCards) { - final WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); - + public void cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards, @NonNull ResponseCallback<FullBoard> callback) { 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.")); + callback.onError(new DeckException(DeckException.Hint.UNKNOWN_ACCOUNT_USER_ID, "User with Account-UID \"" + originAccount.getUserName() + "\" not found.")); return; } FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(originAccountId, originBoardLocalId); @@ -663,7 +668,7 @@ public class SyncManager { if (newLabelId != null) { dataBaseAdapter.createJoinCardWithLabel(newLabelId, createdCardId, DBStatus.LOCAL_EDITED); } else - DeckLog.error("ID of created Label is null! Skipping assignment of \"" + oldLabel.getTitle() + "\"..."); + DeckLog.error("ID of created Label is null! Skipping assignment of ", oldLabel.getTitle(), "…"); } } if (isSameAccount && oldCard.getAssignedUsers() != null) { @@ -686,20 +691,19 @@ public class SyncManager { .setResponseCallback(new IResponseCallback<Boolean>(targetAccount) { @Override public void onResponse(Boolean response) { - liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); + callback.onResponse(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { - super.onError(throwable); - liveData.postError(throwable); + callback.onError(throwable); } }).doUpSyncFor(new BoardWithStacksAndLabelsUpSyncDataProvider(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId))); } else { - liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); + callback.onResponse(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); } }); - return liveData; } @AnyThread @@ -715,7 +719,7 @@ public class SyncManager { } }).doSyncFor(new ActivityDataProvider(null, card)); } else { - DeckLog.log("Can not fetch activities for card \"" + card.getTitle() + "\" because this card does not have a remote id yet."); + DeckLog.log("Can not fetch activities for card ", card.getTitle(), "because this card does not have a remote id yet."); } } }); @@ -755,17 +759,15 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<Void> deleteComment(long accountId, long localCardId, long localCommentId) { - WrappedLiveData<Void> liveData = new WrappedLiveData<>(); + public void deleteComment(long accountId, long localCardId, long localCommentId, @NonNull ResponseCallback<Void> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); Card card = dataBaseAdapter.getCardByLocalIdDirectly(accountId, localCardId); DeckComment entity = dataBaseAdapter.getCommentByLocalIdDirectly(accountId, localCommentId); OcsComment commentEntity = OcsComment.of(entity); new DataPropagationHelper(serverAdapter, dataBaseAdapter).deleteEntity(new DeckCommentsDataProvider(null, card), - commentEntity, getCallbackToLiveDataConverter(account, liveData)); + commentEntity, IResponseCallback.from(account, callback)); }); - return liveData; } public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) { @@ -773,37 +775,22 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<Void> deleteBoard(@NonNull Board board) { - WrappedLiveData<Void> liveData = new WrappedLiveData<>(); + public void deleteBoard(@NonNull Board board, @NonNull ResponseCallback<Void> callback) { doAsync(() -> { long accountId = board.getAccountId(); Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullBoard fullBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, board.getLocalId()); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).deleteEntity(new BoardDataProvider(), fullBoard, getCallbackToLiveDataConverter(account, liveData)); + new DataPropagationHelper(serverAdapter, dataBaseAdapter).deleteEntity(new BoardDataProvider(), fullBoard, IResponseCallback.from(account, callback)); }); - return liveData; } @AnyThread - public WrappedLiveData<FullBoard> updateBoard(@NonNull FullBoard board) { - WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); - long accountId = board.getAccountId(); + public void updateBoard(@NonNull FullBoard board, @NonNull ResponseCallback<FullBoard> callback) { doAsync(() -> { + long accountId = board.getAccountId(); Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new BoardDataProvider(), board, new IResponseCallback<FullBoard>(account) { - @Override - public void onResponse(FullBoard response) { - liveData.postValue(response); - } - - @SuppressLint("MissingSuperCall") - @Override - public void onError(Throwable throwable) { - liveData.postError(throwable); - } - }); + new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new BoardDataProvider(), board, IResponseCallback.from(account, callback)); }); - return liveData; } public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { @@ -814,20 +801,23 @@ public class SyncManager { return dataBaseAdapter.getStack(accountId, localStackId); } + @WorkerThread + public Long getBoardLocalIdByLocalCardIdDirectly(long localCardId) { + return dataBaseAdapter.getBoardLocalIdByLocalCardIdDirectly(localCardId); + } + @AnyThread - public WrappedLiveData<AccessControl> createAccessControl(long accountId, AccessControl entity) { - WrappedLiveData<AccessControl> liveData = new WrappedLiveData<>(); + public void createAccessControl(long accountId, @NonNull AccessControl entity, @NonNull ResponseCallback<AccessControl> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, entity.getBoardId()); new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity( - new AccessControlDataProvider(null, board, Collections.singletonList(entity)), entity, getCallbackToLiveDataConverter(account, liveData), ((entity1, response) -> { + new AccessControlDataProvider(null, board, Collections.singletonList(entity)), entity, IResponseCallback.from(account, callback), ((entity1, response) -> { response.setBoardId(entity.getBoardId()); response.setUserId(entity.getUser().getLocalId()); }) ); }); - return liveData; } @WorkerThread @@ -840,36 +830,17 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<AccessControl> updateAccessControl(@NonNull AccessControl entity) { - WrappedLiveData<AccessControl> liveData = new WrappedLiveData<>(); + public void updateAccessControl(@NonNull AccessControl entity, @NonNull ResponseCallback<AccessControl> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(entity.getAccountId()); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(entity.getAccountId(), entity.getBoardId()); new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity( - new AccessControlDataProvider(null, board, Collections.singletonList(entity)), entity, getCallbackToLiveDataConverter(account, liveData)); + new AccessControlDataProvider(null, board, Collections.singletonList(entity)), entity, IResponseCallback.from(account, callback)); }); - return liveData; } @AnyThread - private <T> IResponseCallback<T> getCallbackToLiveDataConverter(Account account, @NonNull WrappedLiveData<T> liveData) { - return new IResponseCallback<T>(account) { - @Override - public void onResponse(T response) { - liveData.postValue(response); - } - - @SuppressLint("MissingSuperCall") - @Override - public void onError(Throwable throwable) { - liveData.postError(throwable); - } - }; - } - - @AnyThread - public WrappedLiveData<Void> deleteAccessControl(@NonNull AccessControl entity) { - WrappedLiveData<Void> liveData = new WrappedLiveData<>(); + public void deleteAccessControl(@NonNull AccessControl entity, @NonNull ResponseCallback<Void> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(entity.getAccountId()); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(entity.getAccountId(), entity.getBoardId()); @@ -881,17 +852,16 @@ public class SyncManager { if (entity.getAccountId() == entity.getAccountId() && entity.getUser().getUid().equals(account.getUserName())) { dataBaseAdapter.deleteBoardPhysically(board.getBoard()); } - liveData.postValue(response); + callback.onResponse(response); } @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { - liveData.postError(throwable); + callback.onError(throwable); } }); }); - return liveData; } public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { @@ -899,8 +869,7 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<FullStack> createStack(long accountId, @NonNull String title, long boardLocalId) { - WrappedLiveData<FullStack> liveData = new WrappedLiveData<>(); + public void createStack(long accountId, @NonNull String title, long boardLocalId, @NonNull ResponseCallback<FullStack> callback) { doAsync(() -> { Stack stack = new Stack(title, boardLocalId); Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); @@ -911,65 +880,34 @@ public class SyncManager { stack.setBoardId(board.getLocalId()); fullStack.setStack(stack); fullStack.setAccountId(accountId); - 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); - } - }); + new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new StackDataProvider(null, board), fullStack, IResponseCallback.from(account, callback)); }); - return liveData; } @AnyThread - public WrappedLiveData<Void> deleteStack(long accountId, long stackLocalId, long boardLocalId) { - WrappedLiveData<Void> liveData = new WrappedLiveData<>(); + public void deleteStack(long accountId, long stackLocalId, long boardLocalId, @NonNull ResponseCallback<Void> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullStack fullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackLocalId); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, boardLocalId); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).deleteEntity(new StackDataProvider(null, board), fullStack, getCallbackToLiveDataConverter(account, liveData)); + new DataPropagationHelper(serverAdapter, dataBaseAdapter).deleteEntity(new StackDataProvider(null, board), fullStack, IResponseCallback.from(account, callback)); }); - return liveData; } @AnyThread - public WrappedLiveData<FullStack> updateStackTitle(long localStackId, @NonNull String newTitle) { - WrappedLiveData<FullStack> liveData = new WrappedLiveData<>(); + public void updateStackTitle(long localStackId, @NonNull String newTitle, @NonNull ResponseCallback<FullStack> callback) { doAsync(() -> { FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(localStackId); FullBoard fullBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(stack.getAccountId(), stack.getStack().getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(stack.getAccountId()); stack.getStack().setTitle(newTitle); - updateStack(account, fullBoard, stack, liveData); + updateStack(account, fullBoard, stack, callback); }); - 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); - } - } - - @SuppressLint("MissingSuperCall") - @Override - public void onError(Throwable throwable) { - if (liveData != null) { - liveData.postError(throwable); - } - } - })); + private void updateStack(@NonNull Account account, @NonNull FullBoard board, @NonNull FullStack stack, @NonNull ResponseCallback<FullStack> callback) { + doAsync(() -> new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new StackDataProvider(null, board), stack, IResponseCallback.from(account, callback))); } /** @@ -994,8 +932,8 @@ public class SyncManager { int orderFirst = stacks.first.getStack().getOrder(); stacks.first.getStack().setOrder(stacks.second.getStack().getOrder()); stacks.second.getStack().setOrder(orderFirst); - updateStack(account, fullBoard, stacks.first, null); - updateStack(account, fullBoard, stacks.second, null); + updateStack(account, fullBoard, stacks.first, ResponseCallback.empty()); + updateStack(account, fullBoard, stacks.second, ResponseCallback.empty()); }); } @@ -1087,7 +1025,6 @@ public class SyncManager { } } - if (serverAdapter.hasInternetConnection()) { new SyncHelper(serverAdapter, dataBaseAdapter, null) .setResponseCallback(new IResponseCallback<Boolean>(account) { @@ -1099,7 +1036,7 @@ public class SyncManager { @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { - if (throwable.getClass() == DeckException.class && ((DeckException)throwable).getHint().equals(DeckException.Hint.DEPENDENCY_NOT_SYNCED_YET)) { + if (throwable.getClass() == DeckException.class && ((DeckException) throwable).getHint().equals(DeckException.Hint.DEPENDENCY_NOT_SYNCED_YET)) { liveData.postValue(card); } else { liveData.postError(throwable); @@ -1115,8 +1052,7 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<Void> deleteCard(@NonNull Card card) { - WrappedLiveData<Void> liveData = new WrappedLiveData<>(); + public void deleteCard(@NonNull Card card, @NonNull ResponseCallback<Void> callback) { doAsync(() -> { FullCard fullCard = dataBaseAdapter.getFullCardByLocalIdDirectly(card.getAccountId(), card.getLocalId()); if (fullCard == null) { @@ -1125,22 +1061,19 @@ public class SyncManager { Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).deleteEntity(new CardPropagationDataProvider(null, board, stack), fullCard, getCallbackToLiveDataConverter(account, liveData)); + new DataPropagationHelper(serverAdapter, dataBaseAdapter).deleteEntity(new CardPropagationDataProvider(null, board, stack), fullCard, IResponseCallback.from(account, callback)); }); - return liveData; } @AnyThread - public WrappedLiveData<FullCard> archiveCard(@NonNull FullCard card) { - WrappedLiveData<FullCard> liveData = new WrappedLiveData<>(); + public void archiveCard(@NonNull FullCard card, @NonNull ResponseCallback<FullCard> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); card.getCard().setArchived(true); - updateCardForArchive(stack, board, card, getCallbackToLiveDataConverter(account, liveData)); + updateCardForArchive(stack, board, card, IResponseCallback.from(account, callback)); }); - return liveData; } private void updateCardForArchive(FullStack stack, Board board, FullCard card, @NonNull IResponseCallback<FullCard> callback) { @@ -1148,91 +1081,81 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<FullCard> dearchiveCard(@NonNull FullCard card) { - WrappedLiveData<FullCard> liveData = new WrappedLiveData<>(); + public void dearchiveCard(@NonNull FullCard card, @NonNull ResponseCallback<FullCard> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); card.getCard().setArchived(false); - updateCardForArchive(stack, board, card, getCallbackToLiveDataConverter(account, liveData)); + updateCardForArchive(stack, board, card, IResponseCallback.from(account, callback)); }); - return liveData; } @AnyThread - public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation) { - WrappedLiveData<Void> liveData = new WrappedLiveData<>(); + public void archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation, @NonNull ResponseCallback<Void> callback) { 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, filterInformation); - if (cards.size() > 0) { - CountDownLatch latch = new CountDownLatch(cards.size()); - for (FullCard card : cards) { - if (card.getCard().isArchived()) { + if (cards.size() <= 0) { + callback.onResponse(null); + return; + } + final CountDownLatch latch = new CountDownLatch(cards.size()); + for (FullCard card : cards) { + if (card.getCard().isArchived()) { + latch.countDown(); + continue; + } + card.getCard().setArchived(true); + updateCardForArchive(stack, board, card, new IResponseCallback<FullCard>(account) { + @Override + public void onResponse(FullCard response) { latch.countDown(); - continue; } - card.getCard().setArchived(true); - 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(); - liveData.postError(throwable); - } - }); - } - try { - latch.await(); - liveData.postValue(null); - } catch (InterruptedException e) { - liveData.postError(e); - } - } else { - liveData.postValue(null); + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + latch.countDown(); + callback.onError(throwable); + } + }); + } + try { + latch.await(); + callback.onResponse(null); + } catch (InterruptedException e) { + callback.onError(e); } }); - return liveData; } @AnyThread - public WrappedLiveData<FullBoard> archiveBoard(@NonNull Board board) { - WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); + public void archiveBoard(@NonNull Board board, @NonNull ResponseCallback<FullBoard> callback) { doAsync(() -> { try { FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); b.getBoard().setArchived(true); - updateBoard(b); - liveData.postValue(b); + updateBoard(b, callback); } catch (Throwable e) { - liveData.postError(e); + callback.onError(e); } }); - return liveData; } @AnyThread - public WrappedLiveData<FullBoard> dearchiveBoard(@NonNull Board board) { - WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); + public void dearchiveBoard(@NonNull Board board, @NonNull ResponseCallback<FullBoard> callback) { doAsync(() -> { try { FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); b.getBoard().setArchived(false); - updateBoard(b); - liveData.postValue(b); + updateBoard(b, callback); } catch (Throwable e) { - liveData.postError(e); + callback.onError(e); } }); - return liveData; } @AnyThread @@ -1314,7 +1237,6 @@ public class SyncManager { @AnyThread public WrappedLiveData<Void> moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) { return LiveDataHelper.wrapInLiveData(() -> { - final FullCard originalCard = dataBaseAdapter.getFullCardByLocalIdDirectly(originAccountId, originCardLocalId); int newIndex = dataBaseAdapter.getHighestCardOrderInStack(targetStackLocalId) + 1; final FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalCardIdDirectly(originCardLocalId); @@ -1325,7 +1247,7 @@ public class SyncManager { } // ### get rid of original card where it is now. final Card originalInnerCard = originalCard.getCard(); - deleteCard(new Card(originalInnerCard)); + deleteCard(new Card(originalInnerCard), ResponseCallback.empty()); // ### clone card itself originalInnerCard.setAccountId(targetAccountId); originalInnerCard.setId(null); @@ -1442,31 +1364,18 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) { - WrappedLiveData<Label> liveData = new WrappedLiveData<>(); + public void createLabel(long accountId, Label label, long localBoardId, @NonNull ResponseCallback<Label> callback) { doAsync(() -> { Label existing = dataBaseAdapter.getLabelByBoardIdAndTitleDirectly(label.getBoardId(), label.getTitle()); if (existing != null) { - liveData.postError(new SQLiteConstraintException("label \"" + label.getTitle() + "\" already exists for this board!")); + callback.onError(new SQLiteConstraintException("label \"" + label.getTitle() + "\" already exists for this board!")); return; } Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(localBoardId); label.setAccountId(accountId); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new LabelDataProvider(null, board, null), label, new IResponseCallback<Label>(account) { - @Override - public void onResponse(Label response) { - liveData.postValue(response); - } - - @SuppressLint("MissingSuperCall") - @Override - public void onError(Throwable throwable) { - liveData.postError(throwable); - } - }, (entity, response) -> response.setBoardId(board.getLocalId())); + new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new LabelDataProvider(null, board, null), label, IResponseCallback.from(account, callback), (entity, response) -> response.setBoardId(board.getLocalId())); }); - return liveData; } public MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId) { @@ -1500,27 +1409,23 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<Void> deleteLabel(@NonNull Label label) { - WrappedLiveData<Void> liveData = new WrappedLiveData<>(); + public void deleteLabel(@NonNull Label label, @NonNull ResponseCallback<Void> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(label.getAccountId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId()); new DataPropagationHelper(serverAdapter, dataBaseAdapter) - .deleteEntity(new LabelDataProvider(null, board, Collections.emptyList()), label, getCallbackToLiveDataConverter(account, liveData)); + .deleteEntity(new LabelDataProvider(null, board, Collections.emptyList()), label, IResponseCallback.from(account, callback)); }); - return liveData; } @AnyThread - public WrappedLiveData<Label> updateLabel(@NonNull Label label) { - WrappedLiveData<Label> liveData = new WrappedLiveData<>(); + public void updateLabel(@NonNull Label label, @NonNull ResponseCallback<Label> callback) { doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(label.getAccountId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId()); new DataPropagationHelper(serverAdapter, dataBaseAdapter) - .updateEntity(new LabelDataProvider(null, board, Collections.emptyList()), label, getCallbackToLiveDataConverter(account, liveData)); + .updateEntity(new LabelDataProvider(null, board, Collections.emptyList()), label, IResponseCallback.from(account, callback)); }); - return liveData; } @AnyThread @@ -1850,8 +1755,7 @@ public class SyncManager { * Also this leads to the attachment being present in the card forever with a DBStatus.LOCAL_EDITED */ @AnyThread - public WrappedLiveData<Attachment> addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file) { - WrappedLiveData<Attachment> liveData = new WrappedLiveData<>(); + public void addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file, @NonNull ResponseCallback<Attachment> callback) { doAsync(() -> { Attachment attachment = populateAttachmentEntityForFile(new Attachment(), localCardId, mimeType, file); final Instant now = Instant.now(); @@ -1863,10 +1767,9 @@ public class SyncManager { Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity( new AttachmentDataProvider(null, board, stack, card, Collections.singletonList(attachment)), - attachment, getCallbackToLiveDataConverter(account, liveData) + attachment, IResponseCallback.from(account, callback) ); }); - return liveData; } @AnyThread @@ -1911,8 +1814,7 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<Void> deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId) { - WrappedLiveData<Void> liveData = new WrappedLiveData<>(); + public void deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId, @NonNull ResponseCallback<Void> callback) { doAsync(() -> { if (serverAdapter.hasInternetConnection()) { FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, localCardId); @@ -1922,16 +1824,75 @@ public class SyncManager { Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); new DataPropagationHelper(serverAdapter, dataBaseAdapter) - .deleteEntity(new AttachmentDataProvider(null, board, stack, card, Collections.singletonList(attachment)), attachment, getCallbackToLiveDataConverter(account, liveData)); + .deleteEntity(new AttachmentDataProvider(null, board, stack, card, Collections.singletonList(attachment)), attachment, IResponseCallback.from(account, callback)); } }); - return liveData; } // ------------------- // Widgets // ------------------- + // # filter widget + + @AnyThread + public void createFilterWidget(@NonNull FilterWidget filterWidget, @NonNull ResponseCallback<Integer> callback) { + doAsync(() -> { + try { + int filterWidgetId = dataBaseAdapter.createFilterWidgetDirectly(filterWidget); + callback.onResponse(filterWidgetId); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void updateFilterWidget(@NonNull FilterWidget filterWidget, @NonNull IResponseCallback<Boolean> callback) { + doAsync(() -> { + try { + dataBaseAdapter.updateFilterWidgetDirectly(filterWidget); + callback.onResponse(Boolean.TRUE); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void getFilterWidget(@NonNull Integer filterWidgetId, @NonNull ResponseCallback<FilterWidget> callback) { + doAsync(() -> { + try { + callback.onResponse(dataBaseAdapter.getFilterWidgetByIdDirectly(filterWidgetId)); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void deleteFilterWidget(int filterWidgetId, @NonNull ResponseCallback<Boolean> callback) { + doAsync(() -> { + try { + dataBaseAdapter.deleteFilterWidgetDirectly(filterWidgetId); + callback.onResponse(Boolean.TRUE); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + public boolean filterWidgetExists(int id) { + return dataBaseAdapter.filterWidgetExists(id); + } + + @WorkerThread + public List<FilterWidgetCard> getCardsForFilterWidget(@NonNull Integer filterWidgetId) { + return dataBaseAdapter.getCardsForFilterWidget(filterWidgetId); + } + + // # single card widget + /** * Can be called from a configuration screen or a picker. * Creates a new entry in the database, if row with given widgetId does not yet exist. @@ -1942,10 +1903,10 @@ public class SyncManager { } @WorkerThread - public FullSingleCardWidgetModel getSingleCardWidgetModelDirectly(int widgetId) throws NoSuchElementException { - final FullSingleCardWidgetModel model = dataBaseAdapter.getFullSingleCardWidgetModel(widgetId); + public FullSingleCardWidgetModel getSingleCardWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { + final FullSingleCardWidgetModel model = dataBaseAdapter.getFullSingleCardWidgetModel(appWidgetId); if (model == null) { - throw new NoSuchElementException(); + throw new NoSuchElementException("There is no " + FullSingleCardWidgetModel.class.getSimpleName() + " with the given appWidgetId " + appWidgetId); } return model; } @@ -1978,4 +1939,15 @@ public class SyncManager { 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()); } + + @WorkerThread + public Stack getStackDirectly(long stackLocalId) { + return dataBaseAdapter.getStackByLocalIdDirectly(stackLocalId); + } + + @ColorInt + @WorkerThread + public Integer getBoardColorDirectly(long accountId, long localBoardId) { + return dataBaseAdapter.getBoardColorDirectly(accountId, localBoardId); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java index f43cbd34f..af6e08826 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java @@ -40,7 +40,7 @@ public class SyncWorker extends Worker { sharedPreferencesEditor.putLong(getApplicationContext().getString(R.string.shared_preference_last_background_sync), System.currentTimeMillis()); sharedPreferencesEditor.apply(); boolean success = syncManager.synchronizeEverything(); - DeckLog.info("Finishing background synchronization with result " + success); + DeckLog.info("Finishing background synchronization. Success: ", success); return success ? Result.failure() : Result.success(); } return Result.success(); @@ -66,17 +66,17 @@ public class SyncWorker extends Worker { unit = TimeUnit.HOURS; } if (unit == null) { - DeckLog.info("Do not register a new " + SyncWorker.class.getSimpleName() + " because setting " + preferenceValue + " is not a valid time frame"); + DeckLog.info("Do not register a new", SyncWorker.class.getSimpleName(), "because setting", preferenceValue, "is not a valid time frame"); } else { final PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(SyncWorker.class, repeatInterval, unit) .setConstraints(constraints).build(); - DeckLog.info("Registering " + SyncWorker.class.getSimpleName() + " running each " + repeatInterval + " " + unit); + DeckLog.info("Registering", SyncWorker.class.getSimpleName(), "running each", repeatInterval, unit); WorkManager.getInstance(context.getApplicationContext()).enqueueUniquePeriodicWork(SyncWorker.WORKER_TAG, ExistingPeriodicWorkPolicy.REPLACE, work); } } private static void deregister(@NonNull Context context) { - DeckLog.info("Deregistering all " + SyncWorker.class.getSimpleName() + " with tag \"" + WORKER_TAG + "\""); + DeckLog.info("Deregistering all", SyncWorker.class.getSimpleName(), "with tag", WORKER_TAG); WorkManager.getInstance(context.getApplicationContext()).cancelAllWorkByTag(WORKER_TAG); } } 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 bff8ddfdb..8cadbaec8 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 @@ -24,11 +24,13 @@ import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.api.RequestHelper; import it.niedermann.nextcloud.deck.exceptions.OfflineException; 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.enums.EAttachmentType; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.full.FullStack; @@ -51,6 +53,8 @@ import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN; public class ServerAdapter { private final String prefKeyWifiOnly; + private final String prefKeyEtags; + final SharedPreferences sharedPreferences; @NonNull private final Context applicationContext; @@ -63,7 +67,9 @@ public class ServerAdapter { public ServerAdapter(@NonNull Context applicationContext, @Nullable String ssoAccountName) { this.applicationContext = applicationContext; prefKeyWifiOnly = applicationContext.getResources().getString(R.string.pref_key_wifi_only); + prefKeyEtags = applicationContext.getResources().getString(R.string.pref_key_etags); provider = new ApiProvider(applicationContext, ssoAccountName); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); } public String getServerUrl() { @@ -88,7 +94,6 @@ public class ServerAdapter { public boolean hasInternetConnection() { ConnectivityManager cm = (ConnectivityManager) applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE); if (cm != null) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); if (sharedPreferences.getBoolean(prefKeyWifiOnly, false)) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { Network network = cm.getActiveNetwork(); @@ -104,8 +109,6 @@ public class ServerAdapter { } return networkInfo.isConnected(); } - - } else { return cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnected(); } @@ -126,9 +129,13 @@ public class ServerAdapter { } public void getBoards(IResponseCallback<ParsedResponse<List<FullBoard>>> responseCallback) { - RequestHelper.request(provider, () -> - provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId()), responseCallback.getAccount().getBoardsEtag()), - responseCallback); + RequestHelper.request(provider, () -> isEtagsEnabled() + ? provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId()), responseCallback.getAccount().getBoardsEtag()) + : provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId())), responseCallback); + } + + public boolean isEtagsEnabled() { + return sharedPreferences.getBoolean(prefKeyEtags, true); } public void getCapabilities(String eTag, IResponseCallback<ParsedResponse<Capabilities>> responseCallback) { @@ -220,7 +227,13 @@ public class ServerAdapter { public void getCard(long boardId, long stackId, long cardId, IResponseCallback<FullCard> responseCallback) { ensureInternetConnection(); - RequestHelper.request(provider, () -> provider.getDeckAPI().getCard(boardId, stackId, cardId, getLastSyncDateFormatted(responseCallback.getAccount().getId())), responseCallback); + RequestHelper.request(provider, () -> { + final Account account = responseCallback.getAccount(); + if (account != null && account.getServerDeckVersionAsObject().supportsFileAttachments()) { + return provider.getDeckAPI().getCard_1_1(boardId, stackId, cardId, getLastSyncDateFormatted(responseCallback.getAccount().getId())); + } + return provider.getDeckAPI().getCard_1_0(boardId, stackId, cardId, getLastSyncDateFormatted(responseCallback.getAccount().getId())); + }, responseCallback); } public void createCard(long boardId, long stackId, Card card, IResponseCallback<FullCard> responseCallback) { @@ -281,10 +294,14 @@ public class ServerAdapter { } // ## ATTACHMENTS - public void uploadAttachment(Long remoteBoardId, long remoteStackId, long remoteCardId, String contentType, File attachment, IResponseCallback<Attachment> responseCallback) { - ensureInternetConnection(); - MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", attachment.getName(), RequestBody.create(MediaType.parse(getMimeType(attachment)), attachment)); - MultipartBody.Part typePart = MultipartBody.Part.createFormData("type", null, RequestBody.create(MediaType.parse(TEXT_PLAIN), "deck_file")); + public void uploadAttachment(Long remoteBoardId, long remoteStackId, long remoteCardId, File attachment, IResponseCallback<Attachment> responseCallback) { + ensureInternetConnection(); + final Account account = responseCallback.getAccount(); + final String type = (account != null && account.getServerDeckVersionAsObject().supportsFileAttachments()) + ? EAttachmentType.FILE.getValue() + : EAttachmentType.DECK_FILE.getValue(); + final MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", attachment.getName(), RequestBody.create(MediaType.parse(getMimeType(attachment)), attachment)); + final MultipartBody.Part typePart = MultipartBody.Part.createFormData("type", null, RequestBody.create(MediaType.parse(TEXT_PLAIN), type)); RequestHelper.request(provider, () -> provider.getDeckAPI().uploadAttachment(remoteBoardId, remoteStackId, remoteCardId, typePart, filePart), responseCallback); } @@ -304,9 +321,13 @@ public class ServerAdapter { public void updateAttachment(Long remoteBoardId, long remoteStackId, long remoteCardId, long remoteAttachmentId, String contentType, Uri attachmentUri, IResponseCallback<Attachment> responseCallback) { ensureInternetConnection(); - File attachment = new File(attachmentUri.getPath()); - MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", attachment.getName(), RequestBody.create(MediaType.parse(contentType), attachment)); - MultipartBody.Part typePart = MultipartBody.Part.createFormData("type", attachment.getName(), RequestBody.create(MediaType.parse(TEXT_PLAIN), "deck_file")); + final File attachment = new File(attachmentUri.getPath()); + final Account account = responseCallback.getAccount(); + final String type = (account != null && account.getServerDeckVersionAsObject().supportsFileAttachments()) + ? EAttachmentType.FILE.getValue() + : EAttachmentType.DECK_FILE.getValue(); + final MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", attachment.getName(), RequestBody.create(MediaType.parse(contentType), attachment)); + final MultipartBody.Part typePart = MultipartBody.Part.createFormData("type", attachment.getName(), RequestBody.create(MediaType.parse(TEXT_PLAIN), type)); RequestHelper.request(provider, () -> provider.getDeckAPI().updateAttachment(remoteBoardId, remoteStackId, remoteCardId, remoteAttachmentId, typePart, filePart), 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 982844f06..e21d689fe 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 @@ -1,8 +1,11 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db; +import android.appwidget.AppWidgetManager; import android.content.Context; +import android.content.Intent; import androidx.annotation.AnyThread; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; @@ -11,7 +14,15 @@ import androidx.sqlite.db.SimpleSQLiteQuery; import java.time.Instant; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.stream.Collectors; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.AccessControl; @@ -45,23 +56,36 @@ 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.filter.EWidgetType; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetAccount; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetBoard; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetLabel; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetProject; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetSort; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetStack; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetUser; +import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard; 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; public class DataBaseAdapter { - private DeckDatabase db; + private final DeckDatabase db; @NonNull - private Context context; + private final Context context; public DataBaseAdapter(@NonNull Context applicationContext) { + this(applicationContext, DeckDatabase.getInstance(applicationContext)); + } + + private DataBaseAdapter(@NonNull Context applicationContext, @NonNull DeckDatabase db) { this.context = applicationContext; - this.db = DeckDatabase.getInstance(applicationContext); + this.db = db; } @NonNull @@ -169,7 +193,7 @@ public class DataBaseAdapter { } } - private void filterRelationsForCard(@Nullable List<FullCard> card) { + private void filterRelationsForCard(@Nullable Collection<FullCard> card) { if (card == null) { return; } @@ -191,39 +215,51 @@ public class DataBaseAdapter { } - private void fillSqlWithListValues(StringBuilder query, List<Object> args, @NonNull List<? extends IRemoteEntity> entities) { - for (int i = 0; i < entities.size(); i++) { + private void fillSqlWithEntityListValues(StringBuilder query, List<Object> args, @NonNull List<? extends IRemoteEntity> entities) { + List<Long> idList = entities.stream().map(IRemoteEntity::getLocalId).collect(Collectors.toList()); + fillSqlWithListValues(query, args, idList); + } + + private void fillSqlWithListValues(StringBuilder query, List<Object> args, @NonNull List<?> values) { + for (int i = 0; i < values.size(); i++) { if (i > 0) { query.append(", "); } query.append("?"); - args.add(entities.get(i).getLocalId()); + args.add(values.get(i)); } } @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)); + public List<FullCard> getFullCardsForStackDirectly(long accountId, long localStackId, @Nullable FilterInformation filter) { + return filter == null + ? db.getCardDao().getFullCardsForStackDirectly(accountId, localStackId) + : db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, accountId, localStackId)); } @AnyThread private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, long accountId, long localStackId) { + return getQueryForFilter(filter, Collections.singletonList(accountId), Collections.singletonList(localStackId)); + } + + @AnyThread + private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, List<Long> accountIds, List<Long> localStackIds) { List<Object> args = new ArrayList<>(); - args.add(accountId); - args.add(localStackId); - StringBuilder query = new StringBuilder("SELECT * FROM card c " + - "WHERE accountId = ? AND stackId = ? "); + StringBuilder query = new StringBuilder("SELECT * FROM card c WHERE 1=1 "); + if (accountIds != null && !accountIds.isEmpty()) { + query.append("and accountId in ("); + fillSqlWithListValues(query, args, accountIds); + query.append(") "); + } + if (localStackIds != null && !localStackIds.isEmpty()) { + query.append("and stackId in ("); + fillSqlWithListValues(query, args, localStackIds); + query.append(") "); + } if (!filter.getLabels().isEmpty()) { query.append("and (exists(select 1 from joincardwithlabel j where c.localId = cardId and labelId in ("); - fillSqlWithListValues(query, args, filter.getLabels()); + fillSqlWithEntityListValues(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)) "); @@ -236,7 +272,7 @@ public class DataBaseAdapter { if (!filter.getUsers().isEmpty()) { query.append("and (exists(select 1 from joincardwithuser j where c.localId = cardId and userId in ("); - fillSqlWithListValues(query, args, filter.getUsers()); + fillSqlWithEntityListValues(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)) "); @@ -247,6 +283,19 @@ public class DataBaseAdapter { query.append("and not exists(select 1 from joincardwithuser j where c.localId = cardId and j.status<>3) "); } + if (!filter.getProjects().isEmpty()) { + query.append("and (exists(select 1 from joincardwithproject j where c.localId = cardId and projectId in ("); + fillSqlWithEntityListValues(query, args, filter.getProjects()); + query.append(") and j.status<>3) "); + if (filter.isNoAssignedProject()) { + query.append("or not exists(select 1 from joincardwithproject j where c.localId = cardId and j.status<>3)) "); + } else { + query.append(") "); + } + } else if (filter.isNoAssignedProject()) { + query.append("and not exists(select 1 from joincardwithproject j where c.localId = cardId and j.status<>3) "); + } + if (filter.getDueType() != EDueType.NO_FILTER) { switch (filter.getDueType()) { case NO_DUE: @@ -265,13 +314,13 @@ public class DataBaseAdapter { query.append("and datetime(c.duedate/1000, 'unixepoch', 'localtime') between datetime('now', 'localtime') and datetime('now', '+30 day', 'localtime')"); break; default: - throw new IllegalArgumentException("Xou need to add your new EDueType value\"" + filter.getDueType() + "\" here!"); + throw new IllegalArgumentException("You need to add your new EDueType value\"" + filter.getDueType() + "\" here!"); } } if (filter.getArchiveStatus() != FilterInformation.EArchiveStatus.ALL) { query.append(" and c.archived = " + (filter.getArchiveStatus() == FilterInformation.EArchiveStatus.ARCHIVED ? 1 : 0)); } - query.append(" and status<>3 order by `order`, createdAt asc;"); + query.append(" and status<>3 order by accountId asc, stackId asc, `order`, createdAt asc;"); return new SimpleSQLiteQuery(query.toString(), args.toArray()); } @@ -283,7 +332,25 @@ public class DataBaseAdapter { @WorkerThread public long createUser(long accountId, User user) { user.setAccountId(accountId); - return db.getUserDao().insert(user); + long newId = db.getUserDao().insert(user); + new Thread(() -> { + Account account = db.getAccountDao().getAccountByIdDirectly(accountId); + if (account.getUserName().equals(user.getUid())) { + for (FilterWidget widget : getFilterWidgetsByType(EWidgetType.UPCOMING_WIDGET)) { + for (FilterWidgetAccount widgetAccount : widget.getAccounts()) { + if (widgetAccount.getAccountId() == accountId && widgetAccount.getUsers().isEmpty()) { + FilterWidgetUser u = new FilterWidgetUser(); + u.setFilterAccountId(widgetAccount.getId()); + u.setUserId(newId); + widgetAccount.getUsers().add(u); + updateFilterWidgetDirectly(widget); + } + } + } + } + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.USER, newId); + }).start(); + return newId; } @WorkerThread @@ -291,6 +358,7 @@ public class DataBaseAdapter { markAsEditedIfNeeded(user, setStatus); user.setAccountId(accountId); db.getUserDao().update(user); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.USER, user.getLocalId()); } @AnyThread @@ -306,7 +374,9 @@ public class DataBaseAdapter { @WorkerThread public long createLabelDirectly(long accountId, @NonNull Label label) { label.setAccountId(accountId); - return db.getLabelDao().insert(label); + long newId = db.getLabelDao().insert(label); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.LABEL, newId); + return newId; } public void createJoinCardWithLabel(long localLabelId, long localCardId) { @@ -319,15 +389,15 @@ public class DataBaseAdapter { // readded! existing.setStatusEnum(DBStatus.LOCAL_EDITED); db.getJoinCardWithLabelDao().update(existing); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.LABEL, existing.getLabelId()); } else { JoinCardWithLabel join = new JoinCardWithLabel(); join.setCardId(localCardId); join.setLabelId(localLabelId); join.setStatus(status.getId()); db.getJoinCardWithLabelDao().insert(join); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.LABEL, join.getLabelId()); } - - } public void deleteJoinedLabelsForCard(long localCardId) { @@ -336,10 +406,12 @@ public class DataBaseAdapter { public void deleteJoinedLabelForCard(long localCardId, long localLabelId) { db.getJoinCardWithLabelDao().setDbStatus(localCardId, localLabelId, DBStatus.LOCAL_DELETED.getId()); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.LABEL, localLabelId); } public void deleteJoinedUserForCard(long localCardId, long localUserId) { db.getJoinCardWithUserDao().setDbStatus(localCardId, localUserId, DBStatus.LOCAL_DELETED.getId()); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.USER, localUserId); } public void deleteJoinedLabelForCardPhysically(long localCardId, long localLabelId) { @@ -360,6 +432,7 @@ public class DataBaseAdapter { // readded! existing.setStatusEnum(DBStatus.LOCAL_EDITED); db.getJoinCardWithUserDao().update(existing); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.USER, localUserId); } else if (existing != null) { return; } else { @@ -368,6 +441,7 @@ public class DataBaseAdapter { join.setUserId(localUserId); join.setStatus(status.getId()); db.getJoinCardWithUserDao().insert(join); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.USER, localUserId); } } @@ -411,11 +485,13 @@ public class DataBaseAdapter { public void updateLabel(Label label, boolean setStatus) { markAsEditedIfNeeded(label, setStatus); db.getLabelDao().update(label); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.LABEL, label.getLocalId()); } public void deleteLabel(Label label, boolean setStatus) { markAsDeletedIfNeeded(label, setStatus); db.getLabelDao().update(label); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.LABEL, label.getLocalId()); } public void deleteLabelPhysically(Label label) { @@ -425,12 +501,23 @@ public class DataBaseAdapter { public WrappedLiveData<Account> createAccount(Account account) { return LiveDataHelper.wrapInLiveData(() -> { long id = db.getAccountDao().insert(account); + + new Thread(() -> { + DeckLog.verbose("Adding new created", Account.class.getSimpleName(), " with ", id, " to all instances of ", EWidgetType.UPCOMING_WIDGET.name()); + for (FilterWidget widget : getFilterWidgetsByType(EWidgetType.UPCOMING_WIDGET)) { + widget.getAccounts().add(new FilterWidgetAccount(id, false)); + updateFilterWidgetDirectly(widget); + } + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.ACCOUNT, id); + }).start(); return readAccountDirectly(id); }); } public void deleteAccount(long id) { db.getAccountDao().deleteById(id); + notifyAllWidgets(); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.ACCOUNT, id); } public void updateAccount(Account account) { @@ -474,7 +561,8 @@ public class DataBaseAdapter { return LiveDataHelper.wrapInLiveData(() -> { board.setAccountId(accountId); long id = db.getBoardDao().insert(board); - return db.getBoardDao().getBoardByIdDirectly(id); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.BOARD, id); + return db.getBoardDao().getBoardByLocalIdDirectly(id); }); } @@ -482,21 +570,27 @@ public class DataBaseAdapter { @WorkerThread public long createBoardDirectly(long accountId, @NonNull Board board) { board.setAccountId(accountId); - return db.getBoardDao().insert(board); + long id = db.getBoardDao().insert(board); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.BOARD, id); + return id; } public void deleteBoard(Board board, boolean setStatus) { markAsDeletedIfNeeded(board, setStatus); db.getBoardDao().update(board); + notifyAllWidgets(); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.BOARD, board.getLocalId()); } public void deleteBoardPhysically(Board board) { db.getBoardDao().delete(board); + notifyAllWidgets(); } public void updateBoard(Board board, boolean setStatus) { markAsEditedIfNeeded(board, setStatus); db.getBoardDao().update(board); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.BOARD, board.getLocalId()); } public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { @@ -516,28 +610,35 @@ public class DataBaseAdapter { @WorkerThread public long createStack(long accountId, Stack stack) { stack.setAccountId(accountId); - return db.getStackDao().insert(stack); + long id = db.getStackDao().insert(stack); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.STACK, id); + return id; } @WorkerThread public void deleteStack(Stack stack, boolean setStatus) { markAsDeletedIfNeeded(stack, setStatus); db.getStackDao().update(stack); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.STACK, stack.getLocalId()); + notifyAllWidgets(); } @WorkerThread public void deleteStackPhysically(Stack stack) { db.getStackDao().delete(stack); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.STACK, stack.getLocalId()); + notifyAllWidgets(); } @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); - } + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.STACK, stack.getLocalId()); +// if (db.getStackWidgetModelDao().containsStackLocalId(stack.getLocalId())) { +// DeckLog.info("Notifying " + StackWidget.class.getSimpleName() + " about card changes for \"" + stack.getTitle() + "\""); +// // FIXME StackWidget.notifyDatasetChanged(context); +// } } @WorkerThread @@ -571,6 +672,7 @@ public class DataBaseAdapter { long newCardId = db.getCardDao().insert(card); notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId()); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.STACK, card.getStackId()); return newCardId; } @@ -595,6 +697,7 @@ public class DataBaseAdapter { } notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId()); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.STACK, card.getStackId()); } @WorkerThread @@ -608,22 +711,24 @@ public class DataBaseAdapter { Long originalStackLocalId = db.getCardDao().getLocalStackIdByLocalCardId(card.getLocalId()); db.getCardDao().update(card); if (db.getSingleCardWidgetModelDao().containsCardLocalId(card.getLocalId())) { - DeckLog.info("Notifying " + SingleCardWidget.class.getSimpleName() + " about card changes for \"" + card.getTitle() + "\""); + DeckLog.info("Notifying", SingleCardWidget.class.getSimpleName(), "about card changes for", card.getTitle()); SingleCardWidget.notifyDatasetChanged(context); } + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.STACK, originalStackLocalId); 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); - } +// if (db.getStackWidgetModelDao().containsStackLocalId(affectedStackIds)) { +// DeckLog.info("Notifying " + StackWidget.class.getSimpleName() + " about card changes for \"" + cardTitle + "\""); +// // FIXME StackWidget.notifyDatasetChanged(context); +// } } @WorkerThread public long createAccessControl(long accountId, @NonNull AccessControl entity) { entity.setAccountId(accountId); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.ACCOUNT, accountId); return db.getAccessControlDao().insert(entity); } @@ -642,6 +747,7 @@ public class DataBaseAdapter { public void updateAccessControl(AccessControl entity, boolean setStatus) { markAsEditedIfNeeded(entity, setStatus); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.ACCOUNT, entity.getAccountId()); db.getAccessControlDao().update(entity); } @@ -652,6 +758,7 @@ public class DataBaseAdapter { } else { db.getAccessControlDao().delete(entity); } + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.ACCOUNT, entity.getAccountId()); } public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { @@ -660,7 +767,7 @@ public class DataBaseAdapter { @WorkerThread public Board getBoardByLocalIdDirectly(long localId) { - return db.getBoardDao().getBoardByIdDirectly(localId); + return db.getBoardDao().getBoardByLocalIdDirectly(localId); } public LiveData<User> getUserByLocalId(long accountId, long localId) { @@ -818,6 +925,11 @@ public class DataBaseAdapter { } @WorkerThread + public Long getBoardLocalIdByLocalCardIdDirectly(long localCardId) { + return db.getBoardDao().getBoardLocalIdByLocalCardIdDirectly(localCardId); + } + + @WorkerThread public FullBoard getFullBoardByLocalCardIdDirectly(long localCardId) { return db.getBoardDao().getFullBoardByLocalCardIdDirectly(localCardId); } @@ -1038,24 +1150,224 @@ public class DataBaseAdapter { db.getSingleCardWidgetModelDao().delete(model); } - public long createStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { + public void 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); +// db.getStackWidgetModelDao().insert(model); } public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) { - return db.getStackWidgetModelDao().getStackWidgetByAppWidgetIdDirectly(appWidgetId); +// return db.getStackWidgetModelDao().getStackWidgetByAppWidgetIdDirectly(appWidgetId); + return null; + } + + public int createFilterWidgetDirectly(@NonNull FilterWidget filterWidget) { + db.getFilterWidgetDao().insert(filterWidget); + insertFilterWidgetDecendants(filterWidget); + return filterWidget.getId(); + } + + private void insertFilterWidgetDecendants(FilterWidget filterWidget) { + long widgetId = filterWidget.getId(); + for (FilterWidgetAccount account : filterWidget.getAccounts()) { + account.setFilterWidgetId(widgetId); + long accountId = db.getFilterWidgetAccountDao().insert(account); + for (FilterWidgetUser user : account.getUsers()) { + user.setFilterAccountId(accountId); + db.getFilterWidgetUserDao().insert(user); + } + for (FilterWidgetProject project : account.getProjects()) { + project.setFilterAccountId(accountId); + db.getFilterWidgetProjectDao().insert(project); + } + for (FilterWidgetBoard board : account.getBoards()) { + board.setFilterAccountId(accountId); + long boardId = db.getFilterWidgetBoardDao().insert(board); + for (FilterWidgetStack stack : board.getStacks()) { + stack.setFilterBoardId(boardId); + db.getFilterWidgetStackDao().insert(stack); + } + for (FilterWidgetLabel label : board.getLabels()) { + label.setFilterBoardId(boardId); + db.getFilterWidgetLabelDao().insert(label); + } + } + } + for (FilterWidgetSort sort : filterWidget.getSorts()) { + sort.setFilterWidgetId(widgetId); + db.getFilterWidgetSortDao().insert(sort); + } + } + + public void deleteFilterWidgetDirectly(Integer filterWidgetId) { + db.getFilterWidgetDao().delete(filterWidgetId); + } + + public void updateFilterWidgetDirectly(FilterWidget filterWidget) { + db.getFilterWidgetSortDao().deleteByFilterWidgetId(filterWidget.getId()); + db.getFilterWidgetAccountDao().deleteByFilterWidgetId(filterWidget.getId()); + db.getFilterWidgetDao().update(filterWidget); + insertFilterWidgetDecendants(filterWidget); + } + + public FilterWidget getFilterWidgetByIdDirectly(Integer filterWidgetId) { + FilterWidget filterWidget = db.getFilterWidgetDao().getFilterWidgetByIdDirectly(filterWidgetId); + if (filterWidget == null) { + throw new NoSuchElementException("No widget with id " + filterWidgetId + " configured."); + } + filterWidget.setSorts(db.getFilterWidgetSortDao().getFilterWidgetSortByFilterWidgetIdDirectly(filterWidgetId)); + filterWidget.setAccounts(db.getFilterWidgetAccountDao().getFilterWidgetAccountsByFilterWidgetIdDirectly(filterWidgetId)); + for (FilterWidgetAccount account : filterWidget.getAccounts()) { + account.setBoards(db.getFilterWidgetBoardDao().getFilterWidgetBoardsByFilterWidgetAccountIdDirectly(account.getId())); + account.setUsers(db.getFilterWidgetUserDao().getFilterWidgetUsersByFilterWidgetAccountIdDirectly(account.getId())); + account.setProjects(db.getFilterWidgetProjectDao().getFilterWidgetProjectsByFilterWidgetAccountIdDirectly(account.getId())); + for (FilterWidgetBoard board : account.getBoards()) { + board.setLabels(db.getFilterWidgetLabelDao().getFilterWidgetLabelsByFilterWidgetBoardIdDirectly(board.getId())); + board.setStacks(db.getFilterWidgetStackDao().getFilterWidgetStacksByFilterWidgetBoardIdDirectly(board.getId())); + } + } + + return filterWidget; + } + + public void notifyFilterWidgetsAboutChangedEntity(@NonNull FilterWidget.EChangedEntityType type, Long entityId) { + new Thread(() -> { + final List<EWidgetType> widgetTypesToNotify = db.getFilterWidgetDao().getChangedListTypesByEntity(type.toString(), entityId); + for (EWidgetType t : widgetTypesToNotify) { + DeckLog.info("Notifying", t.getWidgetClass().getSimpleName(), "about entity change:", type.name(), "with ID", entityId); + context.sendBroadcast(new Intent(context, t.getWidgetClass()).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); + } + }).start(); + } + + public List<FilterWidgetCard> getCardsForFilterWidget(Integer filterWidgetId) { + FilterWidget filterWidget = getFilterWidgetByIdDirectly(filterWidgetId); + FilterInformation filter = new FilterInformation(); + Set<FullCard> cardsResult = new HashSet<>(); + if (filterWidget.getDueType() != null) { + filter.setDueType(filterWidget.getDueType()); + } else filter.setDueType(EDueType.NO_FILTER); + + if (filterWidget.getAccounts().isEmpty()) { + cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, null, null))); + } else { + for (FilterWidgetAccount account : filterWidget.getAccounts()) { + filter.setNoAssignedUser(account.isIncludeNoUser()); + List<User> users = new ArrayList<>(); + if (!account.getUsers().isEmpty()) { + for (FilterWidgetUser user : account.getUsers()) { + User u = new User(); + u.setLocalId(user.getUserId()); + users.add(u); + } + } + filter.setUsers(users); + filter.setNoAssignedProject(account.isIncludeNoProject()); + List<OcsProject> projects = new ArrayList<>(); + if (!account.getProjects().isEmpty()) { + for (FilterWidgetProject project : account.getProjects()) { + OcsProject u = new OcsProject(); + u.setLocalId(project.getProjectId()); + projects.add(u); + } + } + filter.setProjects(projects); + if (!account.getBoards().isEmpty()) { + for (FilterWidgetBoard board : account.getBoards()) { + filter.setNoAssignedLabel(board.isIncludeNoLabel()); + List<Long> stacks; + for (FilterWidgetLabel label : board.getLabels()) { + Label l = new Label(); + l.setLocalId(label.getLabelId()); + filter.addLabel(l); + } + if (board.getStacks().isEmpty()) { + stacks = db.getStackDao().getLocalStackIdsByLocalBoardIdDirectly(board.getBoardId()); + } else { + stacks = new ArrayList<>(); + for (FilterWidgetStack stack : board.getStacks()) { + stacks.add(stack.getStackId()); + } + } + cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, Collections.singletonList(account.getAccountId()), stacks))); + } + } else { + cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, Collections.singletonList(account.getAccountId()), null))); + } + } + } + + handleWidgetTypeExtras(filterWidget, cardsResult); + + filterRelationsForCard(cardsResult); + + List<FilterWidgetCard> result = new ArrayList<>(cardsResult.size()); + Map<Long, Board> boardCache = new HashMap<>(); + Map<Long, Stack> stackCache = new HashMap<>(); + for (FullCard fullCard : cardsResult) { + Long stackId = fullCard.getCard().getStackId(); + Stack stack = stackCache.get(stackId); + if (stack == null) { + stack = db.getStackDao().getStackByLocalIdDirectly(stackId); + stackCache.put(stackId, stack); + } + + Board board = boardCache.get(stack.getBoardId()); + if (board == null) { + board = db.getBoardDao().getBoardByLocalIdDirectly(stackId); + boardCache.put(stackId, board); + } + result.add(new FilterWidgetCard(fullCard, stack, board)); + } + return result; + } + + private void handleWidgetTypeExtras(FilterWidget filterWidget, Collection<FullCard> cardsResult) { + if (filterWidget.getWidgetType() == EWidgetType.UPCOMING_WIDGET) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/819 "no due" cards are only shown if they are on a shared board + for (FullCard fullCard : new ArrayList<>(cardsResult)) { + if (fullCard.getCard().getDueDate() == null && !db.getStackDao().isStackOnSharedBoardDirectly(fullCard.getCard().getStackId())) { + cardsResult.remove(fullCard); + } + } + List<Long> accountIds = null; + if (!filterWidget.getAccounts().isEmpty()) { + accountIds = filterWidget.getAccounts().stream().map(FilterWidgetAccount::getAccountId).collect(Collectors.toList()); + } + // https://github.com/stefan-niedermann/nextcloud-deck/issues/822 exclude archived cards and boards + List<Long> archivedStacks = db.getStackDao().getLocalStackIdsInArchivedBoardsByAccountIdsDirectly(accountIds); + for (Long archivedStack : archivedStacks) { + List<FullCard> archivedCards = cardsResult.stream() + .filter(c -> c.getCard().isArchived() || archivedStack.equals(c.getCard().getStackId())) + .collect(Collectors.toList()); + cardsResult.removeAll(archivedCards); + } + // https://github.com/stefan-niedermann/nextcloud-deck/issues/800 all cards within non-shared boards need to be included + cardsResult.addAll(db.getCardDao().getFullCardsForNonSharedBoardsWithDueDateForUpcomingCardsWidgetDirectly(accountIds)); + } + } + + public List<FilterWidget> getFilterWidgetsByType(EWidgetType type) { + List<Integer> ids = db.getFilterWidgetDao().getFilterWidgetIdsByType(type.getId()); + List<FilterWidget> widgets = new ArrayList<>(ids.size()); + for (Integer id : ids) { + widgets.add(getFilterWidgetByIdDirectly(id)); + } + return widgets; + } + + public boolean filterWidgetExists(int id) { + return db.getFilterWidgetDao().filterWidgetExists(id); } public void deleteStackWidget(int appWidgetId) { StackWidgetModel model = new StackWidgetModel(); model.setAppWidgetId(appWidgetId); - db.getStackWidgetModelDao().delete(model); +// db.getStackWidgetModelDao().delete(model); } public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) { @@ -1119,8 +1431,20 @@ public class DataBaseAdapter { assignment.setStatus(DBStatus.UP_TO_DATE.getId()); assignment.setCardId(card.getLocalId()); assignment.setProjectId(localProjectId); - db.getJoinCardWithOcsProjectDao().insert(assignment); + long id = db.getJoinCardWithOcsProjectDao().insert(assignment); + notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.PROJECT, id); } } } + + private void notifyAllWidgets() { + SingleCardWidget.notifyDatasetChanged(context); + /// FIXME StackWidget.notifyDatasetChanged(context); +// UpcomingWidget.notifyDatasetChanged(context); + } + + @ColorInt + public Integer getBoardColorDirectly(long accountId, long localBoardId) { + return db.getBoardDao().getBoardColorByLocalIdDirectly(accountId, localBoardId); + } } 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 a491946ed..6f7ba45e0 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,21 +1,14 @@ 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; import androidx.room.Room; import androidx.room.RoomDatabase; 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; @@ -32,8 +25,6 @@ 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; @@ -42,8 +33,17 @@ 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.filter.FilterWidget; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetAccount; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetBoard; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetLabel; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetProject; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetSort; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetStack; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetUser; 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.converter.DateTypeConverter; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.converter.EnumConverter; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.AccessControlDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.AccountDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.ActivityDao; @@ -67,7 +67,36 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.Jo 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; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter.FilterWidgetAccountDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter.FilterWidgetBoardDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter.FilterWidgetDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter.FilterWidgetLabelDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter.FilterWidgetProjectDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter.FilterWidgetSortDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter.FilterWidgetStackDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter.FilterWidgetUserDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_10_11; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_11_12; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_12_13; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_13_14; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_14_15; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_15_16; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_16_17; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_17_18; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_18_19; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_19_20; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_20_21; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_21_22; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_22_23; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_23_24; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_24_25; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_25_26; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_26_27; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_27_28; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_28_29; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_29_30; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_8_9; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_9_10; @Database( entities = { @@ -89,316 +118,34 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.Sta DeckComment.class, Mention.class, SingleCardWidgetModel.class, - StackWidgetModel.class, OcsProject.class, OcsProjectResource.class, JoinCardWithProject.class, UserInGroup.class, UserInBoard.class, + FilterWidget.class, + FilterWidgetAccount.class, + FilterWidgetBoard.class, + FilterWidgetStack.class, + FilterWidgetLabel.class, + FilterWidgetUser.class, + FilterWidgetProject.class, + FilterWidgetSort.class, }, exportSchema = false, - version = 23 + version = 30 ) -@TypeConverters({DateTypeConverter.class}) +@TypeConverters({DateTypeConverter.class, EnumConverter.class}) public abstract class DeckDatabase extends RoomDatabase { - private static final String DECK_DB_NAME = "NC_DECK_DB.db"; private static volatile DeckDatabase instance; - private static final Migration MIGRATION_8_9 = new Migration(8, 9) { - @Override - public void migrate(SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE `DeckComment` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `status` INTEGER NOT NULL, `lastModified` INTEGER, `lastModifiedLocal` INTEGER, `objectId` INTEGER, `actorType` TEXT, `creationDateTime` INTEGER, `actorId` TEXT, `actorDisplayName` TEXT, `message` TEXT, FOREIGN KEY(`objectId`) REFERENCES `Card`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); - database.execSQL("CREATE TABLE `Mention` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `commentId` INTEGER, `mentionId` TEXT, `mentionType` TEXT, `mentionDisplayName` TEXT, FOREIGN KEY(`commentId`) REFERENCES `DeckComment`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); - database.execSQL("CREATE INDEX `index_DeckComment_accountId` ON `DeckComment` (`accountId`)"); - database.execSQL("CREATE INDEX `comment_accID` ON `DeckComment` (`accountId`)"); - database.execSQL("CREATE UNIQUE INDEX `index_DeckComment_accountId_id` ON `DeckComment` (`accountId`, `id`)"); - database.execSQL("CREATE INDEX `index_DeckComment_id` ON `DeckComment` (`id`)"); - database.execSQL("CREATE INDEX `index_DeckComment_lastModifiedLocal` ON `DeckComment` (`lastModifiedLocal`)"); - database.execSQL("CREATE INDEX `index_DeckComment_objectId` ON `DeckComment` (`objectId`)"); - database.execSQL("CREATE INDEX `index_Mention_commentId` ON `Mention` (`commentId`)"); - } - }; - - private static final Migration MIGRATION_9_10 = new Migration(9, 10) { - @Override - public void migrate(SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `Account` ADD `color` TEXT NOT NULL DEFAULT '#0082c9'"); - database.execSQL("ALTER TABLE `Account` ADD `textColor` TEXT NOT NULL DEFAULT '#ffffff'"); - database.execSQL("ALTER TABLE `Account` ADD `serverDeckVersion` TEXT NOT NULL DEFAULT '0.6.4'"); - database.execSQL("ALTER TABLE `Account` ADD `maintenanceEnabled` INTEGER NOT NULL DEFAULT 0"); - } - }; - - private static final Migration MIGRATION_10_11 = new Migration(10, 11) { - @Override - public void migrate(SupportSQLiteDatabase database) { - // replace duplicates with the server-known ones - Cursor duplucatesCursor = database.query("SELECT boardId, title, count(*) FROM Label group by boardid, title having count(*) > 1"); - if (duplucatesCursor != null && duplucatesCursor.moveToFirst()) { - do { - long boardId = duplucatesCursor.getLong(0); - String title = duplucatesCursor.getString(1); - Cursor singleDuplicateCursor = database.query("select localId from Label where boardId = ? and title = ? order by id desc", new Object[]{boardId, title}); - if (singleDuplicateCursor != null && singleDuplicateCursor.moveToFirst()) { - long idToUse = -1; - do { - if (idToUse < 0) { - // desc order -> first one is the one with remote ID or a random one. keep this one. - idToUse = singleDuplicateCursor.getLong(0); - continue; - } - long idToReplace = singleDuplicateCursor.getLong(0); - Cursor cardsAssignedToDuplicateCursor = database.query("select cardId, exists(select 1 from JoinCardWithLabel ij where ij.labelId = ? and ij.cardId = cardId) " + - "from JoinCardWithLabel where labelId = ?", new Object[]{idToUse, idToReplace}); - if (cardsAssignedToDuplicateCursor != null && cardsAssignedToDuplicateCursor.moveToFirst()) { - do { - long cardId = cardsAssignedToDuplicateCursor.getLong(0); - boolean hasDestinationLabelAssigned = cardsAssignedToDuplicateCursor.getInt(1) > 0; - database.execSQL("DELETE FROM JoinCardWithLabel where labelId = ? and cardId = ?", new Object[]{idToReplace, cardId}); - - if (!hasDestinationLabelAssigned) { - database.execSQL("INSERT INTO JoinCardWithLabel (status,labelId,cardId) VALUES (?, ?, ?)", new Object[]{DBStatus.LOCAL_EDITED.getId(), idToUse, cardId}); - } - } while (cardsAssignedToDuplicateCursor.moveToNext()); - } - database.execSQL("DELETE FROM Label where localId = ?", new Object[]{idToReplace}); - } while (singleDuplicateCursor.moveToNext()); - } - } while (duplucatesCursor.moveToNext()); - } -// database.execSQL("DELETE FROM Label WHERE id IS NULL AND EXISTS(SELECT 1 FROM Label il WHERE il.boardId = boardId AND il.title = title AND id IS NOT NULL)"); - database.execSQL("CREATE UNIQUE INDEX idx_label_title_unique ON Label(boardId, title)"); - } - }; - - private static final Migration MIGRATION_11_12 = new Migration(11, 12) { - @Override - public void migrate(SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE `SingleCardWidgetModel` (`widgetId` INTEGER PRIMARY KEY, `accountId` INTEGER, `boardId` INTEGER, `cardId` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`cardId`) REFERENCES `Card`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); - database.execSQL("CREATE INDEX `index_SingleCardWidgetModel_cardId` ON `SingleCardWidgetModel` (`cardId`)"); - } - }; - - private static final Migration MIGRATION_12_13 = new Migration(12, 13) { - @Override - public void migrate(SupportSQLiteDatabase database) { - database.execSQL("CREATE INDEX `idx_cardWidgetModel_accountId` ON `SingleCardWidgetModel` (`accountId`)"); - database.execSQL("CREATE INDEX `idx_cardWidgetModel_boardId` ON `SingleCardWidgetModel` (`boardId`)"); - } - }; - - private static final Migration MIGRATION_13_14 = new Migration(13, 14) { - @Override - public void migrate(SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `DeckComment` ADD `parentId` INTEGER REFERENCES DeckComment(localId) ON DELETE CASCADE"); - database.execSQL("CREATE INDEX `idx_comment_parentID` ON DeckComment(parentId)"); - } - }; - - 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); - DeckLog.log("onCreate triggered!!!"); + DeckLog.info("Database", DECK_DB_NAME, "created."); LastSyncUtil.resetAll(); } }; @@ -415,45 +162,28 @@ public abstract class DeckDatabase extends RoomDatabase { context, DeckDatabase.class, DECK_DB_NAME) - .addMigrations(MIGRATION_8_9) - .addMigrations(MIGRATION_9_10) - .addMigrations(MIGRATION_10_11) - .addMigrations(MIGRATION_11_12) - .addMigrations(MIGRATION_12_13) - .addMigrations(MIGRATION_13_14) - .addMigrations(new Migration(14, 15) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - // https://github.com/stefan-niedermann/nextcloud-deck/issues/570 - SyncWorker.update(context); - // https://github.com/stefan-niedermann/nextcloud-deck/issues/525 - PreferenceManager - .getDefaultSharedPreferences(context) - .edit() - .remove("it.niedermann.nextcloud.deck.theme_text") - .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) + .addMigrations(new Migration_8_9()) + .addMigrations(new Migration_9_10()) + .addMigrations(new Migration_10_11()) + .addMigrations(new Migration_11_12()) + .addMigrations(new Migration_12_13()) + .addMigrations(new Migration_13_14()) + .addMigrations(new Migration_14_15(context)) + .addMigrations(new Migration_15_16()) + .addMigrations(new Migration_16_17()) + .addMigrations(new Migration_17_18()) + .addMigrations(new Migration_18_19()) + .addMigrations(new Migration_19_20()) + .addMigrations(new Migration_20_21()) + .addMigrations(new Migration_21_22(context)) + .addMigrations(new Migration_22_23()) + .addMigrations(new Migration_23_24(context)) + .addMigrations(new Migration_24_25()) + .addMigrations(new Migration_25_26()) + .addMigrations(new Migration_26_27()) + .addMigrations(new Migration_27_28()) + .addMigrations(new Migration_28_29()) + .addMigrations(new Migration_29_30(context)) .fallbackToDestructiveMigration() .addCallback(ON_CREATE_CALLBACK) .build(); @@ -495,8 +225,6 @@ public abstract class DeckDatabase extends RoomDatabase { public abstract SingleCardWidgetModelDao getSingleCardWidgetModelDao(); - public abstract StackWidgetModelDao getStackWidgetModelDao(); - public abstract OcsProjectDao getOcsProjectDao(); public abstract OcsProjectResourceDao getOcsProjectResourceDao(); @@ -506,4 +234,21 @@ public abstract class DeckDatabase extends RoomDatabase { public abstract UserInGroupDao getUserInGroupDao(); public abstract UserInBoardDao getUserInBoardDao(); + + public abstract FilterWidgetDao getFilterWidgetDao(); + + public abstract FilterWidgetAccountDao getFilterWidgetAccountDao(); + + public abstract FilterWidgetBoardDao getFilterWidgetBoardDao(); + + public abstract FilterWidgetStackDao getFilterWidgetStackDao(); + + public abstract FilterWidgetLabelDao getFilterWidgetLabelDao(); + + public abstract FilterWidgetUserDao getFilterWidgetUserDao(); + + public abstract FilterWidgetProjectDao getFilterWidgetProjectDao(); + + public abstract FilterWidgetSortDao getFilterWidgetSortDao(); + }
\ No newline at end of file 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/converter/DateTypeConverter.java index f6811e033..a39a4a665 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/converter/DateTypeConverter.java @@ -1,4 +1,4 @@ -package it.niedermann.nextcloud.deck.persistence.sync.adapters.db; +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.converter; import androidx.room.TypeConverter; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/converter/EnumConverter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/converter/EnumConverter.java new file mode 100644 index 000000000..2cafaa8fe --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/converter/EnumConverter.java @@ -0,0 +1,67 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.converter; + +import androidx.annotation.Nullable; +import androidx.room.TypeConverter; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.model.enums.EAttachmentType; +import it.niedermann.nextcloud.deck.model.enums.EDueType; +import it.niedermann.nextcloud.deck.model.enums.ESortCriteria; +import it.niedermann.nextcloud.deck.model.widget.filter.EWidgetType; + +public class EnumConverter { + // #### EWidgetType + @TypeConverter + public static EWidgetType toWidgetTypeEnum(Integer value) { + try { + return value == null ? null : EWidgetType.findById(value); + } catch (IllegalArgumentException e) { + DeckLog.error(EWidgetType.class.getSimpleName(), value, "not found. Falling back to generic", EWidgetType.FILTER_WIDGET); + return EWidgetType.FILTER_WIDGET; + } + } + + @TypeConverter + public static Integer fromWidgetTypeEnum(EWidgetType value) { + return value == null ? null : value.getId(); + } + + // #### EDueType + @TypeConverter + @Nullable + public static EDueType toDueTypeEnum(@Nullable Integer value) { + return value == null ? null : EDueType.findById(value); + } + + @TypeConverter + @Nullable + public static Integer fromDueTypeEnum(@Nullable EDueType value) { + return value == null ? null : value.getId(); + } + + // #### ESortCriteria + @TypeConverter + @Nullable + public static ESortCriteria toSortCriteriaEnum(@Nullable Integer value) { + return value == null ? null : ESortCriteria.findById(value); + } + + @TypeConverter + @Nullable + public static Integer fromSortCriteriaEnum(@Nullable ESortCriteria value) { + return value == null ? null : value.getId(); + } + + // #### EAttachmentType + @TypeConverter + @Nullable + public static EAttachmentType toEAttachmentType(@Nullable String value) { + return value == null ? null : EAttachmentType.findByValue(value); + } + + @TypeConverter + @Nullable + public static String fromEAttachmentType(@Nullable EAttachmentType value) { + return value == null ? null : value.getValue(); + } +} 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 6cb322aa3..89ada6e18 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 @@ -33,7 +33,7 @@ public interface BoardDao extends GenericDao<Board> { Board getBoardByRemoteIdDirectly(long accountId, long remoteId); @Query("SELECT * FROM board WHERE localId = :localId") - Board getBoardByIdDirectly(long localId); + Board getBoardByLocalIdDirectly(long localId); @Transaction @Query("SELECT * FROM board WHERE accountId = :accountId and id = :remoteId") @@ -51,10 +51,12 @@ public interface BoardDao extends GenericDao<Board> { @Query("SELECT * FROM board WHERE accountId = :accountId and localId = :localId") LiveData<FullBoard> getFullBoardById(final long accountId, final long localId); - - @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId") + @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON s.localId = c.stackId where c.localId = :localCardId") Board getBoardByLocalCardIdDirectly(long localCardId); + @Query("SELECT b.localId FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON s.localId = c.stackId where c.localId = :localCardId") + Long getBoardLocalIdByLocalCardIdDirectly(long 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); @@ -66,7 +68,6 @@ public interface BoardDao extends GenericDao<Board> { @Query("SELECT * FROM board WHERE accountId = :accountId and archived = 0 and permissionEdit = 1 and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") LiveData<List<Board>> getBoardsWithEditPermissionsForAccount(long accountId); - @Query("SELECT b.localId " + "FROM card c " + "inner join stack s on s.localId = c.stackId " + @@ -79,4 +80,7 @@ public interface BoardDao extends GenericDao<Board> { @Query("SELECT * FROM board WHERE accountId = :accountId and title = :title") Board getBoardForAccountByNameDirectly(long accountId, String title); + + @Query("SELECT b.color FROM board b where b.localId = :localBoardId and b.accountId = :accountId") + Integer getBoardColorByLocalIdDirectly(long accountId, long localBoardId); }
\ 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 82bdf1b8d..51fbc0d89 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 @@ -85,4 +85,17 @@ public interface CardDao extends GenericDao<Card> { @Query("SELECT c.stackId FROM card c WHERE localId = :localCardId") Long getLocalStackIdByLocalCardId(Long localCardId); + + @Transaction + @Query("SELECT * FROM card c WHERE " + + "exists(select 1 from Stack s join Board b on s.boardId = b.localId where s.localId = c.stackId " + + "and b.archived = 0 " + + "and not exists(select 1 from AccessControl ac where ac.boardId = b.localId and status <> 3)) " + + "and dueDate is not null " + + "and (coalesce(:accountIds, null) is null or accountId in (:accountIds)) " + + "and status <> 3 " + + "and archived = 0") + List<FullCard> getFullCardsForNonSharedBoardsWithDueDateForUpcomingCardsWidgetDirectly(List<Long> accountIds); + + }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/GenericDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/GenericDao.java index 97e62226d..f6a57a67a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/GenericDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/GenericDao.java @@ -9,12 +9,15 @@ public interface GenericDao<T> { @Insert long insert(T entity); + @SuppressWarnings("unchecked") @Insert long[] insert(T... entity); + @SuppressWarnings("unchecked") @Update void update(T... entity); + @SuppressWarnings("unchecked") @Delete void delete(T... entity); } 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 feb7e453b..1d2649488 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 @@ -38,6 +38,12 @@ public interface StackDao extends GenericDao<Stack> { @Query("SELECT * FROM stack WHERE accountId = :accountId and localId = :localId") LiveData<FullStack> getFullStack(long accountId, long localId); + @Query("SELECT localId FROM stack WHERE accountId = :accountId") + List<Long> getLocalStackIdsByAccountIdDirectly(long accountId); + + @Query("SELECT localId FROM stack WHERE boardId = :localBoardId") + List<Long> getLocalStackIdsByLocalBoardIdDirectly(long localBoardId); + @Transaction @Query("SELECT * FROM stack WHERE accountId = :accountId and boardId = :localBoardId and (status<>1 or id is null or lastModified <> lastModifiedLocal)") List<FullStack> getLocallyChangedStacksForBoardDirectly(long accountId, long localBoardId); @@ -55,4 +61,10 @@ public interface StackDao extends GenericDao<Stack> { @Query("SELECT coalesce(MAX(`order`), -1) FROM stack s WHERE boardId = :localBoardId") Integer getHighestStackOrderInBoard(long localBoardId); + + @Query("SELECT exists(select 1 from Stack s join Board b on s.boardId = b.localId where s.localId = :localStackId and exists(select 1 from AccessControl ac where ac.boardId = b.localId and status <> 3))") + boolean isStackOnSharedBoardDirectly(Long localStackId); + + @Query("SELECT s.localId FROM stack s join Board b on s.boardId = b.localId where b.archived <> 0 and b.accountId in (:accountIds)") + List<Long> getLocalStackIdsInArchivedBoardsByAccountIdsDirectly(List<Long> accountIds); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetAccountDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetAccountDao.java new file mode 100644 index 000000000..9133aa4bc --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetAccountDao.java @@ -0,0 +1,18 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter; + +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetAccount; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface FilterWidgetAccountDao extends GenericDao<FilterWidgetAccount> { + @Query("DELETE FROM FilterWidgetAccount WHERE filterWidgetId = :filterWidgetId") + void deleteByFilterWidgetId (Integer filterWidgetId); + + @Query("select * FROM FilterWidgetAccount WHERE filterWidgetId = :filterWidgetId") + List<FilterWidgetAccount> getFilterWidgetAccountsByFilterWidgetIdDirectly(Integer filterWidgetId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetBoardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetBoardDao.java new file mode 100644 index 000000000..e71c1b51e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetBoardDao.java @@ -0,0 +1,15 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter; + +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetBoard; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface FilterWidgetBoardDao extends GenericDao<FilterWidgetBoard> { + @Query("SELECT * FROM FilterWidgetBoard where filterAccountId = :filterWidgetAccountId") + List<FilterWidgetBoard> getFilterWidgetBoardsByFilterWidgetAccountIdDirectly(Long filterWidgetAccountId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetDao.java new file mode 100644 index 000000000..23dcbea0f --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetDao.java @@ -0,0 +1,44 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter; + +import androidx.room.Dao; +import androidx.room.Query; +import androidx.room.Transaction; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.widget.filter.EWidgetType; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface FilterWidgetDao extends GenericDao<FilterWidget> { + @Query("DELETE FROM filterwidget WHERE id = :filterWidgetId") + void delete (Integer filterWidgetId); + + @Query("SELECT * FROM FilterWidget where id = :filterWidgetId") + FilterWidget getFilterWidgetByIdDirectly(Integer filterWidgetId); + + @Query("SELECT EXISTS (SELECT 1 FROM FilterWidget WHERE id = :filterWidgetId)") + boolean filterWidgetExists(int filterWidgetId); + + @Query("SELECT id FROM FilterWidget WHERE widgetType = :type") + List<Integer> getFilterWidgetIdsByType(int type); + + @Transaction + @Query("SELECT DISTINCT w.widgetType " + + "FROM FilterWidget w " + + "LEFT JOIN FilterWidgetAccount a ON w.id = a.filterWidgetId " + + "LEFT JOIN FilterWidgetBoard b ON a.id = b.filterAccountId " + + "LEFT JOIN FilterWidgetStack s ON b.id = s.filterBoardId " + + "LEFT JOIN FilterWidgetUser u ON a.id = u.filterAccountId " + + "LEFT JOIN FilterWidgetProject p ON a.id = p.filterAccountId " + + "LEFT JOIN FilterWidgetLabel l ON b.id = l.filterBoardId " + + "WHERE (:changedEntityType = 'ACCOUNT' AND (a.accountId = :localIdOfChangedEntity OR a.accountId IS NULL)) " + + "OR (:changedEntityType = 'BOARD' AND (b.boardId = :localIdOfChangedEntity OR b.boardId IS NULL)) " + + "OR (:changedEntityType = 'STACK' AND (s.stackId = :localIdOfChangedEntity OR s.stackId IS NULL)) " + + "OR (:changedEntityType = 'USER' AND (u.userId = :localIdOfChangedEntity OR u.userId IS NULL)) " + + "OR (:changedEntityType = 'PROJECT' AND (p.projectId = :localIdOfChangedEntity OR p.projectId IS NULL)) " + + "OR (:changedEntityType = 'LABEL' AND (l.labelId = :localIdOfChangedEntity OR l.labelId IS NULL)) " + ) + List<EWidgetType> getChangedListTypesByEntity(String changedEntityType, Long localIdOfChangedEntity); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetLabelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetLabelDao.java new file mode 100644 index 000000000..61ed01545 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetLabelDao.java @@ -0,0 +1,15 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter; + +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetLabel; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface FilterWidgetLabelDao extends GenericDao<FilterWidgetLabel> { + @Query("SELECT * FROM FilterWidgetLabel where filterBoardId = :filterWidgetBoardId") + List<FilterWidgetLabel> getFilterWidgetLabelsByFilterWidgetBoardIdDirectly(Long filterWidgetBoardId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetProjectDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetProjectDao.java new file mode 100644 index 000000000..4e49684e3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetProjectDao.java @@ -0,0 +1,15 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter; + +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetProject; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface FilterWidgetProjectDao extends GenericDao<FilterWidgetProject> { + @Query("SELECT * FROM FilterWidgetProject where filterAccountId = :filterWidgetAccountId") + List<FilterWidgetProject> getFilterWidgetProjectsByFilterWidgetAccountIdDirectly(Long filterWidgetAccountId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetSortDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetSortDao.java new file mode 100644 index 000000000..6aa86a4a3 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetSortDao.java @@ -0,0 +1,18 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter; + +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetSort; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface FilterWidgetSortDao extends GenericDao<FilterWidgetSort> { + @Query("DELETE FROM FilterWidgetSort WHERE filterWidgetId = :filterWidgetId") + void deleteByFilterWidgetId (Integer filterWidgetId); + + @Query("select * FROM FilterWidgetSort WHERE filterWidgetId = :filterWidgetId order by ruleOrder asc") + List<FilterWidgetSort> getFilterWidgetSortByFilterWidgetIdDirectly(Integer filterWidgetId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetStackDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetStackDao.java new file mode 100644 index 000000000..fd79a3ba9 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetStackDao.java @@ -0,0 +1,15 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter; + +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetStack; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface FilterWidgetStackDao extends GenericDao<FilterWidgetStack> { + @Query("SELECT * FROM FilterWidgetStack where filterBoardId = :filterWidgetBoardId") + List<FilterWidgetStack> getFilterWidgetStacksByFilterWidgetBoardIdDirectly(Long filterWidgetBoardId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetUserDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetUserDao.java new file mode 100644 index 000000000..6b44103e9 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/filter/FilterWidgetUserDao.java @@ -0,0 +1,15 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.filter; + +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetUser; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface FilterWidgetUserDao extends GenericDao<FilterWidgetUser> { + @Query("SELECT * FROM FilterWidgetUser where filterAccountId = :filterWidgetAccountId") + List<FilterWidgetUser> getFilterWidgetUsersByFilterWidgetAccountIdDirectly(Long filterWidgetAccountId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_10_11.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_10_11.java new file mode 100644 index 000000000..e50c8a62e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_10_11.java @@ -0,0 +1,59 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import android.database.Cursor; + +import androidx.annotation.NonNull; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import it.niedermann.nextcloud.deck.model.enums.DBStatus; + +/** + * Removes duplicate labels and ensures uniqueness + */ +public class Migration_10_11 extends Migration { + + public Migration_10_11() { + super(10, 11); + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // replace duplicates with the server-known ones + Cursor duplucatesCursor = database.query("SELECT boardId, title, count(*) FROM Label group by boardid, title having count(*) > 1"); + if (duplucatesCursor != null && duplucatesCursor.moveToFirst()) { + do { + long boardId = duplucatesCursor.getLong(0); + String title = duplucatesCursor.getString(1); + Cursor singleDuplicateCursor = database.query("select localId from Label where boardId = ? and title = ? order by id desc", new Object[]{boardId, title}); + if (singleDuplicateCursor != null && singleDuplicateCursor.moveToFirst()) { + long idToUse = -1; + do { + if (idToUse < 0) { + // desc order -> first one is the one with remote ID or a random one. keep this one. + idToUse = singleDuplicateCursor.getLong(0); + continue; + } + long idToReplace = singleDuplicateCursor.getLong(0); + Cursor cardsAssignedToDuplicateCursor = database.query("select cardId, exists(select 1 from JoinCardWithLabel ij where ij.labelId = ? and ij.cardId = cardId) " + + "from JoinCardWithLabel where labelId = ?", new Object[]{idToUse, idToReplace}); + if (cardsAssignedToDuplicateCursor != null && cardsAssignedToDuplicateCursor.moveToFirst()) { + do { + long cardId = cardsAssignedToDuplicateCursor.getLong(0); + boolean hasDestinationLabelAssigned = cardsAssignedToDuplicateCursor.getInt(1) > 0; + database.execSQL("DELETE FROM JoinCardWithLabel where labelId = ? and cardId = ?", new Object[]{idToReplace, cardId}); + + if (!hasDestinationLabelAssigned) { + database.execSQL("INSERT INTO JoinCardWithLabel (status,labelId,cardId) VALUES (?, ?, ?)", new Object[]{DBStatus.LOCAL_EDITED.getId(), idToUse, cardId}); + } + } while (cardsAssignedToDuplicateCursor.moveToNext()); + } + database.execSQL("DELETE FROM Label where localId = ?", new Object[]{idToReplace}); + } while (singleDuplicateCursor.moveToNext()); + } + } while (duplucatesCursor.moveToNext()); + } + // database.execSQL("DELETE FROM Label WHERE id IS NULL AND EXISTS(SELECT 1 FROM Label il WHERE il.boardId = boardId AND il.title = title AND id IS NOT NULL)"); + database.execSQL("CREATE UNIQUE INDEX idx_label_title_unique ON Label(boardId, title)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_11_12.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_11_12.java new file mode 100644 index 000000000..acd646f63 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_11_12.java @@ -0,0 +1,20 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Adds support for the Single note widget + */ +public class Migration_11_12 extends Migration { + + public Migration_11_12() { + super(11, 12); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `SingleCardWidgetModel` (`widgetId` INTEGER PRIMARY KEY, `accountId` INTEGER, `boardId` INTEGER, `cardId` INTEGER, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`cardId`) REFERENCES `Card`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE INDEX `index_SingleCardWidgetModel_cardId` ON `SingleCardWidgetModel` (`cardId`)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_12_13.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_12_13.java new file mode 100644 index 000000000..8a85f1651 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_12_13.java @@ -0,0 +1,17 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +public class Migration_12_13 extends Migration { + + public Migration_12_13() { + super(12, 13); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("CREATE INDEX `idx_cardWidgetModel_accountId` ON `SingleCardWidgetModel` (`accountId`)"); + database.execSQL("CREATE INDEX `idx_cardWidgetModel_boardId` ON `SingleCardWidgetModel` (`boardId`)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_13_14.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_13_14.java new file mode 100644 index 000000000..dfb8994e8 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_13_14.java @@ -0,0 +1,20 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Adds support for comment responses + */ +public class Migration_13_14 extends Migration { + + public Migration_13_14() { + super(13, 14); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `DeckComment` ADD `parentId` INTEGER REFERENCES DeckComment(localId) ON DELETE CASCADE"); + database.execSQL("CREATE INDEX `idx_comment_parentID` ON DeckComment(parentId)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_14_15.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_14_15.java new file mode 100644 index 000000000..dc07a12dd --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_14_15.java @@ -0,0 +1,35 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker; + +/** + * @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/570">Reinitializes the background synchronization</a> and + * <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/525">cleans up old shared preferences</a> + */ +public class Migration_14_15 extends Migration { + + @NonNull + private final Context context; + + public Migration_14_15(@NonNull Context context) { + super(14, 15); + this.context = context; + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + SyncWorker.update(context); + PreferenceManager + .getDefaultSharedPreferences(context) + .edit() + .remove("it.niedermann.nextcloud.deck.theme_text") + .apply(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_15_16.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_15_16.java new file mode 100644 index 000000000..3dd1a3d61 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_15_16.java @@ -0,0 +1,23 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Adds support for Stack widget + */ +public class Migration_15_16 extends Migration { + + public Migration_15_16() { + super(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`)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_16_17.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_16_17.java new file mode 100644 index 000000000..f276fe879 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_16_17.java @@ -0,0 +1,34 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/573">Adds support for projects</a> + */ +public class Migration_16_17 extends Migration { + + public Migration_16_17() { + super(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`)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_17_18.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_17_18.java new file mode 100644 index 000000000..f42be42af --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_17_18.java @@ -0,0 +1,19 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/435">Implement ETags for Capabilities endpoint</a> + */ +public class Migration_17_18 extends Migration { + + public Migration_17_18() { + super(17, 18); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `Account` ADD `etag` TEXT"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_18_19.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_18_19.java new file mode 100644 index 000000000..072a11541 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_18_19.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/619">Handle unknown project types</a> + */ +public class Migration_18_19 extends Migration { + + public Migration_18_19() { + super(18, 19); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + 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`)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_19_20.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_19_20.java new file mode 100644 index 000000000..f6667ec02 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_19_20.java @@ -0,0 +1,35 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Fixes issues with LDAP users when filtering + * https://github.com/stefan-niedermann/nextcloud-deck/issues/492 + * https://github.com/stefan-niedermann/nextcloud-deck/issues/631 + */ +public class Migration_19_20 extends Migration { + + public Migration_19_20() { + super(19, 20); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + 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`)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_20_21.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_20_21.java new file mode 100644 index 000000000..36a907b20 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_20_21.java @@ -0,0 +1,123 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import android.database.Cursor; +import android.graphics.Color; + +import androidx.annotation.ColorInt; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import it.niedermann.android.util.ColorUtil; + +/** + * @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/556">Store colors as integer in database</a> + */ +public class Migration_20_21 extends Migration { + + public Migration_20_21() { + super(20, 21); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + 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 int color1; + @ColorInt int color2; + 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 int color1; + 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 int color1; + 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}); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_21_22.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_21_22.java new file mode 100644 index 000000000..768de3c5d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_21_22.java @@ -0,0 +1,34 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; + +import androidx.annotation.NonNull; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/715">Migrate from java.util.Date and java.util.Calendar to java.time.*</a> + */ +public class Migration_21_22 extends Migration { + + @NonNull + private final Context context; + + public Migration_21_22(@NonNull Context context) { + super(21, 22); + this.context = context; + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + final SharedPreferences.Editor lastSyncPref = context.getApplicationContext().getSharedPreferences("it.niedermann.nextcloud.deck.last_sync", Context.MODE_PRIVATE).edit(); + final Cursor cursor = database.query("select id from `Account`"); + while (cursor.moveToNext()) { + lastSyncPref.remove("lS_" + cursor.getLong(0)); + } + cursor.close(); + lastSyncPref.apply(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_22_23.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_22_23.java new file mode 100644 index 000000000..240f14ac1 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_22_23.java @@ -0,0 +1,30 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/359">Implement ETags for synchronization Speed-Up</a> + */ +public class Migration_22_23 extends Migration { + + public Migration_22_23() { + super(22, 23); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + 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"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_23_24.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_23_24.java new file mode 100644 index 000000000..cd0eb094e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_23_24.java @@ -0,0 +1,39 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import it.niedermann.nextcloud.deck.R; + +/** + * <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/392">Dark mode following system default</a> + */ +public class Migration_23_24 extends Migration { + + @NonNull + private final Context context; + + public Migration_23_24(@NonNull Context context) { + super(23, 24); + this.context = context; + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + final String themePref = context.getString(R.string.pref_key_dark_theme); + + if (sharedPreferences.contains(themePref)) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + final boolean darkTheme = sharedPreferences.getBoolean(themePref, false); + editor.remove(themePref); + editor.putString(themePref, darkTheme ? context.getString(R.string.pref_value_theme_dark) : context.getString(R.string.pref_value_theme_light)); + editor.apply(); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_24_25.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_24_25.java new file mode 100644 index 000000000..6d2bff544 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_24_25.java @@ -0,0 +1,22 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Reset ETags (comments weren't loading due to bug) + */ +public class Migration_24_25 extends Migration { + + public Migration_24_25() { + super(24, 25); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("UPDATE `Account` SET `boardsEtag` = NULL"); + database.execSQL("UPDATE `Board` SET `etag` = NULL"); + database.execSQL("UPDATE `Stack` SET `etag` = NULL"); + database.execSQL("UPDATE `Card` SET `etag` = NULL"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_25_26.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_25_26.java new file mode 100644 index 000000000..37c648d00 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_25_26.java @@ -0,0 +1,42 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Implement <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/597">Filter widget</a> + */ +public class Migration_25_26 extends Migration { + + public Migration_25_26() { + super(25, 26); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `FilterWidget` (`id` INTEGER PRIMARY KEY NOT NULL, `title` TEXT, `dueType` INTEGER, `widgetType` INTEGER NOT NULL)"); + database.execSQL("CREATE TABLE `FilterWidgetAccount` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `filterWidgetId` INTEGER, `accountId` INTEGER, `includeNoUser` INTEGER NOT NULL, `includeNoProject` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`filterWidgetId`) REFERENCES `FilterWidget`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE `FilterWidgetBoard` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `filterAccountId` INTEGER, `boardId` INTEGER, `includeNoLabel` INTEGER NOT NULL, FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`filterAccountId`) REFERENCES `FilterWidgetAccount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE `FilterWidgetLabel` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `filterBoardId` INTEGER, `labelId` INTEGER, FOREIGN KEY(`labelId`) REFERENCES `Label`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`filterBoardId`) REFERENCES `FilterWidgetBoard`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE `FilterWidgetSort` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `filterWidgetId` INTEGER, `direction` INTEGER NOT NULL, `criteria` INTEGER NOT NULL, `ruleOrder` INTEGER NOT NULL, FOREIGN KEY(`filterWidgetId`) REFERENCES `FilterWidget`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE `FilterWidgetStack` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `filterBoardId` INTEGER, `stackId` INTEGER, FOREIGN KEY(`stackId`) REFERENCES `Stack`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`filterBoardId`) REFERENCES `FilterWidgetBoard`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE `FilterWidgetUser` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `filterAccountId` INTEGER, `userId` INTEGER, FOREIGN KEY(`userId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`filterAccountId`) REFERENCES `FilterWidgetAccount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE `FilterWidgetProject` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `filterAccountId` INTEGER, `projectId` INTEGER, FOREIGN KEY(`projectId`) REFERENCES `OcsProject`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`filterAccountId`) REFERENCES `FilterWidgetAccount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE INDEX `index_FilterWidgetAccount_filterWidgetId` ON `FilterWidgetAccount` (`filterWidgetId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetAccount_accountId` ON `FilterWidgetAccount` (`accountId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetBoard_boardId` ON `FilterWidgetBoard` (`boardId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetBoard_filterAccountId` ON `FilterWidgetBoard` (`filterAccountId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetLabel_filterBoardId` ON `FilterWidgetLabel` (`filterBoardId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetLabel_labelId` ON `FilterWidgetLabel` (`labelId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetSort_filterWidgetId` ON `FilterWidgetSort` (`filterWidgetId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetStack_filterBoardId` ON `FilterWidgetStack` (`filterBoardId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetStack_stackId` ON `FilterWidgetStack` (`stackId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetUser_filterAccountId` ON `FilterWidgetUser` (`filterAccountId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetUser_userId` ON `FilterWidgetUser` (`userId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetProject_filterAccountId` ON `FilterWidgetProject` (`filterAccountId`)"); + database.execSQL("CREATE INDEX `idx_FilterWidgetProject_projectId` ON `FilterWidgetProject` (`projectId`)"); + database.execSQL("CREATE INDEX `unique_idx_FilterWidgetSort_filterWidgetId_criteria` ON `FilterWidgetSort` (`filterWidgetId`, `criteria`)"); + database.execSQL("CREATE INDEX `unique_idx_FilterWidgetSort_filterWidgetId_ruleOrder` ON `FilterWidgetSort` (`filterWidgetId`, `ruleOrder`)"); + + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_26_27.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_26_27.java new file mode 100644 index 000000000..f8e27ec6b --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_26_27.java @@ -0,0 +1,62 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import it.niedermann.nextcloud.deck.model.widget.filter.EWidgetType; + +/** + * @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/767">Migrate Stack Widget to Filter Widget infrastructure</a> + */ +public class Migration_26_27 extends Migration { + + public Migration_26_27() { + super(26, 27); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + + Cursor cursor = database.query("select s.localId, s.boardId, s.accountId, w.appWidgetId from `StackWidgetModel` w inner join `Stack` s on s.localId = w.stackId"); + while (cursor.moveToNext()) { + Long localStackId = cursor.getLong(0); + Long localBoardId = cursor.getLong(1); + Long accountId = cursor.getLong(2); + Long filterWidgetId = cursor.getLong(3); + + // widget + ContentValues values = new ContentValues(); + values.put("widgetType", EWidgetType.STACK_WIDGET.getId()); + values.put("id", filterWidgetId); + database.insert("FilterWidget", SQLiteDatabase.CONFLICT_NONE, values); + + // account + values = new ContentValues(); + values.put("filterWidgetId", filterWidgetId); + values.put("accountId", accountId); + values.put("includeNoUser", false); + values.put("includeNoProject", false); + long filterWidgetAccountId = database.insert("FilterWidgetAccount", SQLiteDatabase.CONFLICT_NONE, values); + + // board + values = new ContentValues(); + values.put("filterAccountId", filterWidgetAccountId); + values.put("boardId", localBoardId); + values.put("includeNoLabel", false); + long filterWidgetBoardId = database.insert("FilterWidgetBoard", SQLiteDatabase.CONFLICT_NONE, values); + + // stack + values = new ContentValues(); + values.put("filterBoardId", filterWidgetBoardId); + values.put("stackId", localStackId); + database.insert("FilterWidgetStack", SQLiteDatabase.CONFLICT_NONE, values); + } + + // cleanup + database.execSQL("DROP TABLE `StackWidgetModel`"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_27_28.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_27_28.java new file mode 100644 index 000000000..3f588e4f4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_27_28.java @@ -0,0 +1,19 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Adds support for new attachment types with Deck server <code>1.3.0</code> + */ +public class Migration_27_28 extends Migration { + + public Migration_27_28() { + super(27, 28); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `Attachment` ADD COLUMN `fileId` INTEGER"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_28_29.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_28_29.java new file mode 100644 index 000000000..69c163639 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_28_29.java @@ -0,0 +1,22 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Reset ETags for cards because <a href="https://github.com/nextcloud/deck/issues/2874">the attachments for this card might not be complete</a>. + */ +public class Migration_28_29 extends Migration { + + public Migration_28_29() { + super(28, 29); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("UPDATE `Account` SET `boardsEtag` = NULL"); + database.execSQL("UPDATE `Board` SET `etag` = NULL"); + database.execSQL("UPDATE `Stack` SET `etag` = NULL"); + database.execSQL("UPDATE `Card` SET `etag` = NULL"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_29_30.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_29_30.java new file mode 100644 index 000000000..665fedeb4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_29_30.java @@ -0,0 +1,30 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/392">Dark mode following system default</a> + */ +public class Migration_29_30 extends Migration { + + @NonNull + private final Context context; + + public Migration_29_30(@NonNull Context context) { + super(29, 30); + this.context = context; + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .remove("branding") + .apply(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_8_9.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_8_9.java new file mode 100644 index 000000000..4840dc050 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_8_9.java @@ -0,0 +1,27 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Adds support for comments + */ +public class Migration_8_9 extends Migration { + + public Migration_8_9() { + super(8, 9); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `DeckComment` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `status` INTEGER NOT NULL, `lastModified` INTEGER, `lastModifiedLocal` INTEGER, `objectId` INTEGER, `actorType` TEXT, `creationDateTime` INTEGER, `actorId` TEXT, `actorDisplayName` TEXT, `message` TEXT, FOREIGN KEY(`objectId`) REFERENCES `Card`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE TABLE `Mention` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `commentId` INTEGER, `mentionId` TEXT, `mentionType` TEXT, `mentionDisplayName` TEXT, FOREIGN KEY(`commentId`) REFERENCES `DeckComment`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE INDEX `index_DeckComment_accountId` ON `DeckComment` (`accountId`)"); + database.execSQL("CREATE INDEX `comment_accID` ON `DeckComment` (`accountId`)"); + database.execSQL("CREATE UNIQUE INDEX `index_DeckComment_accountId_id` ON `DeckComment` (`accountId`, `id`)"); + database.execSQL("CREATE INDEX `index_DeckComment_id` ON `DeckComment` (`id`)"); + database.execSQL("CREATE INDEX `index_DeckComment_lastModifiedLocal` ON `DeckComment` (`lastModifiedLocal`)"); + database.execSQL("CREATE INDEX `index_DeckComment_objectId` ON `DeckComment` (`objectId`)"); + database.execSQL("CREATE INDEX `index_Mention_commentId` ON `Mention` (`commentId`)"); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_9_10.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_9_10.java new file mode 100644 index 000000000..59e9200f4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_9_10.java @@ -0,0 +1,22 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration; + +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * Adds support for account colors, deck server versions and Nextcloud maintenance mode + */ +public class Migration_9_10 extends Migration { + + public Migration_9_10() { + super(9, 10); + } + + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `Account` ADD `color` TEXT NOT NULL DEFAULT '#0082c9'"); + database.execSQL("ALTER TABLE `Account` ADD `textColor` TEXT NOT NULL DEFAULT '#ffffff'"); + database.execSQL("ALTER TABLE `Account` ADD `serverDeckVersion` TEXT NOT NULL DEFAULT '0.6.4'"); + database.execSQL("ALTER TABLE `Account` ADD `maintenanceEnabled` INTEGER NOT NULL DEFAULT 0"); + } +} 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 003bceb26..df2eac222 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,11 +1,8 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.MutableLiveData; -import it.niedermann.nextcloud.deck.DeckLog; - /** * Extends a {@link MutableLiveData} with an error state * @@ -29,13 +26,7 @@ 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(locallyCreatedEntity); + postValue(null); } } 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 782b6d951..fa5483b26 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 @@ -35,7 +35,7 @@ public class DataPropagationHelper { boolean connected = serverAdapter.hasInternetConnection(); if (connected) { try { - provider.createOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) { + provider.createOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(callback.getAccount()) { @Override public void onResponse(T response) { new Thread(() -> { @@ -53,11 +53,11 @@ public class DataPropagationHelper { @Override public void onError(Throwable throwable) { super.onError(throwable); - new Thread(() -> callback.onError(throwable, entity)).start(); + new Thread(() -> callback.onError(throwable)).start(); } }, entity); } catch (Throwable t) { - callback.onError(t, entity); + callback.onError(t); } } else { callback.onResponse(entity); @@ -90,12 +90,12 @@ public class DataPropagationHelper { public void onError(Throwable throwable) { super.onError(throwable); new Thread(() -> { - callback.onError(throwable, entity); + callback.onError(throwable); }).start(); } }, entity); } catch (Throwable t) { - callback.onError(t, entity); + callback.onError(t); } } else { callback.onResponse(entity); 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 17c0de2cf..20421ff1a 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 @@ -27,11 +27,14 @@ public class SyncHelper { private long accountId; private IResponseCallback<Boolean> responseCallback; private final Instant lastSync; + private final boolean etagsEnabled; public SyncHelper(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, Instant lastSync) { this.serverAdapter = serverAdapter; this.dataBaseAdapter = dataBaseAdapter; this.lastSync = lastSync; + // check only once per sync + this.etagsEnabled = serverAdapter.isEtagsEnabled(); } // Sync Server -> App @@ -45,7 +48,7 @@ public class SyncHelper { 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()); + DeckLog.error("Skipped null value from server for DataProvider:", provider.getClass().getSimpleName()); continue; } entityFromServer.setAccountId(accountId); @@ -56,10 +59,10 @@ public class SyncHelper { } else { //TODO: how to handle deletes? what about archived? if (existingEntity.getStatus() != DBStatus.UP_TO_DATE.getId()) { - DeckLog.warn("Conflicting changes on entity: " + existingEntity); + DeckLog.warn("Conflicting changes on entity:", existingEntity); // TODO: what to do? } else { - if (entityFromServer.getEtag() != null && entityFromServer.getEtag().equals(existingEntity.getEtag())) { + if (etagsEnabled && 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; } @@ -80,7 +83,6 @@ 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()){ @@ -90,7 +92,8 @@ public class SyncHelper { return; } } - provider.onError(throwable, responseCallback); + super.onError(throwable); + provider.onError(responseCallback); responseCallback.onError(throwable); } }, lastSync); 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 11fb38a66..8e8104692 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 @@ -129,12 +129,10 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> { //do nothing } - public void onError(Throwable error, IResponseCallback<Boolean> responseCallback) { + public void onError(IResponseCallback<Boolean> responseCallback) { if (parent != null) { parent.childDone(this, responseCallback, false); } - //TODO: what to do? what side effect would the following have: -// responseCallback.onError(error); } public T applyUpdatesFromRemote(T localEntity, T remoteEntity, Long accountId) { 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 6dde6b45c..6f908af1a 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 @@ -65,7 +65,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo serverAdapter.getSingleUserData(uid, new IResponseCallback<OcsUser>(account) { @Override public void onResponse(OcsUser response) { - DeckLog.log(response.toString()); + DeckLog.log(response); User user = new User(); user.setUid(response.getId()); user.setPrimaryKey(response.getId()); 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 781f7b1f4..9442085cb 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 @@ -1,5 +1,6 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import android.annotation.SuppressLint; import android.net.Uri; import java.io.File; @@ -67,7 +68,7 @@ public class AttachmentDataProvider extends AbstractSyncDataProvider<Attachment> @Override public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<Attachment> responder, Attachment entity) { File file = new File(entity.getLocalPath()); - serverAdapter.uploadAttachment(board.getId(), stack.getId(), card.getId(), entity.getType(), file, new IResponseCallback<Attachment>(responder.getAccount()) { + serverAdapter.uploadAttachment(board.getId(), stack.getId(), card.getId(), file, new IResponseCallback<Attachment>(responder.getAccount()) { @Override public void onResponse(Attachment response) { if (file.delete()) { @@ -77,10 +78,11 @@ public class AttachmentDataProvider extends AbstractSyncDataProvider<Attachment> } } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { if (!file.delete()) { - DeckLog.error("Could not delete local file: " + file.getAbsolutePath()); + DeckLog.error("Could not delete local file:", file.getAbsolutePath()); } // if (HandledServerErrors.ATTACHMENTS_FILE_ALREADY_EXISTS == HandledServerErrors.fromThrowable(throwable)) { dataBaseAdapter.deleteAttachment(accountId, entity, false); 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 0ceac2cd8..ddb34937f 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 @@ -2,6 +2,8 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; import android.annotation.SuppressLint; +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; + import java.time.Instant; import java.util.ArrayList; import java.util.Collections; @@ -22,7 +24,6 @@ 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.model.ocs.Version; import it.niedermann.nextcloud.deck.model.propagation.CardUpdate; import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; @@ -30,6 +31,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { + private static final String ALREADY_ARCHIVED_INDICATOR = "Operation not allowed. This card is archived."; protected Board board; protected FullStack stack; @@ -42,11 +44,12 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { @Override 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()) { - responder.onResponse(result); + responder.onResponse(new ArrayList<>()); return; } + List<FullCard> result = Collections.synchronizedList(new ArrayList<>()); for (Card card : stack.getCards()) { serverAdapter.getCard(board.getId(), stack.getId(), card.getId(), new IResponseCallback<FullCard>(responder.getAccount()) { @Override @@ -142,7 +145,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } syncHelper.doSyncFor(new AttachmentDataProvider(this, board, stack.getStack(), existingEntity, attachments)); - if (callback.getAccount().getServerDeckVersionAsObject().isGreaterOrEqualTo(new Version("1.0.0", 1, 0, 0))) { + if (callback.getAccount().getServerDeckVersionAsObject().supportsComments()) { DeckLog.verbose("Comments - Version is OK, SYNC"); syncHelper.doSyncFor(new DeckCommentsDataProvider(this, existingEntity.getCard())); } else { @@ -167,7 +170,27 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { public void updateOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<FullCard> callback, FullCard entity) { CardUpdate update = toCardUpdate(entity); update.setStackId(stack.getId()); - serverAdapter.updateCard(board.getId(), stack.getId(), update, callback); + // https://github.com/stefan-niedermann/nextcloud-deck/issues/787 resolve archiving-conflict + serverAdapter.updateCard(board.getId(), stack.getId(), update, new IResponseCallback<FullCard>(callback.getAccount()) { + @Override + public void onResponse(FullCard response) { + callback.onResponse(response); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + if (throwable.getClass() == NextcloudHttpRequestFailedException.class && + throwable.getCause() != null && + throwable.getCause().getClass() == IllegalStateException.class && + throwable.getCause().getMessage() != null && + throwable.getCause().getMessage().contains(ALREADY_ARCHIVED_INDICATOR)) { + callback.onResponse(entity); + } else { + callback.onError(throwable); + } + } + }); } @Override @@ -205,6 +228,10 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { Account account = callback.getAccount(); for (JoinCardWithLabel changedLabelLocal : changedLabels) { Card card = dataBaseAdapter.getCardByLocalIdDirectly(account.getId(), changedLabelLocal.getCardId()); + if (card == null) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/683#issuecomment-759116820 + continue; + } if (this.stack == null) { stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); } else { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardPropagationDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardPropagationDataProvider.java index 422a11caa..aa5da8558 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardPropagationDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardPropagationDataProvider.java @@ -1,8 +1,11 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import android.annotation.SuppressLint; + import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.Card; +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.propagation.CardUpdate; @@ -17,9 +20,42 @@ public class CardPropagationDataProvider extends CardDataProvider { @Override public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<FullCard> responder, FullCard entity) { - Card card = entity.getCard(); - card.setStackId(stack.getId()); - serverAdapter.createCard(board.getId(), stack.getId(), card, responder); + // make sure, all ancestors are synced properly + if (board.getId() == null) { + serverAdapter.createBoard(board, new IResponseCallback<FullBoard>(responder.getAccount()) { + @Override + public void onResponse(FullBoard response) { + board.setId(response.getId()); + board.setEtag(response.getEtag()); + createOnServer(serverAdapter, dataBaseAdapter, accountId, responder, entity); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + responder.onError(throwable); + } + }); + } else if (stack.getId() == null) { + serverAdapter.createStack(board, stack.getStack(), new IResponseCallback<FullStack>(responder.getAccount()) { + @Override + public void onResponse(FullStack response) { + stack.setId(response.getId()); + stack.setEtag(response.getEtag()); + createOnServer(serverAdapter, dataBaseAdapter, accountId, responder, entity); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + responder.onError(throwable); + } + }); + } else { + Card card = entity.getCard(); + card.setStackId(stack.getId()); + serverAdapter.createCard(board.getId(), stack.getId(), card, responder); + } } 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 caa7c68e6..62354d1cf 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,5 +1,7 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; +import android.annotation.SuppressLint; + import java.time.Instant; import java.util.List; @@ -61,10 +63,11 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { responder.onResponse(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { if (HandledServerErrors.LABELS_TITLE_MUST_BE_UNIQUE == HandledServerErrors.fromThrowable(throwable)) { - DeckLog.log(throwable.getCause().getMessage() + ": " + entitiy.toString()); + DeckLog.log(throwable.getCause().getMessage() + ":", entitiy); dataBaseAdapter.deleteLabelPhysically(entitiy); responder.onResponse(entitiy); } else { 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 index b9bfb8b31..5379be6d9 100644 --- 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 @@ -33,6 +33,7 @@ public class OcsProjectDataProvider extends AbstractSyncDataProvider<OcsProject> public void onError(Throwable throwable) { super.onError(throwable); // dont break the sync! + // TODO i got here HTTP 404 once, maybe this should be considered? DeckLog.logError(throwable); responder.onResponse(Collections.emptyList()); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/util/RealPathUtils.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/util/RealPathUtils.java deleted file mode 100644 index f45226ebd..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/util/RealPathUtils.java +++ /dev/null @@ -1,94 +0,0 @@ -package it.niedermann.nextcloud.deck.persistence.util; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.provider.DocumentsContract; -import android.provider.MediaStore; - -import androidx.loader.content.CursorLoader; - -import java.io.File; - -import it.niedermann.nextcloud.deck.DeckLog; - -public class RealPathUtils { - - public static File getRealPath(Context context, Uri uri){ - String path = null; - if (Build.VERSION.SDK_INT < 11) - path = RealPathUtils.getRealPathFromURI_BelowAPI11(context, uri); - - // SDK >= 11 && SDK < 19 - else if (Build.VERSION.SDK_INT < 19) - path = RealPathUtils.getRealPathFromURI_API11to18(context, uri); - - // SDK > 19 (Android 4.4) - else - path = RealPathUtils.getRealPathFromURI_API19(context, uri); - DeckLog.log("File Path: " + path); - // Get the file instance - return new File(path); - } - -@SuppressLint("NewApi") -public static String getRealPathFromURI_API19(Context context, Uri uri){ - String filePath = ""; - String wholeID = DocumentsContract.getDocumentId(uri); - - // Split at colon, use second item in the array - String id; - if (wholeID.contains(":")){ - id = wholeID.split(":")[1]; - } else { - id = wholeID; - } - - String[] column = { MediaStore.Images.Media.DATA }; - - // where id is equal to - String sel = MediaStore.Images.Media._ID + "=?"; - - Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - column, sel, new String[]{ id }, null); - - int columnIndex = cursor.getColumnIndex(column[0]); - - if (cursor.moveToFirst()) { - filePath = cursor.getString(columnIndex); - } - cursor.close(); - return filePath; -} - - -@SuppressLint("NewApi") -public static String getRealPathFromURI_API11to18(Context context, Uri contentUri) { - String[] proj = { MediaStore.Images.Media.DATA }; - String result = null; - - CursorLoader cursorLoader = new CursorLoader( - context, - contentUri, proj, null, null, null); - Cursor cursor = cursorLoader.loadInBackground(); - - if(cursor != null){ - int column_index = - cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); - cursor.moveToFirst(); - result = cursor.getString(column_index); - } - return result; -} - -public static String getRealPathFromURI_BelowAPI11(Context context, Uri contentUri){ - String[] proj = { MediaStore.Images.Media.DATA }; - Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null); - int column_index - = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); - cursor.moveToFirst(); - return cursor.getString(column_index); -} -}
\ No newline at end of file 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 3ded31bb7..bcf5340d8 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 @@ -1,20 +1,17 @@ package it.niedermann.nextcloud.deck.ui; import android.annotation.SuppressLint; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.sqlite.SQLiteConstraintException; -import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; import androidx.preference.PreferenceManager; import com.nextcloud.android.sso.AccountImporter; @@ -89,12 +86,6 @@ public class ImportAccountActivity extends AppCompatActivity { AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this); } }); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - Drawable wrapDrawable = DrawableCompat.wrap(binding.progressCircular.getIndeterminateDrawable()); - DrawableCompat.setTint(wrapDrawable, ContextCompat.getColor(this, R.color.defaultBrand)); - binding.progressCircular.setIndeterminateDrawable(DrawableCompat.unwrap(wrapDrawable)); - } } @Override @@ -137,7 +128,7 @@ public class ImportAccountActivity extends AppCompatActivity { } else { // Remember last account - THIS HAS TO BE DONE SYNCHRONOUSLY SharedPreferences.Editor editor = sharedPreferences.edit(); - DeckLog.log("--- Write: shared_preference_last_account" + " | " + createdAccount.getId()); + DeckLog.log("--- Write: shared_preference_last_account | ", createdAccount.getId()); editor.putLong(sharedPreferenceLastAccount, createdAccount.getId()); editor.commit(); @@ -145,7 +136,7 @@ public class ImportAccountActivity extends AppCompatActivity { @Override public void onResponse(Capabilities response) { if (!response.isMaintenanceEnabled()) { - if (response.getDeckVersion().isSupported(getApplicationContext())) { + if (response.getDeckVersion().isSupported()) { syncManager.synchronize(new IResponseCallback<Boolean>(account) { @Override public void onResponse(Boolean response) { @@ -216,7 +207,7 @@ public class ImportAccountActivity extends AppCompatActivity { DeckLog.log("Rolling back account creation for " + accountId); syncManager.deleteAccount(accountId); SharedPreferences.Editor editor = sharedPreferences.edit(); - DeckLog.log("--- Remove: shared_preference_last_account" + " | " + accountId); + DeckLog.log("--- Remove: shared_preference_last_account |", accountId); editor.remove(sharedPreferenceLastAccount); editor.commit(); // Has to be done synchronously runOnUiThread(() -> binding.addButton.setEnabled(true)); @@ -251,4 +242,8 @@ public class ImportAccountActivity extends AppCompatActivity { editor.putBoolean(prefKeyWifiOnly, originalWifiOnlyValue); editor.apply(); } + + public static Intent createIntent(@NonNull Context context) { + return new Intent(context, ImportAccountActivity.class); + } }
\ No newline at end of file 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 d7e726a7d..58f08237e 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 @@ -11,9 +11,7 @@ import android.net.Network; import android.net.NetworkInfo; import android.net.NetworkRequest; import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -22,10 +20,10 @@ import android.widget.PopupMenu; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.annotation.UiThread; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.util.Pair; @@ -34,6 +32,7 @@ import androidx.core.view.ViewCompat; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; import androidx.viewpager2.widget.ViewPager2; import com.bumptech.glide.Glide; @@ -58,6 +57,7 @@ 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; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.databinding.ActivityMainBinding; import it.niedermann.nextcloud.deck.databinding.NavHeaderMainBinding; import it.niedermann.nextcloud.deck.exceptions.OfflineException; @@ -107,7 +107,7 @@ import static it.niedermann.nextcloud.deck.DeckApplication.NO_STACK_ID; 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.saveCurrentAccount; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardId; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentStackId; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; @@ -132,9 +132,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener private FilterViewModel filterViewModel; private PickStackViewModel pickStackViewModel; - protected static final int ACTIVITY_ABOUT = 1; protected static final int ACTIVITY_SETTINGS = 2; - public static final int ACTIVITY_MANAGE_ACCOUNTS = 4; @NonNull protected List<Account> accountsList = new ArrayList<>(); @@ -158,8 +156,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener private boolean firstAccountAdded = false; private ConnectivityManager.NetworkCallback networkCallback; - private String accountAlreadyAdded; - private String urlFragmentUpdateDeck; private String addList; private String addBoard; @Nullable @@ -186,8 +182,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener addList = getString(R.string.add_list); addBoard = getString(R.string.add_board); - accountAlreadyAdded = getString(R.string.account_already_added); - urlFragmentUpdateDeck = getString(R.string.url_fragment_update_deck); setSupportActionBar(binding.toolbar); @@ -198,11 +192,26 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener binding.navigationView.setNavigationItemSelectedListener(this); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + DeckApplication.readCurrentAccountColor().observe(this, (accountColor) -> { + headerBinding.headerView.setBackgroundColor(accountColor); + @ColorInt final int headerTextColor = contrastRatioIsSufficientBigAreas(accountColor, Color.WHITE) ? Color.WHITE : Color.BLACK; + DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor); + DrawableCompat.setTint(headerBinding.copyDebugLogs.getDrawable(), headerTextColor); + }); + + mainViewModel.isDebugModeEnabled().observe(this, (enabled) -> headerBinding.copyDebugLogs.setVisibility(enabled ? View.VISIBLE : View.GONE)); + headerBinding.copyDebugLogs.setOnClickListener((v) -> { + try { + DeckLog.shareLogAsFile(this); + } catch (Exception e) { + ExceptionDialogFragment.newInstance(e, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); switchMap(mainViewModel.hasAccounts(), hasAccounts -> { if (hasAccounts) { return mainViewModel.readAccounts(); } else { - startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + startActivityForResult(ImportAccountActivity.createIntent(this), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); return null; } }).observe(this, (List<Account> accounts) -> { @@ -221,21 +230,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener mainViewModel.setCurrentAccount(account); if (!firstAccountAdded) { DeckLog.info("Syncing the current account on app start"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - registerAutoSyncOnNetworkAvailable(); - } else { - mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) { - @Override - public void onResponse(Boolean response) { - } - - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - showSyncFailedSnackbar(throwable); - } - }); - } + registerAutoSyncOnNetworkAvailable(); firstAccountAdded = false; } break; @@ -246,7 +241,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener SingleAccountHelper.setCurrentAccount(getApplicationContext(), mainViewModel.getCurrentAccount().getName()); mainViewModel.recreateSyncManager(); - saveCurrentAccountId(this, mainViewModel.getCurrentAccount().getId()); + saveCurrentAccount(this, mainViewModel.getCurrentAccount()); if (mainViewModel.getCurrentAccount().isMaintenanceEnabled()) { refreshCapabilities(mainViewModel.getCurrentAccount()); } @@ -264,18 +259,21 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener } boardsList = boards; + Board currentBoard = null; if (boardsList.size() > 0) { boolean currentBoardIdWasInList = false; for (int i = 0; i < boardsList.size(); i++) { if (lastBoardId == boardsList.get(i).getLocalId() || lastBoardId == NO_BOARD_ID) { - setCurrentBoard(boardsList.get(i)); + currentBoard = boardsList.get(i); + setCurrentBoard(currentBoard); currentBoardIdWasInList = true; break; } } if (!currentBoardIdWasInList) { - setCurrentBoard(boardsList.get(0)); + currentBoard = boardsList.get(0); + setCurrentBoard(currentBoard); } binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); @@ -286,13 +284,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener binding.filter.setOnClickListener(null); } + final Board finalCurrentBoard = currentBoard; if (hasArchivedBoardsLiveData != null && hasArchivedBoardsLiveDataObserver != null) { hasArchivedBoardsLiveData.removeObserver(hasArchivedBoardsLiveDataObserver); } hasArchivedBoardsLiveData = mainViewModel.hasArchivedBoards(currentAccount.getId()); hasArchivedBoardsLiveDataObserver = (hasArchivedBoards) -> { mainViewModel.setCurrentAccountHasArchivedBoards(Boolean.TRUE.equals(hasArchivedBoards)); - inflateBoardMenu(); + inflateBoardMenu(finalCurrentBoard); }; hasArchivedBoardsLiveData.observe(this, hasArchivedBoardsLiveDataObserver); }; @@ -300,21 +299,21 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener Glide .with(binding.accountSwitcher.getContext()) - .load(currentAccount.getAvatarUrl(64)) + .load(currentAccount.getAvatarUrl(binding.accountSwitcher.getWidth())) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) .into(binding.accountSwitcher); - DeckLog.verbose("Displaying maintenance mode info for " + mainViewModel.getCurrentAccount().getName() + ": " + mainViewModel.getCurrentAccount().isMaintenanceEnabled()); + DeckLog.verbose("Displaying maintenance mode info for", mainViewModel.getCurrentAccount().getName() + ":" + mainViewModel.getCurrentAccount().isMaintenanceEnabled()); binding.infoBox.setVisibility(mainViewModel.getCurrentAccount().isMaintenanceEnabled() ? View.VISIBLE : View.GONE); if (mainViewModel.isCurrentAccountIsSupportedVersion()) { binding.infoBoxVersionNotSupported.setVisibility(View.GONE); } else { - binding.infoBoxVersionNotSupportedText.setText(getString(R.string.info_box_version_not_supported, mainViewModel.getCurrentAccount().getServerDeckVersion(), Version.minimumSupported(this).getOriginalVersion())); + binding.infoBoxVersionNotSupportedText.setText(getString(R.string.info_box_version_not_supported, mainViewModel.getCurrentAccount().getServerDeckVersion(), Version.minimumSupported().getOriginalVersion())); binding.infoBoxVersionNotSupportedText.setOnClickListener((v) -> { Intent openURL = new Intent(Intent.ACTION_VIEW); - openURL.setData(Uri.parse(mainViewModel.getCurrentAccount().getUrl() + urlFragmentUpdateDeck)); + openURL.setData(Uri.parse(mainViewModel.getCurrentAccount().getUrl() + getString(R.string.url_fragment_update_deck))); startActivity(openURL); }); binding.infoBoxVersionNotSupported.setVisibility(View.VISIBLE); @@ -329,7 +328,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener dragAndDrop.register(binding.viewPager, binding.stackTitles, getSupportFragmentManager()); dragAndDrop.addItemMovedByDragListener((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); + DeckLog.info("Card", movedCard.getCard().getTitle(), "was moved to Stack", stackId, "on position", position); }); @@ -429,10 +428,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener applyBrandToFAB(mainColor, binding.fab); // TODO We assume, that the background of the spinner is always white 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); - headerBinding.appName.setTextColor(headerTextColor); DrawableCompat.setTint(binding.filterIndicator.getDrawable(), getSecondaryForegroundColorDependingOnTheme(this, mainColor)); } @@ -446,27 +441,35 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onCreateStack(String stackName) { - 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) + DeckLog.info("Create Stack in account", mainViewModel.getCurrentAccount().getName(), "on board", mainViewModel.getCurrentBoardLocalId()); + mainViewModel.createStack(mainViewModel.getCurrentAccount().getId(), stackName, mainViewModel.getCurrentBoardLocalId(), new ResponseCallback<FullStack>() { + @Override + public void onResponse(FullStack response) { + runOnUiThread(() -> binding.viewPager.setCurrentItem(stackAdapter.getItemCount())); + } + + @Override + public void onError(Throwable error) { + ResponseCallback.super.onError(error); + runOnUiThread(() -> 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()); + .show()); } }); } @Override public void onUpdateStack(long localStackId, String stackName) { - 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()); + mainViewModel.updateStackTitle(localStackId, stackName, new ResponseCallback<FullStack>() { + @Override + public void onResponse(FullStack response) { + DeckLog.info("Successfully updated", Stack.class.getSimpleName(), "to", stackName); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } }); } @@ -481,30 +484,42 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener boardToCreate.setPermissionEdit(true); boardToCreate.setPermissionManage(true); - 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(); + mainViewModel.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate, new ResponseCallback<FullBoard>() { + @Override + public void onResponse(FullBoard response) { + runOnUiThread(() -> { + if (response != null) { + boardsList.add(response.getBoard()); + setCurrentBoard(response.getBoard()); + inflateBoardMenu(response.getBoard()); + EditStackDialogFragment.newInstance(NO_STACK_ID).show(getSupportFragmentManager(), addList); + } + boardsLiveData.observe(MainActivity.this, boardsLiveDataObserver); + }); } - if (createdBoard != null && !createLiveData.hasError()) { - boardsList.add(createdBoard.getBoard()); - setCurrentBoard(createdBoard.getBoard()); - inflateBoardMenu(); - EditStackDialogFragment.newInstance(NO_STACK_ID).show(getSupportFragmentManager(), addList); + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> BrandedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG) + .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) + .show()); } - boardsLiveData.observe(this, boardsLiveDataObserver); }); } @Override public void onUpdateBoard(FullBoard 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()); + mainViewModel.updateBoard(fullBoard, new ResponseCallback<FullBoard>() { + @Override + public void onResponse(FullBoard response) { + DeckLog.info("Successfully updated board", fullBoard.getBoard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } }); } @@ -517,18 +532,20 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener binding.swipeRefreshLayout.setRefreshing(false); } else { // If we notice after updating the capabilities, that the new version is not supported, but it was previously, recreate the activity to make sure all elements are disabled properly - if (mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().isSupported(MainActivity.this) && !response.getDeckVersion().isSupported(MainActivity.this)) { - recreate(); + if (mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().isSupported() && !response.getDeckVersion().isSupported()) { + ActivityCompat.recreate(MainActivity.this); } } } @Override public void onError(Throwable throwable) { + binding.swipeRefreshLayout.setRefreshing(false); if (throwable instanceof OfflineException) { DeckLog.info("Cannot refresh capabilities because device is offline."); } else { super.onError(throwable); + ExceptionDialogFragment.newInstance(throwable, account).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } } }); @@ -552,6 +569,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener lastBoardId = board.getLocalId(); saveCurrentBoardId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()); + binding.navigationView.setCheckedItem(boardsList.indexOf(board)); binding.toolbar.setTitle(board.getTitle()); @@ -637,21 +655,22 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener } @UiThread - protected void inflateBoardMenu() { + protected void inflateBoardMenu(@Nullable Board currentBoard) { binding.navigationView.setItemIconTintList(null); Menu menu = binding.navigationView.getMenu(); menu.clear(); - DrawerMenuUtil.inflateBoards(this, menu, this.boardsList, mainViewModel.currentAccountHasArchivedBoards(), mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().isSupported(this)); + DrawerMenuUtil.inflateBoards(this, menu, this.boardsList, mainViewModel.currentAccountHasArchivedBoards(), mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().isSupported()); + binding.navigationView.setCheckedItem(boardsList.indexOf(currentBoard)); } @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { switch (item.getItemId()) { case MENU_ID_ABOUT: - startActivityForResult(AboutActivity.createIntent(this, mainViewModel.getCurrentAccount()), MainActivity.ACTIVITY_ABOUT); + startActivity(AboutActivity.createIntent(this, mainViewModel.getCurrentAccount())); break; case MENU_ID_SETTINGS: - startActivityForResult(new Intent(this, SettingsActivity.class), MainActivity.ACTIVITY_SETTINGS); + startActivityForResult(SettingsActivity.createIntent(this), MainActivity.ACTIVITY_SETTINGS); break; case MENU_ID_ADD_BOARD: EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard); @@ -681,10 +700,18 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener : 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()); + mainViewModel.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation, new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully archived all cards in stack local id", stackLocalId); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } } }); }) @@ -761,7 +788,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener switch (requestCode) { case MainActivity.ACTIVITY_SETTINGS: if (resultCode == RESULT_OK) { - recreate(); + ActivityCompat.recreate(this); } break; case ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT: @@ -787,17 +814,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onResponse(Capabilities response) { if (!response.isMaintenanceEnabled()) { - if (response.getDeckVersion().isSupported(getApplicationContext())) { + if (response.getDeckVersion().isSupported()) { runOnUiThread(() -> { mainViewModel.setSyncManager(importSyncManager); - mainViewModel.setCurrentAccount(account); final Snackbar importSnackbar = BrandedSnackbar.make(binding.coordinatorLayout, R.string.account_is_getting_imported, Snackbar.LENGTH_INDEFINITE); importSnackbar.show(); - importSyncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) { + importSyncManager.synchronize(new IResponseCallback<Boolean>(createdAccount) { @Override public void onResponse(Boolean response) { importSnackbar.dismiss(); + runOnUiThread(() -> BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.account_imported), Snackbar.LENGTH_LONG) + .setAction(R.string.simple_switch, (a) -> mainViewModel.setCurrentAccount(createdAccount)) + .show()); } @Override @@ -811,14 +840,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener }); }); } else { - DeckLog.warn("Cannot import account because server version is too low (" + response.getDeckVersion() + "). Minimum server version is currently " + Version.minimumSupported(getApplicationContext())); + DeckLog.warn("Cannot import account because server version is too low (" + response.getDeckVersion() + "). Minimum server version is currently", Version.minimumSupported()); runOnUiThread(() -> new BrandedAlertDialogBuilder(MainActivity.this) .setTitle(R.string.update_deck) .setMessage(getString(R.string.deck_outdated_please_update, response.getDeckVersion().getOriginalVersion())) .setNegativeButton(R.string.simple_discard, null) .setPositiveButton(R.string.simple_update, (dialog, whichButton) -> { final Intent openURL = new Intent(Intent.ACTION_VIEW); - openURL.setData(Uri.parse(createdAccount.getUrl() + urlFragmentUpdateDeck)); + openURL.setData(Uri.parse(createdAccount.getUrl() + getString(R.string.url_fragment_update_deck))); startActivity(openURL); finish(); }).show()); @@ -855,7 +884,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener final Throwable error = accountLiveData.getError(); if (error instanceof SQLiteConstraintException) { DeckLog.warn("Account already added"); - BrandedSnackbar.make(binding.coordinatorLayout, accountAlreadyAdded, Snackbar.LENGTH_LONG).show(); + BrandedSnackbar.make(binding.coordinatorLayout, R.string.account_already_added, Snackbar.LENGTH_LONG).show(); } else { ExceptionDialogFragment.newInstance(error, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } @@ -877,7 +906,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener } } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void registerAutoSyncOnNetworkAvailable() { final ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkRequest.Builder builder = new NetworkRequest.Builder(); @@ -919,10 +947,18 @@ 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 = mainViewModel.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId()); - observeOnce(deleteStackLiveData, this, (v) -> { - if (deleteStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteStackLiveData.getError())) { - ExceptionDialogFragment.newInstance(deleteStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + mainViewModel.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId(), new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted stack with local id", stackLocalId, "and remote id", stackId); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } } }); } @@ -942,10 +978,18 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener } } - 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()); + mainViewModel.deleteBoard(board, new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted board", board.getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } } }); @@ -968,10 +1012,16 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onArchive(@NonNull Board 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()); + mainViewModel.archiveBoard(board, new ResponseCallback<FullBoard>() { + @Override + public void onResponse(FullBoard response) { + DeckLog.info("Successfully archived board", board.getTitle()); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } }); } @@ -987,18 +1037,27 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener 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(); + mainViewModel.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0], new ResponseCallback<FullBoard>() { + @Override + public void onResponse(FullBoard response) { + runOnUiThread(() -> { + snackbar.dismiss(); + setCurrentBoard(response.getBoard()); + BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.successfully_cloned_board, response.getBoard().getTitle()), Snackbar.LENGTH_LONG) + .setAction(R.string.edit, v -> EditBoardDialogFragment.newInstance(response.getLocalId()).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName())) + .show(); + }); } - })); + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> { + snackbar.dismiss(); + ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + }); + } + }); }) .setNeutralButton(android.R.string.cancel, null) .show(); 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 ac244b88e..53443d07c 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 @@ -9,11 +9,15 @@ import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import androidx.preference.PreferenceManager; import java.io.File; import java.util.List; +import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData; +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.model.AccessControl; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; @@ -48,6 +52,10 @@ public class MainViewModel extends AndroidViewModel { this.syncManager = new SyncManager(application); } + public LiveData<Boolean> isDebugModeEnabled() { + return new SharedPreferenceBooleanLiveData(PreferenceManager.getDefaultSharedPreferences(getApplication()), getApplication().getString(R.string.pref_key_debugging), false); + } + public Account getCurrentAccount() { return currentAccount.getValue(); } @@ -58,7 +66,7 @@ public class MainViewModel extends AndroidViewModel { public void setCurrentAccount(Account currentAccount) { this.currentAccount.setValue(currentAccount); - this.currentAccountIsSupportedVersion = currentAccount.getServerDeckVersionAsObject().isSupported(getApplication().getApplicationContext()); + this.currentAccountIsSupportedVersion = currentAccount.getServerDeckVersionAsObject().isSupported(); } public void setCurrentBoard(@NonNull Board currentBoard) { @@ -127,12 +135,12 @@ public class MainViewModel extends AndroidViewModel { return syncManager.readAccounts(); } - public WrappedLiveData<FullBoard> createBoard(long accountId, @NonNull Board board) { - return syncManager.createBoard(accountId, board); + public void createBoard(long accountId, @NonNull Board board, @NonNull ResponseCallback<FullBoard> callback) { + syncManager.createBoard(accountId, board, callback); } - public WrappedLiveData<FullBoard> updateBoard(@NonNull FullBoard board) { - return syncManager.updateBoard(board); + public void updateBoard(@NonNull FullBoard board, @NonNull ResponseCallback<FullBoard> callback) { + syncManager.updateBoard(board, callback); } public LiveData<List<Board>> getBoards(long accountId, boolean archived) { @@ -143,64 +151,64 @@ public class MainViewModel extends AndroidViewModel { return syncManager.getFullBoardById(accountId, localId); } - public WrappedLiveData<FullBoard> archiveBoard(@NonNull Board board) { - return syncManager.archiveBoard(board); + public void archiveBoard(@NonNull Board board, @NonNull ResponseCallback<FullBoard> callback) { + syncManager.archiveBoard(board, callback); } - public WrappedLiveData<FullBoard> dearchiveBoard(@NonNull Board board) { - return syncManager.dearchiveBoard(board); + public void dearchiveBoard(@NonNull Board board, @NonNull ResponseCallback<FullBoard> callback) { + syncManager.dearchiveBoard(board, callback); } - public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards) { - return syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards); + public void cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards, @NonNull ResponseCallback<FullBoard> callback) { + syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards, callback); } - public WrappedLiveData<Void> deleteBoard(@NonNull Board board) { - return syncManager.deleteBoard(board); + public void deleteBoard(@NonNull Board board, @NonNull ResponseCallback<Void> callback) { + syncManager.deleteBoard(board, callback); } public LiveData<Boolean> hasArchivedBoards(long accountId) { return syncManager.hasArchivedBoards(accountId); } - public WrappedLiveData<AccessControl> createAccessControl(long accountId, AccessControl entity) { - return syncManager.createAccessControl(accountId, entity); + public void createAccessControl(long accountId, @NonNull AccessControl entity, @NonNull ResponseCallback<AccessControl> callback) { + syncManager.createAccessControl(accountId, entity, callback); } - public WrappedLiveData<AccessControl> updateAccessControl(@NonNull AccessControl entity) { - return syncManager.updateAccessControl(entity); + public void updateAccessControl(@NonNull AccessControl entity, @NonNull ResponseCallback<AccessControl> callback) { + syncManager.updateAccessControl(entity, callback); } 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 void deleteAccessControl(@NonNull AccessControl entity, @NonNull ResponseCallback<Void> callback) { + syncManager.deleteAccessControl(entity, callback); } - public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) { - return syncManager.createLabel(accountId, label, localBoardId); + public void createLabel(long accountId, Label label, long localBoardId, @NonNull ResponseCallback<Label> callback) { + syncManager.createLabel(accountId, label, localBoardId, callback); } public LiveData<Integer> countCardsWithLabel(long localLabelId) { return syncManager.countCardsWithLabel(localLabelId); } - public WrappedLiveData<Label> updateLabel(@NonNull Label label) { - return syncManager.updateLabel(label); + public void updateLabel(@NonNull Label label, @NonNull ResponseCallback<Label> callback) { + syncManager.updateLabel(label, callback); } - public WrappedLiveData<Void> deleteLabel(@NonNull Label label) { - return syncManager.deleteLabel(label); + public void deleteLabel(@NonNull Label label, @NonNull ResponseCallback<Void> callback) { + syncManager.deleteLabel(label, callback); } 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 void createStack(long accountId, @NonNull String title, long boardLocalId, @NonNull ResponseCallback<FullStack> callback) { + syncManager.createStack(accountId, title, boardLocalId, callback); } public LiveData<FullStack> getStack(long accountId, long localStackId) { @@ -211,12 +219,12 @@ public class MainViewModel extends AndroidViewModel { syncManager.swapStackOrder(accountId, boardLocalId, stackLocalIds); } - public WrappedLiveData<FullStack> updateStackTitle(long localStackId, @NonNull String newTitle) { - return syncManager.updateStackTitle(localStackId, newTitle); + public void updateStackTitle(long localStackId, @NonNull String newTitle, @NonNull ResponseCallback<FullStack> callback) { + syncManager.updateStackTitle(localStackId, newTitle, callback); } - public WrappedLiveData<Void> deleteStack(long accountId, long stackLocalId, long boardLocalId) { - return syncManager.deleteStack(accountId, stackLocalId, boardLocalId); + public void deleteStack(long accountId, long stackLocalId, long boardLocalId, @NonNull ResponseCallback<Void> callback) { + syncManager.deleteStack(accountId, stackLocalId, boardLocalId, callback); } public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) { @@ -227,8 +235,8 @@ public class MainViewModel extends AndroidViewModel { return syncManager.countCardsInStack(accountId, localStackId); } - public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation) { - return syncManager.archiveCardsInStack(accountId, stackLocalId, filterInformation); + public void archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation, @NonNull ResponseCallback<Void> callback) { + syncManager.archiveCardsInStack(accountId, stackLocalId, filterInformation, callback); } public WrappedLiveData<FullCard> updateCard(@NonNull FullCard fullCard) { @@ -239,8 +247,8 @@ public class MainViewModel extends AndroidViewModel { 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 addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file, @NonNull ResponseCallback<Attachment> callback) { + syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file, callback); } public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) { @@ -271,15 +279,15 @@ public class MainViewModel extends AndroidViewModel { return syncManager.getUserByUidDirectly(accountId, uid); } - public WrappedLiveData<FullCard> archiveCard(@NonNull FullCard card) { - return syncManager.archiveCard(card); + public void archiveCard(@NonNull FullCard card, @NonNull ResponseCallback<FullCard> callback) { + syncManager.archiveCard(card, callback); } - public WrappedLiveData<FullCard> dearchiveCard(@NonNull FullCard card) { - return syncManager.dearchiveCard(card); + public void dearchiveCard(@NonNull FullCard card, @NonNull ResponseCallback<FullCard> callback) { + syncManager.dearchiveCard(card, callback); } - public WrappedLiveData<Void> deleteCard(@NonNull Card card) { - return syncManager.deleteCard(card); + public void deleteCard(@NonNull Card card, @NonNull ResponseCallback<Void> callback) { + syncManager.deleteCard(card, callback); } } 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 index 2339a8783..d60ce4002 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java @@ -1,6 +1,5 @@ package it.niedermann.nextcloud.deck.ui; -import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.Color; import android.os.Bundle; @@ -31,7 +30,6 @@ 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 { @@ -39,8 +37,6 @@ public abstract class PickStackActivity extends AppCompatActivity implements Bra protected ActivityPickStackBinding binding; protected PickStackViewModel viewModel; - private boolean brandingEnabled; - private Account selectedAccount; private Board selectedBoard; private Stack selectedStack; @@ -52,8 +48,6 @@ public abstract class PickStackActivity extends AppCompatActivity implements Bra Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); - brandingEnabled = isBrandingEnabled(this); - binding = ActivityPickStackBinding.inflate(getLayoutInflater()); viewModel = new ViewModelProvider(this).get(PickStackViewModel.class); @@ -64,7 +58,7 @@ public abstract class PickStackActivity extends AppCompatActivity implements Bra if (hasAccounts) { return viewModel.readAccounts(); } else { - startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + startActivityForResult(ImportAccountActivity.createIntent(this), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); return null; } }).observe(this, (List<Account> accounts) -> { @@ -96,20 +90,18 @@ public abstract class PickStackActivity extends AppCompatActivity implements Bra @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)); - } + @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 void onSubmit(Account account, long boardLocalId, 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 cdc20ed50..280ecebd8 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 @@ -18,6 +18,7 @@ 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.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.ProjectUtil; @@ -40,8 +41,9 @@ public class PushNotificationActivity extends AppCompatActivity { super.onResume(); Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + final Intent intent = getIntent(); - if (getIntent() == null) { + if (intent == null) { throw new IllegalArgumentException("Could not retrieve intent"); } @@ -51,101 +53,113 @@ public class PushNotificationActivity extends AppCompatActivity { setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - binding.subject.setText(getIntent().getStringExtra(KEY_SUBJECT)); + binding.subject.setText(intent.getStringExtra(KEY_SUBJECT)); - final String message = getIntent().getStringExtra(KEY_MESSAGE); + final String message = intent.getStringExtra(KEY_MESSAGE); if (!TextUtils.isEmpty(message)) { binding.message.setText(message); binding.message.setVisibility(View.VISIBLE); } - final String link = getIntent().getStringExtra(KEY_LINK); - long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link); - - binding.cancel.setOnClickListener((v) -> finish()); - - final String cardRemoteIdString = getIntent().getStringExtra(KEY_CARD_REMOTE_ID); - final String accountString = getIntent().getStringExtra(KEY_ACCOUNT); - - DeckLog.verbose("cardRemoteIdString = " + cardRemoteIdString); - 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); + final String link = intent.getStringExtra(KEY_LINK); + final long[] ids; + try { + ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link); + + binding.cancel.setOnClickListener((v) -> finish()); + + final String cardRemoteIdString = intent.getStringExtra(KEY_CARD_REMOTE_ID); + final String accountString = intent.getStringExtra(KEY_ACCOUNT); + + DeckLog.verbose("cardRemoteIdString = ", cardRemoteIdString); + 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); } - } else { - DeckLog.warn("Link does not contain two IDs (expected one board id and one card id): " + link); - fallbackToBrowser(link); + } catch (Throwable t) { + final String params = "Error while receiving push notification:\n" + + KEY_SUBJECT + ": [" + intent.getStringExtra(KEY_SUBJECT) + "]\n" + + KEY_MESSAGE + ": [" + intent.getStringExtra(KEY_MESSAGE) + "]\n" + + KEY_LINK + ": [" + intent.getStringExtra(KEY_LINK) + "]\n" + + KEY_CARD_REMOTE_ID + ": [" + intent.getStringExtra(KEY_CARD_REMOTE_ID) + "]\n" + + KEY_ACCOUNT + ": [" + intent.getStringExtra(KEY_ACCOUNT) + "]"; + ExceptionDialogFragment.newInstance(new Exception(params, t), null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + DeckLog.logError(t); } } @@ -182,7 +196,7 @@ public class PushNotificationActivity extends AppCompatActivity { @UiThread private void launchEditActivity(@NonNull Account account, Long boardId, Long cardId) { - DeckLog.info("starting " + EditActivity.class.getSimpleName() + " with [" + account + ", " + boardId + ", " + cardId + "]"); + DeckLog.info("starting", EditActivity.class.getSimpleName(), "with [" + account + ", " + boardId + ", " + cardId + "]"); startActivity(EditActivity.createEditCardIntent(this, account, boardId, cardId)); finish(); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java index bf3734b72..c37d0bc46 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java @@ -6,22 +6,22 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.lifecycle.Lifecycle; +import androidx.fragment.app.FragmentActivity; import androidx.viewpager2.adapter.FragmentStateAdapter; import com.google.android.material.tabs.TabLayoutMediator; +import it.niedermann.nextcloud.deck.DeckApplication; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ActivityAboutBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToPrimaryTabLayout; -public class AboutActivity extends BrandedActivity { +public class AboutActivity extends AppCompatActivity { private static final String BUNDLE_KEY_ACCOUNT = "account"; private ActivityAboutBinding binding; @@ -39,11 +39,11 @@ public class AboutActivity extends BrandedActivity { binding = ActivityAboutBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + DeckApplication.readCurrentAccountColor().observe(this, (mainColor) -> applyBrandToPrimaryTabLayout(mainColor, binding.tabLayout)); + setSupportActionBar(binding.toolbar); - binding.viewPager.setAdapter(new TabsPagerAdapter(getSupportFragmentManager(), getLifecycle(), (Account) getIntent().getSerializableExtra(BUNDLE_KEY_ACCOUNT))); + binding.viewPager.setAdapter(new TabsPagerAdapter(this, (Account) getIntent().getSerializableExtra(BUNDLE_KEY_ACCOUNT))); new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> tab.setText(tabTitles[position])).attach(); - - setResult(RESULT_OK); } private static class TabsPagerAdapter extends FragmentStateAdapter { @@ -51,8 +51,8 @@ public class AboutActivity extends BrandedActivity { @Nullable private final Account account; - TabsPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle, @Nullable Account account) { - super(fragmentManager, lifecycle); + TabsPagerAdapter(final FragmentActivity fa, @Nullable Account account) { + super(fa); this.account = account; } @@ -83,11 +83,6 @@ public class AboutActivity extends BrandedActivity { return true; } - @Override - public void applyBrand(int mainColor) { - applyBrandToPrimaryTabLayout(mainColor, binding.tabLayout); - } - @NonNull public static Intent createIntent(@NonNull Context context, @NonNull Account account) { return new Intent(context, AboutActivity.class) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentCreditsTab.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentCreditsTab.java index 0b0f26e63..6dac2a2a1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentCreditsTab.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentCreditsTab.java @@ -55,7 +55,7 @@ public class AboutFragmentCreditsTab extends Fragment { binding.lastBackgroundSync.setText( lastBackgroundSync == BACKGROUND_SYNC_NEVER_EXECUTED || settingsBackgroundSync.equals(backgroundSyncOffValue) ? disabled(getString(R.string.simple_disabled), requireContext()) - : strong(DateUtil.getRelativeDateTimeString(getContext(), lastBackgroundSync)) + : strong(DateUtil.getRelativeDateTimeString(requireContext(), lastBackgroundSync)) ); binding.aboutMaintainer.setText(url(getString(R.string.about_maintainer), getString(R.string.url_maintainer))); binding.aboutMaintainer.setMovementMethod(new LinkMovementMethod()); 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 0bd92bc78..5e7b7a1ec 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 @@ -11,19 +11,21 @@ import android.view.ViewGroup; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; +import androidx.fragment.app.Fragment; import it.niedermann.android.util.ColorUtil; +import it.niedermann.nextcloud.deck.DeckApplication; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.FragmentAboutLicenseTabBinding; -import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; import static it.niedermann.nextcloud.deck.util.SpannableUtil.setTextWithURL; -public class AboutFragmentLicenseTab extends BrandedFragment { +public class AboutFragmentLicenseTab extends Fragment { private FragmentAboutLicenseTabBinding binding; @@ -37,11 +39,14 @@ public class AboutFragmentLicenseTab extends BrandedFragment { } @Override - public void applyBrand(int mainColor) { - @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(requireContext(), R.color.primary)) - ? mainColor - : isDarkTheme(requireContext()) ? Color.WHITE : Color.BLACK; - DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(finalMainColor)); - binding.aboutAppLicenseButton.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor)); + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + DeckApplication.readCurrentAccountColor().observe(getViewLifecycleOwner(), (mainColor) -> { + @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(requireContext(), R.color.primary)) + ? mainColor + : isDarkTheme(requireContext()) ? Color.WHITE : Color.BLACK; + DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(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 744498c4a..8a70cf12d 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.Intent; import android.net.Uri; import android.os.Bundle; @@ -26,7 +25,6 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; import it.niedermann.nextcloud.deck.ui.manageaccounts.ManageAccountsActivity; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; -import static it.niedermann.nextcloud.deck.ui.MainActivity.ACTIVITY_MANAGE_ACCOUNTS; public class AccountSwitcherDialog extends BrandedDialogFragment { @@ -80,7 +78,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { }); binding.manageAccounts.setOnClickListener((v) -> { - requireActivity().startActivityForResult(new Intent(requireContext(), ManageAccountsActivity.class), ACTIVITY_MANAGE_ACCOUNTS); + requireActivity().startActivity(ManageAccountsActivity.createIntent(requireContext())); dismiss(); }); 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 d6d9cac1e..5a198dec3 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 @@ -10,12 +10,13 @@ import androidx.lifecycle.ViewModelProvider; import java.util.Collections; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.databinding.ActivityArchivedBinding; 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; @@ -24,8 +25,6 @@ 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"; @@ -58,14 +57,7 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa viewModel = new ViewModelProvider(this).get(MainViewModel.class); viewModel.setCurrentAccount(account); - 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()); - } - }); - }); + adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), this::onArchive); binding.recyclerView.setAdapter(adapter); viewModel.getBoards(account.getId(), true).observe(this, (boards) -> { @@ -89,36 +81,62 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa @Override public void onBoardDeleted(Board 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()); + viewModel.deleteBoard(board, new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted board", board.getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } } }); } @Override public void onUpdateBoard(FullBoard 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()); + viewModel.updateBoard(fullBoard, new ResponseCallback<FullBoard>() { + @Override + public void onResponse(FullBoard response) { + DeckLog.info("Successfully updated board", fullBoard.getBoard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } }); } @Override public void onArchive(Board 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()); + viewModel.dearchiveBoard(board, new ResponseCallback<FullBoard>() { + @Override + public void onResponse(FullBoard response) { + DeckLog.info("Successfully dearchived board", response.getBoard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } }); } @Override public void onClone(Board board) { + throw new IllegalStateException("Cloning boards is not available at " + ArchivedBoardsActvitiy.class.getSimpleName()); + } + @Override + public boolean onSupportNavigateUp() { + finish(); // close this activity as oppose to navigating up + return true; } } 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 b5034ebfa..8968cbd72 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,28 +1,28 @@ package it.niedermann.nextcloud.deck.ui.archivedcards; -import android.content.Context; +import android.app.Activity; import android.view.MenuItem; import androidx.annotation.NonNull; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LifecycleOwner; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.ResponseCallback; +import it.niedermann.nextcloud.deck.model.Card; 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.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 MainViewModel viewModel, @NonNull LifecycleOwner lifecycleOwner) { - super(context, fragmentManager, 0L, viewModel, lifecycleOwner, null); + public ArchivedCardsAdapter(@NonNull Activity activity, @NonNull FragmentManager fragmentManager, @NonNull MainViewModel viewModel, @NonNull LifecycleOwner lifecycleOwner) { + super(activity, fragmentManager, 0L, viewModel, lifecycleOwner, null); } @Override @@ -34,18 +34,32 @@ public class ArchivedCardsAdapter extends CardAdapter { 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()); + mainViewModel.dearchiveCard(fullCard, new ResponseCallback<FullCard>() { + @Override + public void onResponse(FullCard response) { + DeckLog.info("Successfully dearchived", Card.class.getSimpleName(), fullCard.getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, 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()); + mainViewModel.deleteCard(fullCard.getCard(), new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName())); + } } }); return true; 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 6f4fe3c74..336e085d7 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 @@ -2,7 +2,6 @@ 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; @@ -37,12 +36,10 @@ public class AttachmentViewHolder extends RecyclerView.ViewHolder { 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.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()); + final String uri = AttachmentUtil.getThumbnailUrl(account, cardRemoteId, attachment, binding.preview.getWidth(), binding.preview.getHeight()); Glide.with(parentContext) .load(uri) .listener(new RequestListener<Drawable>() { 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 6618f7f72..b65b727aa 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 @@ -50,7 +50,7 @@ public class AttachmentsActivity extends AppCompatActivity { supportPostponeEnterTransition(); setSupportActionBar(binding.toolbar); - final Drawable navigationIcon = getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); + final Drawable navigationIcon = ContextCompat.getDrawable(this, R.drawable.ic_arrow_back_white_24dp); DrawableCompat.setTint(navigationIcon, ContextCompat.getColor(this, android.R.color.white)); binding.toolbar.setNavigationIcon(navigationIcon); 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 0a1281fae..e120c665c 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 @@ -28,7 +28,6 @@ import it.niedermann.nextcloud.deck.ui.branding.Branded; import it.niedermann.nextcloud.deck.util.ViewUtil; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements Branded { @@ -93,7 +92,8 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View ViewUtil.addAvatar(acHolder.binding.avatar, account.getUrl(), ac.getUser().getUid(), R.drawable.ic_person_grey600_24dp); acHolder.binding.username.setText(ac.getUser().getDisplayname()); - acHolder.binding.username.setCompoundDrawables(null, null, ac.getStatus() == DBStatus.LOCAL_EDITED.getId() ? context.getResources().getDrawable(R.drawable.ic_sync_blue_24dp) : null, null); + acHolder.binding.username.setCompoundDrawables(null, null, ac.getStatus() == DBStatus.LOCAL_EDITED.getId() + ? ContextCompat.getDrawable(context, R.drawable.ic_sync_blue_24dp) : null, null); acHolder.binding.delete.setOnClickListener((v) -> accessControlChangedListener.deleteAccessControl(ac)); if (hasManagePermission) { @@ -122,13 +122,11 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View accessControlChangedListener.updateAccessControl(ac); }); - if (isBrandingEnabled(context)) { - if (hasManagePermission) { - brandSwitch(context, acHolder.binding.permissionEdit, mainColor); - brandSwitch(context, acHolder.binding.permissionManage, mainColor); - } - brandSwitch(context, acHolder.binding.permissionShare, mainColor); + if (hasManagePermission) { + brandSwitch(context, acHolder.binding.permissionEdit, mainColor); + brandSwitch(context, acHolder.binding.permissionManage, mainColor); } + brandSwitch(context, acHolder.binding.permissionShare, mainColor); break; } } @@ -160,10 +158,8 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View @Override public void applyBrand(int mainColor) { - if (isBrandingEnabled(context)) { - this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); - notifyDataSetChanged(); - } + this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); + notifyDataSetChanged(); } /** 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 78ccd5333..44bbcfe19 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 @@ -18,12 +18,12 @@ import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.databinding.DialogBoardShareBinding; import it.niedermann.nextcloud.deck.model.AccessControl; import it.niedermann.nextcloud.deck.model.User; 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.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; @@ -31,7 +31,6 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedSnackbar; import it.niedermann.nextcloud.deck.ui.card.UserAutoCompleteAdapter; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.board.accesscontrol.AccessControlAdapter.HEADER_ITEM_LOCAL_ID; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText; @@ -101,26 +100,39 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement @Override public void updateAccessControl(AccessControl 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()); + viewModel.updateAccessControl(accessControl, new ResponseCallback<AccessControl>() { + @Override + public void onResponse(AccessControl response) { + DeckLog.info("Successfully updated", AccessControl.class.getSimpleName(), "for user", accessControl.getUser().getDisplayname()); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } }); } @Override public void deleteAccessControl(AccessControl ac) { - final WrappedLiveData<Void> wrappedDeleteLiveData = viewModel.deleteAccessControl(ac); - adapter.remove(ac); - observeOnce(wrappedDeleteLiveData, this, (ignored) -> { - 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())) - .show(); + viewModel.deleteAccessControl(ac, new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted access control for user", ac.getUser().getDisplayname()); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + requireActivity().runOnUiThread(() -> BrandedSnackbar.make(requireView(), getString(R.string.error_revoking_ac, ac.getUser().getDisplayname()), Snackbar.LENGTH_LONG) + .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) + .show()); + } } }); + adapter.remove(ac); } @Override @@ -132,10 +144,16 @@ 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); - 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()); + viewModel.createAccessControl(viewModel.getCurrentAccount().getId(), ac, new ResponseCallback<AccessControl>() { + @Override + public void onResponse(AccessControl response) { + DeckLog.info("Successfully created", AccessControl.class.getSimpleName(), "for user", user.getDisplayname()); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } }); binding.people.setText(""); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsAdapter.java index d8bb57a11..d6b097d89 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsAdapter.java @@ -5,6 +5,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import java.util.LinkedList; @@ -17,7 +18,6 @@ import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.ui.branding.Branded; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; public class ManageLabelsAdapter extends RecyclerView.Adapter<ManageLabelsViewHolder> implements Branded { @@ -33,7 +33,7 @@ public class ManageLabelsAdapter extends RecyclerView.Adapter<ManageLabelsViewHo ManageLabelsAdapter(@NonNull ManageLabelListener listener, @NonNull Context context) { this.listener = listener; this.context = context; - this.mainColor = context.getResources().getColor(R.color.primary); + this.mainColor = ContextCompat.getColor(context, R.color.primary); setHasStableIds(true); } @@ -76,9 +76,7 @@ public class ManageLabelsAdapter extends RecyclerView.Adapter<ManageLabelsViewHo @Override public void applyBrand(int mainColor) { - if (isBrandingEnabled(context)) { - this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); - notifyDataSetChanged(); - } + this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); + notifyDataSetChanged(); } } 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 bc0d98ca3..51ba42dd6 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 @@ -15,10 +15,10 @@ import java.util.Random; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.databinding.DialogBoardManageLabelsBinding; import it.niedermann.nextcloud.deck.model.Label; 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.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.branding.BrandedDeleteAlertDialogBuilder; @@ -80,22 +80,26 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements label.setTitle(binding.addLabelTitle.getText().toString()); label.setColor(colors[new Random().nextInt(colors.length)]); - WrappedLiveData<Label> createLiveData = viewModel.createLabel(viewModel.getCurrentAccount().getId(), label, boardId); - observeOnce(createLiveData, this, (createdLabel) -> { - if (createLiveData.hasError()) { - final Throwable error = createLiveData.getError(); - assert error != null; - if (error instanceof SQLiteConstraintException) { + viewModel.createLabel(viewModel.getCurrentAccount().getId(), label, boardId, new ResponseCallback<Label>() { + @Override + public void onResponse(Label response) { + Toast.makeText(requireContext(), getString(R.string.tag_successfully_added, label.getTitle()), Toast.LENGTH_LONG).show(); + requireActivity().runOnUiThread(() -> { + binding.fab.setEnabled(true); + binding.addLabelTitle.setText(null); + }); + } + + @Override + public void onError(Throwable throwable) { + requireActivity().runOnUiThread(() -> binding.fab.setEnabled(true)); + if (throwable instanceof SQLiteConstraintException) { Toast.makeText(requireContext(), getString(R.string.tag_already_exists, label.getTitle()), Toast.LENGTH_LONG).show(); } else { - Toast.makeText(requireContext(), error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); - DeckLog.logError(error); + Toast.makeText(requireContext(), throwable.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + ResponseCallback.super.onError(throwable); } - } else { - binding.addLabelTitle.setText(null); - Toast.makeText(requireContext(), getString(R.string.tag_successfully_added, label.getTitle()), Toast.LENGTH_LONG).show(); } - binding.fab.setEnabled(true); }); }); binding.addLabelTitle.setOnEditorActionListener((v, actionId, event) -> binding.fab.performClick()); @@ -139,13 +143,18 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements } private void deleteLabel(@NonNull Label label) { - final WrappedLiveData<Void> deleteLiveData = viewModel.deleteLabel(label); - observeOnce(deleteLiveData, this, (v) -> { - if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { - final Throwable error = deleteLiveData.getError(); - assert error != null; - Toast.makeText(requireContext(), error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); - DeckLog.logError(deleteLiveData.getError()); + viewModel.deleteLabel(label, new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted label", label.getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + Toast.makeText(requireContext(), throwable.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + } } }); } @@ -157,16 +166,19 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements @Override public void onLabelUpdated(@NonNull Label label) { - WrappedLiveData<Label> updateLiveData = viewModel.updateLabel(label); - observeOnce(updateLiveData, this, (updatedLabel) -> { - if (updateLiveData.hasError()) { - final Throwable error = updateLiveData.getError(); - assert error != null; + viewModel.updateLabel(label, new ResponseCallback<Label>() { + @Override + public void onResponse(Label label) { + DeckLog.verbose("Successfully update label", label.getTitle()); + } + + @Override + public void onError(Throwable error) { if (error instanceof SQLiteConstraintException) { Toast.makeText(requireContext(), getString(R.string.tag_already_exists, label.getTitle()), Toast.LENGTH_LONG).show(); } else { + ResponseCallback.super.onError(error); Toast.makeText(requireContext(), error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); - DeckLog.logError(updateLiveData.getError()); } } }); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedActivity.java index 3420e888b..03c45305e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedActivity.java @@ -8,7 +8,6 @@ import androidx.appcompat.app.AppCompatActivity; import it.niedermann.nextcloud.deck.R; -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.branding.BrandingUtil.tintMenuIcon; @@ -25,10 +24,8 @@ public abstract class BrandedActivity extends AppCompatActivity implements Brand getTheme().resolveAttribute(R.attr.colorAccent, typedValue, true); colorAccent = typedValue.data; - if (isBrandingEnabled(this)) { - @ColorInt final int mainColor = readBrandMainColor(this); - applyBrand(mainColor); - } + @ColorInt final int mainColor = readBrandMainColor(this); + applyBrand(mainColor); } @Override 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 319df7f79..c84ac8274 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 @@ -21,7 +21,6 @@ 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; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; public class BrandedDatePickerDialog extends DatePickerDialog implements Branded { @@ -31,9 +30,7 @@ public class BrandedDatePickerDialog extends DatePickerDialog implements Branded @Nullable Context context = getContext(); if (context != null) { setThemeDark(isDarkTheme(context)); - if (isBrandingEnabled(context)) { - applyBrand(readBrandMainColor(context)); - } + applyBrand(readBrandMainColor(context)); } return super.onCreateView(inflater, container, savedInstanceState); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDialogFragment.java index adf3844e9..28d4da9ee 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDialogFragment.java @@ -5,7 +5,6 @@ import android.content.Context; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; public abstract class BrandedDialogFragment extends DialogFragment implements Branded { @@ -16,9 +15,7 @@ public abstract class BrandedDialogFragment extends DialogFragment implements Br @Nullable Context context = getContext(); if (context != null) { - if (isBrandingEnabled(context)) { - applyBrand(readBrandMainColor(context)); - } + applyBrand(readBrandMainColor(context)); } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedFragment.java index fe202f48c..87cee2317 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedFragment.java @@ -5,7 +5,6 @@ import android.content.Context; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; public abstract class BrandedFragment extends Fragment implements Branded { @@ -15,8 +14,6 @@ public abstract class BrandedFragment extends Fragment implements Branded { super.onStart(); @Nullable Context context = getContext(); - if (context != null && isBrandingEnabled(context)) { - applyBrand(readBrandMainColor(context)); - } + applyBrand(readBrandMainColor(context)); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedPreferenceCategory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedPreferenceCategory.java index e01c523c6..70db03aa0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedPreferenceCategory.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedPreferenceCategory.java @@ -10,9 +10,8 @@ import androidx.annotation.Nullable; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceViewHolder; +import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountColor; 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.ui.branding.BrandingUtil.readBrandMainColor; public class BrandedPreferenceCategory extends PreferenceCategory { @@ -20,13 +19,11 @@ public class BrandedPreferenceCategory extends PreferenceCategory { public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - if (isBrandingEnabled(getContext())) { - final View v = holder.itemView.findViewById(android.R.id.title); - @Nullable final Context context = getContext(); - if (context != null && v instanceof TextView) { - @ColorInt final int mainColor = getSecondaryForegroundColorDependingOnTheme(context, readBrandMainColor(context)); - ((TextView) v).setTextColor(mainColor); - } + final View v = holder.itemView.findViewById(android.R.id.title); + @Nullable final Context context = getContext(); + if (context != null && v instanceof TextView) { + @ColorInt final int mainColor = getSecondaryForegroundColorDependingOnTheme(context, readCurrentAccountColor(context)); + ((TextView) v).setTextColor(mainColor); } } 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 0159a59dc..8b4cf2f5a 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 @@ -6,15 +6,12 @@ import android.view.View; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.StringRes; -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 static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; public class BrandedSnackbar { @@ -23,12 +20,8 @@ public class BrandedSnackbar { public static Snackbar make( @NonNull View view, @NonNull CharSequence text, @BaseTransientBottomBar.Duration int duration) { final Snackbar snackbar = Snackbar.make(view, text, duration); - if (isBrandingEnabled(view.getContext())) { - @ColorInt final int color = readBrandMainColor(view.getContext()); - snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color); - } else { - snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.defaultBrand)); - } + @ColorInt final int color = readBrandMainColor(view.getContext()); + snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color); return snackbar; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSwitchPreference.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSwitchPreference.java index 463800a0a..607beba03 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSwitchPreference.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSwitchPreference.java @@ -16,7 +16,6 @@ import androidx.preference.SwitchPreference; import it.niedermann.nextcloud.deck.R; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; public class BrandedSwitchPreference extends SwitchPreference implements Branded { @@ -46,7 +45,7 @@ public class BrandedSwitchPreference extends SwitchPreference implements Branded public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - if (isBrandingEnabled(getContext()) && holder.itemView instanceof ViewGroup) { + if (holder.itemView instanceof ViewGroup) { switchView = findSwitchWidget(holder.itemView); if (mainColor != null) { applyBrand(); @@ -58,9 +57,7 @@ public class BrandedSwitchPreference extends SwitchPreference implements Branded public void applyBrand(@ColorInt int mainColor) { this.mainColor = mainColor; // onBindViewHolder is called after applyBrand, therefore we have to store the given values and apply them later. - if (isBrandingEnabled(getContext())) { - applyBrand(); - } + applyBrand(); } private void applyBrand() { 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 2e0c4d3b8..02e09e3c8 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 @@ -21,7 +21,6 @@ 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; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; public class BrandedTimePickerDialog extends TimePickerDialog implements Branded { @@ -31,9 +30,7 @@ public class BrandedTimePickerDialog extends TimePickerDialog implements Branded @Nullable Context context = getContext(); if (context != null) { setThemeDark(isDarkTheme(context)); - if (isBrandingEnabled(context)) { - applyBrand(readBrandMainColor(context)); - } + applyBrand(readBrandMainColor(context)); } return super.onCreateView(inflater, container, savedInstanceState); } 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 b780efec5..863375ae5 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 @@ -31,40 +31,31 @@ public abstract class BrandingUtil { // Util class } - public static boolean isBrandingEnabled(@NonNull Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.pref_key_branding), true); - } - @ColorInt public static int readBrandMainColor(@NonNull Context context) { - if (isBrandingEnabled(context)) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - 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 ContextCompat.getColor(context, R.color.defaultBrand); - } + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + 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)); } public static void saveBrandColors(@NonNull Context context, @ColorInt int mainColor) { - if (isBrandingEnabled(context) && context instanceof BrandedActivity) { + if (context instanceof BrandedActivity) { final BrandedActivity activity = (BrandedActivity) context; activity.applyBrand(mainColor); } - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write: shared_preference_theme_main" + " | " + mainColor); + final SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); + DeckLog.log("--- Write: shared_preference_theme_main |", mainColor); editor.putInt(context.getString(R.string.shared_preference_theme_main), mainColor); editor.apply(); } public static void clearBrandColors(@NonNull Context context) { - if (isBrandingEnabled(context) && context instanceof BrandedActivity) { + if (context instanceof BrandedActivity) { final BrandedActivity activity = (BrandedActivity) context; activity.applyBrand(ContextCompat.getColor(context, R.color.defaultBrand)); } - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write: Remove: shared_preference_theme_main" + " | "); + final SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); + DeckLog.log("--- Write: Remove: shared_preference_theme_main | "); editor.remove(context.getString(R.string.shared_preference_theme_main)); editor.apply(); } 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 87140e544..1e92eb1b3 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,7 +1,7 @@ package it.niedermann.nextcloud.deck.ui.card; +import android.app.Activity; import android.content.ClipData; -import android.content.Context; import android.content.Intent; import android.view.LayoutInflater; import android.view.MenuItem; @@ -24,6 +24,7 @@ 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.api.ResponseCallback; import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding; import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding; import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultOnlyTitleBinding; @@ -32,14 +33,13 @@ 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.Branded; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment; +import it.niedermann.nextcloud.deck.util.CardUtil; 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; @@ -52,7 +52,7 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im protected final FragmentManager fragmentManager; private final long stackId; @NonNull - private final Context context; + protected final Activity activity; @Nullable private final SelectCardListener selectCardListener; @NonNull @@ -66,17 +66,17 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im @StringRes private final int shareLinkRes; - 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); + public CardAdapter(@NonNull Activity activity, @NonNull FragmentManager fragmentManager, long stackId, @NonNull MainViewModel mainViewModel, @NonNull LifecycleOwner lifecycleOwner, @Nullable SelectCardListener selectCardListener) { + this.activity = activity; + this.counterMaxValue = this.activity.getString(R.string.counter_max_value); this.fragmentManager = fragmentManager; this.lifecycleOwner = lifecycleOwner; this.shareLinkRes = mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().getShareLinkResource(); this.stackId = stackId; this.mainViewModel = mainViewModel; this.selectCardListener = selectCardListener; - this.mainColor = ContextCompat.getColor(context, R.color.defaultBrand); - this.compactMode = getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.pref_key_compact), false); + this.mainColor = ContextCompat.getColor(this.activity, R.color.defaultBrand); + this.compactMode = getDefaultSharedPreferences(this.activity).getBoolean(this.activity.getString(R.string.pref_key_compact), false); setHasStableIds(true); } @@ -120,7 +120,7 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im // 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, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId())); + activity.startActivity(EditActivity.createEditCardIntent(activity, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId())); } else { selectCardListener.onCardSelected(fullCard); } @@ -176,7 +176,7 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im @Override public void applyBrand(int mainColor) { - this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); + this.mainColor = getSecondaryForegroundColorDependingOnTheme(activity, mainColor); notifyDataSetChanged(); } @@ -185,15 +185,22 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im int itemId = menuItem.getItemId(); final Account account = mainViewModel.getCurrentAccount(); if (itemId == R.id.share_link) { - Intent shareIntent = new Intent() + final 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(); + .putExtra(Intent.EXTRA_TEXT, account.getUrl() + activity.getString(shareLinkRes, mainViewModel.getCurrentBoardRemoteId(), fullCard.getCard().getId())); + activity.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle())); return true; + } else if (itemId == R.id.share_content) { + final 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, CardUtil.getCardContentAsString(activity, fullCard)); + activity.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle())); } else if (itemId == R.id.action_card_assign) { new Thread(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start(); return true; @@ -202,21 +209,37 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im 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()); + MoveCardDialogFragment + .newInstance(fullCard.getAccountId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getCard().getTitle(), fullCard.getLocalId(), CardUtil.cardHasCommentsOrAttachments(fullCard)) + .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()); + mainViewModel.archiveCard(fullCard, new ResponseCallback<FullCard>() { + @Override + public void onResponse(FullCard response) { + DeckLog.info("Successfully archived", Card.class.getSimpleName(), fullCard.getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName())); } }); 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()); + mainViewModel.deleteCard(fullCard.getCard(), new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName())); + } } }); return true; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java index c0ed29007..68c95bfb6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java @@ -2,8 +2,7 @@ package it.niedermann.nextcloud.deck.ui.card; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.lifecycle.Lifecycle; +import androidx.fragment.app.FragmentActivity; import androidx.viewpager2.adapter.FragmentStateAdapter; import it.niedermann.nextcloud.deck.ui.card.activities.CardActivityFragment; @@ -15,9 +14,8 @@ public class CardTabAdapter extends FragmentStateAdapter { private boolean hasCommentsAbility = false; - @SuppressWarnings("WeakerAccess") - public CardTabAdapter(@NonNull FragmentManager fm, @NonNull Lifecycle lifecycle) { - super(fm, lifecycle); + public CardTabAdapter(final FragmentActivity fa) { + super(fa); } @NonNull 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 2e1ff5e06..15a1a6fb7 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 @@ -14,6 +14,7 @@ import android.view.MenuItem; import android.view.WindowManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.drawable.DrawableCompat; import androidx.lifecycle.ViewModelProvider; @@ -25,7 +26,10 @@ 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.model.Card; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.ocs.Version; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity; import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; @@ -33,7 +37,6 @@ import it.niedermann.nextcloud.deck.util.CardUtil; 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; public class EditActivity extends BrandedActivity { private static final String BUNDLE_KEY_ACCOUNT = "account"; @@ -41,6 +44,7 @@ public class EditActivity extends BrandedActivity { private static final String BUNDLE_KEY_STACK_ID = "stackId"; private static final String BUNDLE_KEY_CARD_ID = "cardId"; private static final String BUNDLE_KEY_TITLE = "title"; + private static final String BUNDLE_KEY_DESCRIPTION = "description"; private ActivityEditBinding binding; private EditCardViewModel viewModel; @@ -76,12 +80,13 @@ public class EditActivity extends BrandedActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + binding = ActivityEditBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(EditCardViewModel.class); + setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - viewModel = new ViewModelProvider(this).get(EditCardViewModel.class); - loadDataFromIntent(); } @@ -122,16 +127,10 @@ public class EditActivity extends BrandedActivity { viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit()); invalidateOptionsMenu(); if (viewModel.isCreateMode()) { - viewModel.initializeNewCard(boardId, args.getLong(BUNDLE_KEY_STACK_ID), account.getServerDeckVersionAsObject().isSupported(this)); + viewModel.initializeNewCard(boardId, args.getLong(BUNDLE_KEY_STACK_ID), account.getServerDeckVersionAsObject().isSupported()); invalidateOptionsMenu(); - String title = args.getString(BUNDLE_KEY_TITLE); - if (!TextUtils.isEmpty(title)) { - if (title.length() > viewModel.getAccount().getServerDeckVersionAsObject().getCardTitleMaxLength()) { - viewModel.getFullCard().getCard().setDescription(title); - } else { - viewModel.getFullCard().getCard().setTitle(title); - } - } + fillTitleAndDescription(viewModel.getFullCard().getCard(), viewModel.getAccount().getServerDeckVersionAsObject(), + args.getString(BUNDLE_KEY_TITLE), args.getString(BUNDLE_KEY_DESCRIPTION)); setupViewPager(); setupTitle(); } else { @@ -143,7 +142,7 @@ public class EditActivity extends BrandedActivity { .setPositiveButton(R.string.simple_close, (a, b) -> super.finish()) .show(); } else { - viewModel.initializeExistingCard(boardId, fullCard, account.getServerDeckVersionAsObject().isSupported(this)); + viewModel.initializeExistingCard(boardId, fullCard, account.getServerDeckVersionAsObject().isSupported()); invalidateOptionsMenu(); setupViewPager(); setupTitle(); @@ -152,7 +151,35 @@ public class EditActivity extends BrandedActivity { } })); - DeckLog.verbose("Finished loading intent data: { accountId = " + viewModel.getAccount().getId() + " , cardId = " + cardId + " }"); + DeckLog.verbose("Finished loading intent data: { accountId =", viewModel.getAccount().getId(), "cardId =", cardId, "}"); + } + + private static void fillTitleAndDescription(@NonNull Card card, @NonNull Version version, @Nullable String title, @Nullable String description) { + if (!TextUtils.isEmpty(title) && !TextUtils.isEmpty(description)) { + assert title != null; + if (title.length() > version.getCardTitleMaxLength()) { + card.setTitle(title.substring(0, version.getCardTitleMaxLength())); + } else { + card.setTitle(title); + } + card.setDescription(description); + } else if (!TextUtils.isEmpty(title)) { + assert title != null; + if (title.length() > version.getCardTitleMaxLength()) { + card.setDescription(title); + card.setTitle(title.substring(0, version.getCardTitleMaxLength())); + } else { + card.setTitle(title); + } + } else if (!TextUtils.isEmpty(description)) { + assert description != null; + if (description.length() > version.getCardTitleMaxLength()) { + card.setDescription(description); + card.setTitle(description.substring(0, version.getCardTitleMaxLength())); + } else { + card.setTitle(description); + } + } } @Override @@ -169,16 +196,21 @@ public class EditActivity extends BrandedActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_card_save) { - saveAndRun(super::finish); + saveAndFinish(); } return super.onOptionsItemSelected(item); } + @Override + public boolean onSupportNavigateUp() { + finish(); // close this activity as oppose to navigating up + return true; + } + /** - * Tries to save the current {@link FullCard} from the {@link EditCardViewModel} and then runs the given {@link Runnable} - * @param runnable + * Tries to save the current {@link FullCard} from the {@link EditCardViewModel} and then finishes this activity. */ - private void saveAndRun(@NonNull Runnable runnable) { + private void saveAndFinish() { if (!viewModel.isPendingCreation()) { viewModel.setPendingCreation(true); final String title = viewModel.getFullCard().getCard().getTitle(); @@ -195,11 +227,13 @@ public class EditActivity extends BrandedActivity { .setOnDismissListener(dialog -> viewModel.setPendingCreation(false)) .show(); } else { - if (viewModel.isCreateMode()) { - observeOnce(viewModel.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run()); - } else { - observeOnce(viewModel.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run()); - } + final WrappedLiveData<FullCard> save$ = viewModel.saveCard(); + save$.observe(this, (fullCard) -> { + if (save$.hasError()) { + DeckLog.logError(save$.getError()); + } + }); + super.finish(); } } } @@ -208,7 +242,7 @@ public class EditActivity extends BrandedActivity { binding.tabLayout.removeAllTabs(); binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); - final CardTabAdapter adapter = new CardTabAdapter(getSupportFragmentManager(), getLifecycle()); + final CardTabAdapter adapter = new CardTabAdapter(this); final TabLayoutMediator mediator = new TabLayoutMediator(binding.tabLayout, binding.pager, (tab, position) -> { tab.setIcon(!viewModel.isCreateMode() && viewModel.hasCommentsAbility() ? tabIconsWithComments[position] @@ -272,7 +306,7 @@ public class EditActivity extends BrandedActivity { 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) -> saveAndRun(super::finish)) + .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndFinish()) .setNegativeButton(R.string.simple_discard, (dialog, whichButton) -> super.finish()).show(); } else { super.finish(); @@ -288,39 +322,43 @@ public class EditActivity extends BrandedActivity { @Override public void applyBrand(int mainColor) { - if (isBrandingEnabled(this)) { - final Drawable navigationIcon = binding.toolbar.getNavigationIcon(); - if (navigationIcon == null) { - DeckLog.error("Expected navigationIcon to be present."); - } else { - DrawableCompat.setTint(binding.toolbar.getNavigationIcon(), colorAccent); - } - applyBrandToPrimaryTabLayout(mainColor, binding.tabLayout); + final Drawable navigationIcon = binding.toolbar.getNavigationIcon(); + if (navigationIcon == null) { + DeckLog.error("Expected navigationIcon to be present."); + } else { + DrawableCompat.setTint(binding.toolbar.getNavigationIcon(), colorAccent); } + applyBrandToPrimaryTabLayout(mainColor, binding.tabLayout); + } + + @NonNull + public static Intent createNewCardIntent(@NonNull Context context, @NonNull Account account, Long boardLocalId, Long stackId, @Nullable String title, @Nullable String description) { + return createNewCardIntent(context, account, boardLocalId, stackId, title) + .putExtra(BUNDLE_KEY_DESCRIPTION, description); } @NonNull - public static Intent createNewCardIntent(@NonNull Context context, @NonNull Account account, Long boardId, Long stackId, @NonNull String title) { - return createNewCardIntent(context, account, boardId, stackId) + public static Intent createNewCardIntent(@NonNull Context context, @NonNull Account account, Long boardLocalId, Long stackId, @Nullable String title) { + return createNewCardIntent(context, account, boardLocalId, stackId) .putExtra(BUNDLE_KEY_TITLE, title); } @NonNull - public static Intent createNewCardIntent(@NonNull Context context, @NonNull Account account, Long boardId, Long stackId) { - return createBasicIntent(context, account, boardId) + public static Intent createNewCardIntent(@NonNull Context context, @NonNull Account account, Long boardLocalId, Long stackId) { + return createBasicIntent(context, account, boardLocalId) .putExtra(BUNDLE_KEY_STACK_ID, stackId); } @NonNull - public static Intent createEditCardIntent(@NonNull Context context, @NonNull Account account, Long boardId, Long cardId) { - return createBasicIntent(context, account, boardId) + public static Intent createEditCardIntent(@NonNull Context context, @NonNull Account account, Long boardLocalId, Long cardId) { + return createBasicIntent(context, account, boardLocalId) .putExtra(BUNDLE_KEY_CARD_ID, cardId); } - private static Intent createBasicIntent(@NonNull Context context, @NonNull Account account, Long boardId) { + private static Intent createBasicIntent(@NonNull Context context, @NonNull Account account, Long boardLocalId) { return new Intent(context, EditActivity.class) .putExtra(BUNDLE_KEY_ACCOUNT, account) - .putExtra(BUNDLE_KEY_BOARD_ID, boardId) + .putExtra(BUNDLE_KEY_BOARD_ID, boardLocalId) .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); } } 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 c754d0800..321f70984 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 @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; import it.niedermann.nextcloud.deck.model.Board; @@ -130,32 +131,33 @@ public class EditCardViewModel extends AndroidViewModel { return syncManager.getFullBoardById(accountId, localId); } - public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) { - return syncManager.createLabel(accountId, label, localBoardId); + public void createLabel(long accountId, Label label, long localBoardId, @NonNull ResponseCallback<Label> callback) { + syncManager.createLabel(accountId, label, localBoardId, callback); } 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); + /** + * Saves the current {@link #fullCard}. If it is a new card, it will be created, otherwise it will be updated. + */ + public WrappedLiveData<FullCard> saveCard() { + return isCreateMode() + ? syncManager.createFullCard(getAccount().getId(), getBoardId(), getFullCard().getCard().getStackId(), getFullCard()) + : syncManager.updateCard(getFullCard()); } 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 void addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file, @NonNull ResponseCallback<Attachment> callback) { + syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file, callback); } - public WrappedLiveData<Void> deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId) { - return syncManager.deleteAttachmentOfCard(accountId, localCardId, localAttachmentId); + public void deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId, @NonNull ResponseCallback<Void> callback) { + syncManager.deleteAttachmentOfCard(accountId, localCardId, localAttachmentId, callback); } public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) { 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 fe974f2a0..de9d23829 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 @@ -12,6 +12,7 @@ import androidx.activity.ComponentActivity; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.lifecycle.LiveData; @@ -68,7 +69,7 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> { binding.label.setTextColor(color); if (ITEM_CREATE == label.getLocalId()) { - final Drawable plusIcon = DrawableCompat.wrap(binding.label.getContext().getResources().getDrawable(R.drawable.ic_plus)); + final Drawable plusIcon = DrawableCompat.wrap(ContextCompat.getDrawable(binding.label.getContext(), R.drawable.ic_plus)); DrawableCompat.setTint(plusIcon, color); binding.label.setChipIcon(plusIcon); } else { 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 index 34d2eb3f3..b18f35de0 100644 --- 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 @@ -24,10 +24,13 @@ 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 it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialog; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -@Deprecated +/** + * TODO maybe this can be merged with {@link PreviewDialog} + */ public class CardAssigneeDialog extends BrandedDialogFragment { private static final String KEY_USER = "user"; 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 533bbe322..51087584e 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 @@ -17,6 +17,7 @@ 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.model.enums.EAttachmentType; import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil; import it.niedermann.nextcloud.deck.util.AttachmentUtil; @@ -26,7 +27,10 @@ public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder { } 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)); + final String downloadUrl = (attachment.getId() == null || cardRemoteId == null) + ? attachment.getLocalPath() + : AttachmentUtil.getCopyDownloadUrl(account, cardRemoteId, attachment); + bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, downloadUrl); } @CallSuper @@ -34,10 +38,15 @@ public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder { 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(EAttachmentType.DECK_FILE.equals(attachment.getType())) { + menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> { + DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName()); + return false; + }); + menu.findItem(R.id.delete).setVisible(true); + } else { + menu.findItem(R.id.delete).setVisible(false); + } if (attachmentUri == null || attachment.getId() == null || cardRemoteId == null) { menu.findItem(android.R.id.copyUrl).setVisible(false); } else { 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 d601f6bbd..16af8bb8f 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,7 +3,6 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.View; @@ -103,7 +102,7 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo 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) { + if (context instanceof Activity) { String transitionName = context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId())); holder.getPreview().setTransitionName(transitionName); context.startActivity(intent, ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) context, holder.getPreview(), transitionName).toBundle()); @@ -115,7 +114,7 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo } case VIEW_TYPE_DEFAULT: default: { - onClickListener = (event) -> openAttachmentInBrowser(context, account.getUrl(), cardRemoteId, attachment.getId()); + onClickListener = (event) -> openAttachmentInBrowser(account, context, cardRemoteId, attachment); break; } } 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 9cb3fb778..0cb59c006 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 @@ -45,12 +45,13 @@ import id.zelory.compressor.constraint.SizeConstraint; import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabAttachmentsBinding; import it.niedermann.nextcloud.deck.exceptions.UploadAttachmentFailedException; import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.enums.DBStatus; 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.branding.BrandedSnackbar; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; @@ -74,7 +75,6 @@ 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; @@ -84,11 +84,10 @@ import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_ 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; +import static it.niedermann.nextcloud.deck.util.FilesUtil.copyContentUriToTempFile; import static java.net.HttpURLConnection.HTTP_CONFLICT; public class CardAttachmentsFragment extends BrandedFragment implements AttachmentDeletedListener, AttachmentClickedListener { @@ -214,15 +213,11 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme if (editViewModel.canEdit()) { binding.fab.setOnClickListener(v -> { - if (SDK_INT < LOLLIPOP) { - openNativeFilePicker(); - } else { - binding.bottomNavigation.setSelectedItemId(R.id.gallery); - showGalleryPicker(); - mBottomSheetBehaviour.setState(STATE_COLLAPSED); - backPressedCallback.setEnabled(true); - requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback); - } + binding.bottomNavigation.setSelectedItemId(R.id.gallery); + showGalleryPicker(); + mBottomSheetBehaviour.setState(STATE_COLLAPSED); + backPressedCallback.setEnabled(true); + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback); }); binding.fab.show(); binding.attachmentsList.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -239,9 +234,7 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme binding.emptyContentView.hideDescription(); } @Nullable Context context = requireContext(); - applyBrand(isBrandingEnabled(context) - ? readBrandMainColor(context) - : ContextCompat.getColor(context, R.color.defaultBrand)); + applyBrand(readBrandMainColor(context)); return binding.getRoot(); } @@ -309,35 +302,29 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme 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); - } +// 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); } } } 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()); - } + startActivityForResult(TakePhotoActivity.createIntent(requireContext()), REQUEST_CODE_PICK_CAMERA); } private void openNativeContactPicker() { @@ -412,28 +399,38 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme super.onDestroy(); } - private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) throws UploadAttachmentFailedException, IOException { + private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) throws UploadAttachmentFailedException { 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 originalFile = copyContentUriToTempFile(requireContext(), sourceUri, editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId()); - if (MimeTypeUtil.isImage(mimeType)) { - JavaCompressor.compress( - (AppCompatActivity) requireActivity(), - originalFile, - (status, file) -> uploadNewAttachmentFromFile(status ? file : originalFile, mimeType), - new ResolutionConstraint(1920, 1920), - new SizeConstraint(1_000_000, 10, 10, 10), - new FormatConstraint(Bitmap.CompressFormat.JPEG), - new QualityConstraint(80) - ); - } else { - uploadNewAttachmentFromFile(originalFile, mimeType); - } + DeckLog.verbose("--- found content URL", sourceUri.getPath()); + // Separate Thread required because picked file might not yet be locally available + // https://github.com/stefan-niedermann/nextcloud-deck/issues/814 + new Thread(() -> { + try { + final File originalFile = copyContentUriToTempFile(requireContext(), sourceUri, editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId()); + requireActivity().runOnUiThread(() -> { + if (MimeTypeUtil.isImage(mimeType)) { + JavaCompressor.compress( + (AppCompatActivity) requireActivity(), + originalFile, + (status, file) -> uploadNewAttachmentFromFile(status ? file : originalFile, mimeType), + new ResolutionConstraint(1920, 1920), + new SizeConstraint(1_000_000, 10, 10, 10), + new FormatConstraint(Bitmap.CompressFormat.JPEG), + new QualityConstraint(80) + ); + } else { + uploadNewAttachmentFromFile(originalFile, mimeType); + } + }); + } catch (IOException e) { + requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(e, editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } + }).start(); break; } default: { @@ -464,22 +461,29 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme 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.addAttachmentToCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload, new ResponseCallback<Attachment>() { + @Override + public void onResponse(Attachment response) { + requireActivity().runOnUiThread(() -> { 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 { - editViewModel.getFullCard().getAttachments().remove(a); - editViewModel.getFullCard().getAttachments().add(0, next); - adapter.replaceAttachment(a, next); + editViewModel.getFullCard().getAttachments().add(0, response); + adapter.replaceAttachment(a, response); + }); + } + + @Override + public void onError(Throwable throwable) { + requireActivity().runOnUiThread(() -> { + if (throwable instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) throwable).getStatusCode() == HTTP_CONFLICT) { + ResponseCallback.super.onError(throwable); + // 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", throwable), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } }); } @@ -522,10 +526,18 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme adapter.removeAttachment(attachment); 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()); + editViewModel.deleteAttachmentOfCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), attachment.getLocalId(), new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully delete", Attachment.class.getSimpleName(), attachment.getFilename(), "from", Card.class.getSimpleName(), editViewModel.getFullCard().getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } } }); } @@ -539,7 +551,6 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme @Override public void applyBrand(int mainColor) { applyBrandToFAB(mainColor, binding.fab); - adapter.applyBrand(mainColor); @ColorInt final int finalMainColor = DeckColorUtil.contrastRatioIsSufficient(mainColor, primaryColor) ? mainColor : accentColor; @@ -555,6 +566,13 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme ); binding.bottomNavigation.setItemIconTintList(list); binding.bottomNavigation.setItemTextColor(list); + + // applyBrand() is also called onStart + // adapter might be null at this point + // https://github.com/stefan-niedermann/nextcloud-deck/issues/782 + if (adapter != null) { + adapter.applyBrand(mainColor); + } } public static Fragment newInstance() { 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 2b5358eb9..b41a4e9d1 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 @@ -40,7 +40,7 @@ public class DefaultAttachmentViewHolder extends AttachmentViewHolder { 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())); + itemView.setOnClickListener((event) -> openAttachmentInBrowser(account, itemView.getContext(), cardRemoteId, attachment)); binding.filename.setText(attachment.getBasename()); binding.filesize.setText(Formatter.formatFileSize(binding.filesize.getContext(), attachment.getFilesize())); if (attachment.getLastModifiedLocal() != null) { 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 3c95da1b7..b93b3d025 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 @@ -37,10 +37,13 @@ public class ImageAttachmentViewHolder extends AttachmentViewHolder { } 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)); + final String downloadUrl = (attachment.getId() == null || cardRemoteId == null) + ? attachment.getLocalPath() + : AttachmentUtil.getCopyDownloadUrl(account, cardRemoteId, attachment); + super.bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, downloadUrl); getPreview().post(() -> { - @Nullable final String uri = AttachmentUtil.getThumbnailUrl(account.getServerDeckVersionAsObject(), account.getUrl(), cardRemoteId, attachment, getPreview().getWidth()); + @Nullable final String uri = AttachmentUtil.getThumbnailUrl(account, cardRemoteId, attachment, getPreview().getWidth()); Glide.with(getPreview().getContext()) .load(uri) .placeholder(R.drawable.ic_image_grey600_24dp) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java index 169a37c79..4fde5a4e1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java @@ -11,7 +11,9 @@ import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; @@ -21,7 +23,7 @@ import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMai public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHolder> { - private int mainColor; + private final int mainColor; @NonNull private final List<FullDeckComment> comments = new ArrayList<>(); @NonNull @@ -34,13 +36,16 @@ public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHol private final CommentSelectAsReplyListener selectAsReplyListener; @NonNull private final FragmentManager fragmentManager; + @NonNull + private final CommentEditedListener editListener; - CardCommentsAdapter(@NonNull Context context, @NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager) { + CardCommentsAdapter(@NonNull Context context, @NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager, CommentEditedListener editListener) { this.account = account; this.menuInflater = menuInflater; this.deletedListener = deletedListener; this.selectAsReplyListener = selectAsReplyListener; this.fragmentManager = fragmentManager; + this.editListener = editListener; this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, readBrandMainColor(context)); setHasStableIds(true); } @@ -58,7 +63,19 @@ public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHol @Override public void onBindViewHolder(@NonNull ItemCommentViewHolder holder, int position) { - holder.bind(comments.get(position), account, mainColor, menuInflater, deletedListener, selectAsReplyListener, fragmentManager); + final FullDeckComment comment = comments.get(position); + holder.bind(comment, account, mainColor, menuInflater, deletedListener, selectAsReplyListener, fragmentManager, (changedText) -> { + if (!Objects.equals(changedText, comment.getComment().getMessage())) { + DeckLog.info("Toggled checkbox in comment with localId", comment.getLocalId()); + this.editListener.onCommentEdited(comment.getLocalId(), changedText.toString()); + } + }); + } + + @Override + public void onViewRecycled(@NonNull ItemCommentViewHolder holder) { + super.onViewRecycled(holder); + holder.unbind(); } @SuppressWarnings("WeakerAccess") 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 3fd536aa6..2f538f9bb 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 @@ -17,24 +17,24 @@ import androidx.lifecycle.ViewModelProvider; import java.time.Instant; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabCommentsBinding; 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 it.niedermann.nextcloud.deck.util.ViewUtil; 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; public class CardCommentsFragment extends BrandedFragment implements CommentEditedListener, CommentDeletedListener, CommentSelectAsReplyListener { @@ -70,17 +70,18 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class); - adapter = new CardCommentsAdapter(requireContext(), mainViewModel.getAccount(), requireActivity().getMenuInflater(), this, this, getChildFragmentManager()); + adapter = new CardCommentsAdapter(requireContext(), mainViewModel.getAccount(), requireActivity().getMenuInflater(), this, this, getChildFragmentManager(), this); binding.comments.setAdapter(adapter); - binding.replyCommentCancelButton.setOnClickListener((v) -> commentsViewModel.setReplyToComment(null)); + ViewUtil.addAvatar(binding.avatar, mainViewModel.getAccount().getUrl(), mainViewModel.getAccount().getUserName(), DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp); + commentsViewModel.getReplyToComment().observe(getViewLifecycleOwner(), (comment) -> { if (comment == null) { binding.replyComment.setVisibility(GONE); } else { - binding.replyCommentText.setText(comment.getComment().getMessage()); + binding.replyCommentText.setMarkdownString(comment.getComment().getMessage()); binding.replyComment.setVisibility(VISIBLE); - setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText); +// setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText); } }); commentsViewModel.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(), @@ -140,10 +141,18 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit @Override public void onCommentDeleted(Long 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()); + commentsViewModel.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId, new ResponseCallback<Void>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted comment with localId", localId); + } + + @Override + public void onError(Throwable throwable) { + if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + ResponseCallback.super.onError(throwable); + requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } } }); } 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 dada94d5b..7e29769cd 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 @@ -9,10 +9,12 @@ import androidx.lifecycle.MutableLiveData; import java.util.List; +import it.niedermann.nextcloud.deck.api.ResponseCallback; 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 static androidx.lifecycle.Transformations.distinctUntilChanged; @SuppressWarnings("WeakerAccess") public class CommentsViewModel extends AndroidViewModel { @@ -35,7 +37,7 @@ public class CommentsViewModel extends AndroidViewModel { } public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) { - return syncManager.getFullCommentsForLocalCardId(localCardId); + return distinctUntilChanged(syncManager.getFullCommentsForLocalCardId(localCardId)); } public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) { @@ -46,7 +48,7 @@ public class CommentsViewModel extends AndroidViewModel { syncManager.updateComment(accountId, localCardId, localCommentId, comment); } - public WrappedLiveData<Void> deleteComment(long accountId, long localCardId, long localCommentId) { - return syncManager.deleteComment(accountId, localCardId, localCommentId); + public void deleteComment(long accountId, long localCardId, long localCommentId, @NonNull ResponseCallback<Void> callback) { + syncManager.deleteComment(accountId, localCardId, localCommentId, callback); } } 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 3e540c95e..c85f9d7b5 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 @@ -1,5 +1,6 @@ package it.niedermann.nextcloud.deck.ui.card.comments; +import android.text.method.LinkMovementMethod; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; @@ -14,6 +15,9 @@ import androidx.recyclerview.widget.RecyclerView; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; import it.niedermann.android.util.ClipboardUtil; import it.niedermann.android.util.DimensionUtil; @@ -21,12 +25,11 @@ 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.Mention; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; import it.niedermann.nextcloud.deck.util.DateUtil; import it.niedermann.nextcloud.deck.util.ViewUtil; -import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions; - public class ItemCommentViewHolder extends RecyclerView.ViewHolder { private final ItemCommentBinding binding; private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); @@ -35,15 +38,22 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { public ItemCommentViewHolder(ItemCommentBinding binding) { super(binding.getRoot()); this.binding = binding; + this.binding.message.setMovementMethod(LinkMovementMethod.getInstance()); } - public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @ColorInt int mainColor, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager) { + public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @ColorInt int mainColor, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager, @NonNull Consumer<CharSequence> editListener) { 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); + final Map<String, String> mentions = new HashMap<>(comment.getComment().getMentions().size()); + for (Mention mention : comment.getComment().getMentions()) { + mentions.put(mention.getMentionId(), mention.getMentionDisplayName()); + } binding.message.setText(comment.getComment().getMessage()); + binding.message.setMarkdownString(comment.getComment().getMessage(), mentions); + binding.message.setMarkdownStringChangedListener(editListener); binding.actorDisplayName.setText(comment.getComment().getActorDisplayName()); binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().toEpochMilli())); - itemView.setOnClickListener(View::showContextMenu); + itemView.setOnClickListener(View::showContextMenu); itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { inflater.inflate(R.menu.comment_menu, menu); menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), comment.getComment().getMessage())); @@ -72,12 +82,10 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { } }); + TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter)); DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor); binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(comment.getStatusEnum()) ? View.VISIBLE : View.GONE); - TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter)); - setupMentions(account, comment.getComment().getMentions(), binding.message); - if (comment.getParent() == null) { binding.parentContainer.setVisibility(View.GONE); } else { @@ -95,4 +103,9 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { }); } } + + public void unbind() { + binding.message.setText(""); + binding.message.setMarkdownStringChangedListener(null); + } }
\ No newline at end of file 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 index 5251291c8..8fa24988d 100644 --- 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 @@ -1,12 +1,14 @@ package it.niedermann.nextcloud.deck.ui.card.comments.util; +import androidx.annotation.Nullable; import androidx.core.util.Pair; public class CommentsUtil { + @Nullable public static Pair<String, Integer> getUserNameForMentionProposal(String text, int cursorPosition) { - Pair result = null; + Pair<String, Integer> result = null; if (text != null) { // find start of relevant substring 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 2c697de08..8376a6817 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 @@ -4,9 +4,8 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,6 +18,7 @@ import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.GridLayoutManager; @@ -28,8 +28,6 @@ import com.wdullaer.materialdatetimepicker.date.DatePickerDialog; import com.wdullaer.materialdatetimepicker.date.DatePickerDialog.OnDateSetListener; import com.wdullaer.materialdatetimepicker.time.TimePickerDialog; import com.wdullaer.materialdatetimepicker.time.TimePickerDialog.OnTimeSetListener; -import com.yydcdut.markdown.MarkdownProcessor; -import com.yydcdut.markdown.syntax.edit.EditFactory; import java.time.Instant; import java.time.LocalDate; @@ -43,10 +41,11 @@ 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.api.ResponseCallback; 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.adapters.db.util.WrappedLiveData; +import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.ui.branding.BrandedDatePickerDialog; import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; import it.niedermann.nextcloud.deck.ui.branding.BrandedSnackbar; @@ -57,11 +56,9 @@ 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.MarkDownUtil; 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; public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener, CardAssigneeListener { @@ -72,6 +69,7 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); private AppCompatActivity activity; + boolean editorActive = true; @Override public void onAttach(@NonNull Context context) { @@ -110,7 +108,6 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis setupDueDate(); setupDescription(); setupProjects(); - binding.description.setText(viewModel.getFullCard().getCard().getDescription()); return binding.getRoot(); } @@ -137,35 +134,48 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis applyBrandToEditText(mainColor, binding.dueDateDate); applyBrandToEditText(mainColor, binding.dueDateTime); applyBrandToEditText(mainColor, binding.people); - applyBrandToEditText(mainColor, binding.description); + binding.descriptionEditor.setSearchColor(mainColor); + binding.descriptionViewer.setSearchColor(mainColor); } private void setupDescription() { if (viewModel.canEdit()) { - MarkdownProcessor markdownProcessor = new MarkdownProcessor(requireContext()); - markdownProcessor.config(MarkDownUtil.getMarkDownConfiguration(binding.description.getContext()).build()); - markdownProcessor.factory(EditFactory.create()); - markdownProcessor.live(binding.description); - binding.description.addTextChangedListener(new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (viewModel.getFullCard() != null) { - viewModel.getFullCard().getCard().setDescription(binding.description.getText().toString()); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Nothing to do + binding.descriptionViewer.setMovementMethod(LinkMovementMethod.getInstance()); + binding.descriptionBar.setOnClickListener((v) -> binding.descriptionEditor.requestFocus()); + binding.descriptionToggle.setOnClickListener((v) -> { + editorActive = !editorActive; + if (editorActive) { + binding.descriptionEditor.setMarkdownString(viewModel.getFullCard().getCard().getDescription()); + binding.descriptionBar.setOnClickListener((view) -> binding.descriptionEditor.requestFocus()); + binding.descriptionEditor.setVisibility(VISIBLE); + binding.descriptionViewer.setVisibility(GONE); + binding.descriptionToggle.setImageResource(R.drawable.ic_baseline_eye_24); + } else { + binding.descriptionViewer.setMarkdownString(viewModel.getFullCard().getCard().getDescription()); + binding.descriptionBar.setOnClickListener(null); + binding.descriptionEditor.setVisibility(GONE); + binding.descriptionViewer.setVisibility(VISIBLE); + binding.descriptionToggle.setImageResource(R.drawable.ic_edit_grey600_24dp); } + }); + binding.descriptionEditor.setMarkdownString(viewModel.getFullCard().getCard().getDescription()); + final Observer<CharSequence> descriptionObserver = (description) -> { - @Override - public void afterTextChanged(Editable s) { - // Nothing to do + if (viewModel.getFullCard() != null) { + viewModel.getFullCard().getCard().setDescription(description == null ? "" : description.toString()); + } else { + ExceptionDialogFragment.newInstance(new IllegalStateException(FullCard.class.getSimpleName() + " was empty when trying to setup description"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } - }); + binding.descriptionToggle.setVisibility(TextUtils.isEmpty(description) ? GONE : VISIBLE); + }; + binding.descriptionEditor.getMarkdownString().observe(getViewLifecycleOwner(), descriptionObserver); + binding.descriptionViewer.getMarkdownString().observe(getViewLifecycleOwner(), descriptionObserver); } else { - binding.description.setEnabled(false); + binding.descriptionEditor.setEnabled(false); + binding.descriptionEditor.setVisibility(VISIBLE); + binding.descriptionViewer.setEnabled(false); + binding.descriptionViewer.setVisibility(GONE); + binding.descriptionViewer.setMarkdownString(viewModel.getFullCard().getCard().getDescription()); } } @@ -233,18 +243,23 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis newLabel.setBoardId(boardId); newLabel.setTitle(((LabelAutoCompleteAdapter) binding.labels.getAdapter()).getLastFilterText()); newLabel.setLocalId(null); - WrappedLiveData<Label> createLabelLiveData = viewModel.createLabel(accountId, newLabel, boardId); - observeOnce(createLabelLiveData, CardDetailsFragment.this, createdLabel -> { - if (createLabelLiveData.hasError()) { - DeckLog.logError(createLabelLiveData.getError()); - BrandedSnackbar.make(requireView(), getString(R.string.error_create_label, newLabel.getTitle()), Snackbar.LENGTH_LONG) - .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(createLabelLiveData.getError(), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())).show(); - } else { - newLabel.setLocalId(createdLabel.getLocalId()); - ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(createdLabel); - viewModel.getFullCard().getLabels().add(createdLabel); - binding.labelsGroup.addView(createChipFromLabel(newLabel)); - binding.labelsGroup.setVisibility(VISIBLE); + viewModel.createLabel(accountId, newLabel, boardId, new ResponseCallback<Label>() { + @Override + public void onResponse(Label response) { + requireActivity().runOnUiThread(() -> { + newLabel.setLocalId(response.getLocalId()); + ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(response); + viewModel.getFullCard().getLabels().add(response); + binding.labelsGroup.addView(createChipFromLabel(newLabel)); + binding.labelsGroup.setVisibility(VISIBLE); + }); + } + + @Override + public void onError(Throwable throwable) { + ResponseCallback.super.onError(throwable); + requireActivity().runOnUiThread(() -> BrandedSnackbar.make(requireView(), getString(R.string.error_create_label, newLabel.getTitle()), Snackbar.LENGTH_LONG) + .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())).show()); } }); } else { 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 index 272945e45..c398d9814 100644 --- 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 @@ -86,14 +86,14 @@ public class CardProjectResourceViewHolder extends RecyclerView.ViewHolder { break; } default: { - DeckLog.info("Unknown resource type for " + resource.getName() + ": " + resource.getType()); + 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"); + DeckLog.warn("Resource type for", resource.getName(), "is null"); binding.type.setVisibility(GONE); } } 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 6bfd82b13..b87cee5f9 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 @@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.ui.exception.tips; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; @@ -16,6 +17,7 @@ import androidx.annotation.StringRes; import androidx.core.util.Consumer; import androidx.recyclerview.widget.RecyclerView; +import com.nextcloud.android.sso.Constants; import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotSupportedException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; @@ -39,14 +41,15 @@ import static it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment. public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { + private static final String[] APPS = new String[]{Constants.PACKAGE_NAME_PROD, Constants.PACKAGE_NAME_DEV}; 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; + private final Consumer<Intent> actionButtonClickedListener; @NonNull - private List<TipsModel> tips = new LinkedList<>(); + private final List<TipsModel> tips = new LinkedList<>(); public TipsAdapter(@NonNull Consumer<Intent> actionButtonClickedListener) { this.actionButtonClickedListener = actionButtonClickedListener; @@ -113,6 +116,19 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { } } else if (throwable instanceof UploadAttachmentFailedException) { add(R.string.error_dialog_attachment_upload_failed); + } else if (throwable instanceof ClassNotFoundException) { + final Throwable cause = ((ClassNotFoundException) throwable).getCause(); + if (cause != null) { + final String message = cause.getMessage(); + if (message != null && message.toLowerCase().contains("certificate")) { + final Intent filesOpenIntent = getOpenFilesIntent(context); + if (filesOpenIntent == null) { + add(R.string.error_dialog_certificate); + } else { + add(R.string.error_dialog_certificate, filesOpenIntent); + } + } + } } else if (throwable instanceof DeckException) { switch (((DeckException) throwable).getHint()) { case CAPABILITIES_VERSION_NOT_PARSABLE: @@ -142,7 +158,7 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { 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")) { + if (throwable.getMessage() != null && throwable.getMessage().toLowerCase().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); @@ -159,4 +175,18 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { tips.add(new TipsModel(text, primaryAction)); notifyItemInserted(tips.size()); } + + @Nullable + private static Intent getOpenFilesIntent(@NonNull Context context) { + final PackageManager pm = context.getPackageManager(); + for (String app : APPS) { + try { + pm.getPackageInfo(app, PackageManager.GET_ACTIVITIES); + return pm.getLaunchIntentForPackage(app) + .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_nextcloud_app); + } catch (PackageManager.NameNotFoundException ignored) { + } + } + return null; + } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsModel.java index 00bfa6b15..749579812 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsModel.java @@ -8,10 +8,9 @@ import androidx.annotation.StringRes; @SuppressWarnings("WeakerAccess") public class TipsModel { @StringRes - private int text; + private final int text; @Nullable - private - Intent actionIntent; + private final Intent actionIntent; TipsModel(@StringRes int text, @Nullable Intent actionIntent) { this.text = text; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsViewHolder.java index 622a32e3b..7d254459e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsViewHolder.java @@ -20,7 +20,7 @@ public class TipsViewHolder extends RecyclerView.ViewHolder { binding = ItemTipBinding.bind(itemView); } - public void bind(TipsModel tip, Consumer<Intent> actionButtonClickedListener) { + public void bind(@NonNull TipsModel tip, @NonNull Consumer<Intent> actionButtonClickedListener) { binding.tip.setText(tip.getText()); final Intent actionIntent = tip.getActionIntent(); if (actionIntent != null && actionIntent.hasExtra(INTENT_EXTRA_BUTTON_TEXT)) { 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 6aa03b811..647dfd651 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 @@ -11,8 +11,6 @@ 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; -import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModelProvider; import androidx.viewpager2.adapter.FragmentStateAdapter; @@ -49,14 +47,14 @@ public class FilterDialogFragment extends BrandedDialogFragment { indicator = ContextCompat.getDrawable(requireContext(), R.drawable.circle_grey600_8dp); assert indicator != null; - indicator.setColorFilter(getResources().getColor(R.color.defaultBrand), PorterDuff.Mode.SRC_ATOP); + indicator.setColorFilter(ContextCompat.getColor(getContext(), R.color.defaultBrand), PorterDuff.Mode.SRC_ATOP); filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); final AlertDialog.Builder dialogBuilder = new BrandedAlertDialogBuilder(requireContext()); binding = DialogFilterBinding.inflate(requireActivity().getLayoutInflater()); - binding.viewPager.setAdapter(new TabsPagerAdapter(getChildFragmentManager(), getLifecycle())); + binding.viewPager.setAdapter(new TabsPagerAdapter(this)); binding.viewPager.setOffscreenPageLimit(tabTitles.length); LiveData<FilterInformation> filterInformationDraft = filterViewModel.getFilterInformationDraft(); @@ -114,8 +112,8 @@ public class FilterDialogFragment extends BrandedDialogFragment { private static class TabsPagerAdapter extends FragmentStateAdapter { - TabsPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) { - super(fragmentManager, lifecycle); + TabsPagerAdapter(final Fragment f) { + super(f); } @NonNull 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 8aa45e39a..aec258d59 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 @@ -1,8 +1,11 @@ package it.niedermann.nextcloud.deck.ui.manageaccounts; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProvider; @@ -51,7 +54,6 @@ public class ManageAccountsActivity extends AppCompatActivity { viewModel.readAccounts().observe(this, (localAccounts -> { if (localAccounts.size() == 0) { Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName()); - setResult(AppCompatActivity.RESULT_FIRST_USER); finish(); } else { adapter.setAccounts(localAccounts); @@ -64,4 +66,8 @@ public class ManageAccountsActivity extends AppCompatActivity { public void onBackPressed() { onSupportNavigateUp(); } + + public static Intent createIntent(@NonNull Context context) { + return new Intent(context, ManageAccountsActivity.class); + } } 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 index 66e9d3850..da89f3588 100644 --- 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 @@ -13,7 +13,7 @@ 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; +import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccount; @SuppressWarnings("WeakerAccess") public class ManageAccountsViewModel extends AndroidViewModel { @@ -36,7 +36,7 @@ public class ManageAccountsViewModel extends AndroidViewModel { public void setNewAccount(@NonNull Account account) { SingleAccountHelper.setCurrentAccount(getApplication(), account.getName()); syncManager = new SyncManager(getApplication()); - saveCurrentAccountId(getApplication(), account.getId()); + saveCurrentAccount(getApplication(), account); } public void deleteAccount(long 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 index 2b7eb52fe..442f2c6eb 100644 --- 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 @@ -32,10 +32,12 @@ public class MoveCardDialogFragment extends BrandedDialogFragment implements Pic 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 static final String KEY_ORIGIN_CARD_HAS_ATTACHMENTS_OR_COMMENTS = "card_has_attachments_or_comments"; private Long originAccountId; private Long originBoardLocalId; private String originCardTitle; private Long originCardLocalId; + private boolean originCardHasAttachmentsOrComments; private DialogMoveCardBinding binding; private PickStackViewModel viewModel; @@ -69,6 +71,7 @@ public class MoveCardDialogFragment extends BrandedDialogFragment implements Pic if (originBoardLocalId < 0) { throw new IllegalArgumentException("Missing " + KEY_ORIGIN_BOARD_LOCAL_ID); } + originCardHasAttachmentsOrComments = args.getBoolean(KEY_ORIGIN_CARD_HAS_ATTACHMENTS_OR_COMMENTS, false); originCardTitle = args.getString(KEY_ORIGIN_CARD_TITLE); } @@ -78,7 +81,7 @@ public class MoveCardDialogFragment extends BrandedDialogFragment implements Pic 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()); + 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(); }); @@ -104,7 +107,7 @@ public class MoveCardDialogFragment extends BrandedDialogFragment implements Pic binding.moveWarning.setVisibility(GONE); } else { binding.submit.setEnabled(true); - binding.moveWarning.setVisibility(board.getLocalId().equals(originBoardLocalId) ? GONE : VISIBLE); + binding.moveWarning.setVisibility(originCardHasAttachmentsOrComments && !board.getLocalId().equals(originBoardLocalId) ? VISIBLE : GONE); } } @@ -115,13 +118,14 @@ public class MoveCardDialogFragment extends BrandedDialogFragment implements Pic binding.submit.setTextColor(mainColorStateList); } - public static DialogFragment newInstance(long originAccountId, long originBoardLocalId, String originCardTitle, Long originCardLocalId) { + public static DialogFragment newInstance(long originAccountId, long originBoardLocalId, String originCardTitle, Long originCardLocalId, boolean hasAttachmentsOrComments) { 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); + args.putBoolean(KEY_ORIGIN_CARD_HAS_ATTACHMENTS_OR_COMMENTS, hasAttachmentsOrComments); dialogFragment.setArguments(args); return dialogFragment; } 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 index d65971cf4..6408f6b64 100644 --- 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 @@ -1,7 +1,6 @@ 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; @@ -142,7 +141,7 @@ public class PickStackFragment extends Fragment { if (hasAccounts) { return viewModel.readAccounts(); } else { - startActivityForResult(new Intent(requireActivity(), ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + startActivityForResult(ImportAccountActivity.createIntent(requireContext()), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); return null; } }).observe(getViewLifecycleOwner(), (List<Account> accounts) -> { 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 f537c9fb4..9534b51b4 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 @@ -50,7 +50,7 @@ public class AccountAdapter extends AbstractAdapter<Account> { } Glide.with(getContext()) - .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details)))) + .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.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/preparecreate/PrepareCreateActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java index 18317e078..53e25a0d9 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 @@ -1,9 +1,7 @@ package it.niedermann.nextcloud.deck.ui.preparecreate; -import android.content.ClipData; import android.content.Intent; import android.os.Bundle; -import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -13,7 +11,7 @@ import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.ui.PickStackActivity; import it.niedermann.nextcloud.deck.ui.card.EditActivity; -import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId; +import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccount; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardId; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentStackId; @@ -30,14 +28,16 @@ public class PrepareCreateActivity extends PickStackActivity { @Override protected void onSubmit(Account account, long boardId, long stackId) { - final String receivedClipData = getReceivedClipData(getIntent()); - if (receivedClipData == null) { + final Intent intent = getIntent(); + if (intent == null) { startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId)); } else { - startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData)); + startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, + intent.getStringExtra(Intent.EXTRA_TITLE), + intent.getStringExtra(Intent.EXTRA_TEXT))); } - saveCurrentAccountId(this, account.getId()); + saveCurrentAccount(this, account); saveCurrentBoardId(this, account.getId(), boardId); saveCurrentStackId(this, account.getId(), boardId, stackId); applyBrand(account.getColor()); @@ -49,25 +49,4 @@ public class PrepareCreateActivity extends PickStackActivity { protected boolean showBoardsWithoutEditPermission() { return false; } - - @Nullable - private static String getReceivedClipData(@Nullable Intent intent) { - if (intent == null) { - return null; - } - final ClipData clipData = intent.getClipData(); - if (clipData == null) { - return null; - } - final int itemCount = clipData.getItemCount(); - if (itemCount <= 0) { - return null; - } - final ClipData.Item item = clipData.getItemAt(0); - if (item == null) { - return null; - } - final CharSequence text = item.getText(); - return TextUtils.isEmpty(text) ? null : text.toString(); - } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java index 6f5a9a7e8..db0c4147c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java @@ -1,24 +1,25 @@ package it.niedermann.nextcloud.deck.ui.settings; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ActivitySettingsBinding; -import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; -public class SettingsActivity extends BrandedActivity { - - private ActivitySettingsBinding binding; +public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); - binding = ActivitySettingsBinding.inflate(getLayoutInflater()); + final ActivitySettingsBinding binding = ActivitySettingsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); @@ -36,8 +37,8 @@ public class SettingsActivity extends BrandedActivity { return true; } - @Override - public void applyBrand(int mainColor) { - // Nothing to do... + @NonNull + public static Intent createIntent(@NonNull Context context) { + return new Intent(context, SettingsActivity.class); } } 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 04429f225..91c7ad995 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 @@ -1,29 +1,30 @@ package it.niedermann.nextcloud.deck.ui.settings; import android.app.Activity; -import android.content.Context; import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import it.niedermann.nextcloud.deck.DeckApplication; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker; -import it.niedermann.nextcloud.deck.ui.branding.Branded; import it.niedermann.nextcloud.deck.ui.branding.BrandedSwitchPreference; import static it.niedermann.nextcloud.deck.DeckApplication.setAppTheme; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; -public class SettingsFragment extends PreferenceFragmentCompat implements Branded { +public class SettingsFragment extends PreferenceFragmentCompat { private BrandedSwitchPreference wifiOnlyPref; - private BrandedSwitchPreference themePref; - private BrandedSwitchPreference brandingPref; private BrandedSwitchPreference compactPref; + private BrandedSwitchPreference debuggingPref; + private BrandedSwitchPreference eTagPref; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -34,38 +35,23 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande if (wifiOnlyPref != null) { wifiOnlyPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { final Boolean syncOnWifiOnly = (Boolean) newValue; - DeckLog.log("syncOnWifiOnly: " + syncOnWifiOnly); + DeckLog.log("syncOnWifiOnly:", syncOnWifiOnly); return true; }); } else { - DeckLog.error("Could not find preference with key: \"" + getString(R.string.pref_key_wifi_only) + "\""); + DeckLog.error("Could not find preference with key: ", getString(R.string.pref_key_wifi_only)); } - themePref = findPreference(getString(R.string.pref_key_dark_theme)); + Preference themePref = findPreference(getString(R.string.pref_key_dark_theme)); if (themePref != null) { themePref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { - final Boolean darkTheme = (Boolean) newValue; - DeckLog.log("darkTheme: " + darkTheme); - setAppTheme(darkTheme); + setAppTheme(Integer.parseInt((String) newValue)); requireActivity().setResult(Activity.RESULT_OK); - requireActivity().recreate(); + ActivityCompat.recreate(requireActivity()); return true; }); } else { - DeckLog.error("Could not find preference with key: \"" + getString(R.string.pref_key_dark_theme) + "\""); - } - - brandingPref = findPreference(getString(R.string.pref_key_branding)); - if (brandingPref != null) { - brandingPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { - final Boolean branding = (Boolean) newValue; - DeckLog.log("branding: " + branding); - requireActivity().setResult(Activity.RESULT_OK); - requireActivity().recreate(); - return true; - }); - } else { - DeckLog.error("Could not find preference with key: \"" + getString(R.string.pref_key_dark_theme) + "\""); + DeckLog.error("Could not find preference with key:", getString(R.string.pref_key_dark_theme)); } compactPref = findPreference(getString(R.string.pref_key_compact)); @@ -77,24 +63,32 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande return true; }); } else { - DeckLog.error("Could not find preference with key: \"" + getString(R.string.pref_key_background_sync) + "\""); + DeckLog.error("Could not find preference with key", getString(R.string.pref_key_background_sync)); } - } - @Override - public void onStart() { - super.onStart(); - @Nullable Context context = getContext(); - if (context != null) { - applyBrand(readBrandMainColor(context)); + debuggingPref = findPreference(getString(R.string.pref_key_debugging)); + if (debuggingPref != null) { + debuggingPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { + DeckLog.enablePersistentLogs((Boolean) newValue); + DeckLog.log("persistet debug logs:", newValue); + return true; + }); + } else { + DeckLog.error("Could not find preference with key:", getString(R.string.pref_key_debugging)); } + + eTagPref = findPreference(getString(R.string.pref_key_etags)); } @Override - public void applyBrand(int mainColor) { - wifiOnlyPref.applyBrand(mainColor); - themePref.applyBrand(mainColor); - brandingPref.applyBrand(mainColor); - compactPref.applyBrand(mainColor); + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + DeckApplication.readCurrentAccountColor().observe(getViewLifecycleOwner(), (mainColor) -> { + wifiOnlyPref.applyBrand(mainColor); + compactPref.applyBrand(mainColor); + debuggingPref.applyBrand(mainColor); + eTagPref.applyBrand(mainColor); + }); } } 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 3a0005fb1..21aafed30 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 @@ -23,6 +23,7 @@ import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.exceptions.UploadAttachmentFailedException; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; @@ -37,7 +38,7 @@ import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.util.MimeTypeUtil; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; -import static it.niedermann.nextcloud.deck.util.AttachmentUtil.copyContentUriToTempFile; +import static it.niedermann.nextcloud.deck.util.FilesUtil.copyContentUriToTempFile; import static java.net.HttpURLConnection.HTTP_CONFLICT; public class ShareTargetActivity extends MainActivity implements SelectCardListener { @@ -125,19 +126,23 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe if (mimeType == null) { throw new IllegalArgumentException("MimeType of uri is null. [" + uri + "]"); } - runOnUiThread(() -> { - 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) { + mainViewModel.addAttachmentToCard(fullCard.getAccountId(), fullCard.getCard().getLocalId(), mimeType, tempFile, new ResponseCallback<Attachment>() { + @Override + public void onResponse(Attachment response) { + runOnUiThread(shareProgressViewModel::increaseProgress); + } + + @Override + public void onError(Throwable throwable) { + runOnUiThread(() -> { + if (throwable instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) throwable).getStatusCode() == HTTP_CONFLICT) { + ResponseCallback.super.onError(throwable); shareProgressViewModel.addDuplicateAttachment(tempFile.getName()); } else { - shareProgressViewModel.addException(liveData.getError()); + shareProgressViewModel.addException(throwable); } - } else { - shareProgressViewModel.increaseProgress(); - } - }); + }); + } }); } catch (Throwable t) { runOnUiThread(() -> shareProgressViewModel.addException(new UploadAttachmentFailedException("Error while uploading attachment for uri [" + uri + "]", t))); @@ -154,7 +159,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe switch (which) { case 0: final String oldDescription = fullCard.getCard().getDescription(); - DeckLog.info("Adding to card #" + fullCard.getCard().getId() + " (" + fullCard.getCard().getTitle() + "): Text \"" + receivedText + "\""); + DeckLog.info("Adding to card with id", fullCard.getCard().getId(), "(" + fullCard.getCard().getTitle() + "):", receivedText); fullCard.getCard().setDescription( (oldDescription == null || oldDescription.length() == 0) ? receivedText 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 8fc56759f..1e0fd4e72 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 @@ -14,8 +14,8 @@ public class StackAdapter extends FragmentStateAdapter { @NonNull private final List<Stack> stackList = new ArrayList<>(); - public StackAdapter(@NonNull FragmentActivity fragmentActivity) { - super(fragmentActivity); + public StackAdapter(final FragmentActivity fa) { + super(fa); } @Override 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 313e6e821..80440b9d0 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 @@ -81,7 +81,7 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car return binding.getRoot(); } - adapter = new CardAdapter(requireContext(), getChildFragmentManager(), stackId, mainViewModel, this, + adapter = new CardAdapter(requireActivity(), getChildFragmentManager(), stackId, mainViewModel, this, (requireActivity() instanceof SelectCardListener) ? (SelectCardListener) requireActivity() : null); @@ -159,7 +159,7 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car 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 + "\""); + DeckLog.log("Moved", Card.class.getSimpleName(), originCardLocalId, "to", Stack.class.getSimpleName(), targetStackLocalId); } }); } 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 index af17464dc..e1398bab7 100644 --- 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 @@ -9,10 +9,10 @@ import android.util.Size; import android.view.OrientationEventListener; import android.view.Surface; import android.view.View; +import android.widget.Toast; 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; @@ -34,12 +34,10 @@ 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 it.niedermann.nextcloud.deck.util.FilesUtil; -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; @@ -81,8 +79,9 @@ public class TakePhotoActivity extends BrandedActivity { cameraProvider.unbindAll(); cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase); }); - } catch (ExecutionException | InterruptedException e) { + } catch (IllegalArgumentException | ExecutionException | InterruptedException e) { DeckLog.logError(e); + Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); finish(); } }, ContextCompat.getMainExecutor(this)); @@ -118,13 +117,13 @@ public class TakePhotoActivity extends BrandedActivity { binding.takePhoto.setEnabled(false); final String photoFileName = Instant.now().atZone(ZoneId.systemDefault()).format(fileNameFromCameraFormatter); try { - final File photoFile = AttachmentUtil.getTempCacheFile(this, "photos/" + photoFileName); + final File photoFile = FilesUtil.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()); + DeckLog.info("onImageSaved - savedUri:", savedUri.toString()); setResult(RESULT_OK, new Intent().setDataAndType(savedUri, IMAGE_JPEG)); finish(); } @@ -167,7 +166,6 @@ public class TakePhotoActivity extends BrandedActivity { } } - @RequiresApi(LOLLIPOP) public static Intent createIntent(@NonNull Context context) { return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); } 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 index 0912a07dd..730ecbdd1 100644 --- 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 @@ -1,8 +1,6 @@ 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; @@ -21,7 +19,6 @@ public class SquareConstraintLayout extends ConstraintLayout { super(context, attrs, defStyleAttr); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @@ -29,6 +26,7 @@ public class SquareConstraintLayout extends ConstraintLayout { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Set a square layout. + //noinspection SuspiciousNameCombination super.onMeasure(widthMeasureSpec, widthMeasureSpec); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareRelativeLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareRelativeLayout.java index 7f3649c94..cdce90251 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareRelativeLayout.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareRelativeLayout.java @@ -1,8 +1,6 @@ package it.niedermann.nextcloud.deck.ui.view; -import android.annotation.TargetApi; import android.content.Context; -import android.os.Build; import android.util.AttributeSet; import android.widget.RelativeLayout; @@ -20,7 +18,6 @@ public class SquareRelativeLayout extends RelativeLayout { super(context, attrs, defStyleAttr); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) public SquareRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidget.java new file mode 100644 index 000000000..89d75eeee --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidget.java @@ -0,0 +1,78 @@ +package it.niedermann.nextcloud.deck.ui.widget.filter; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import java.util.NoSuchElementException; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; + +import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE; + +public class FilterWidget extends AppWidgetProvider { + public static final String ACCOUNT_KEY = "filter_widget_account"; + public static final String BUNDLE_KEY = "filter_widget_bundle"; + + static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds, Account account) { + final SyncManager syncManager = new SyncManager(context); + + for (int appWidgetId : appWidgetIds) { + new Thread(() -> { + try { + // TODO implement + throw new UnsupportedOperationException("Not yet implemented"); + } 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(FilterWidget.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, FilterWidget.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.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + FilterWidget.class.getSimpleName() + " with id " + appWidgetId)); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetConfigurationActivity.java new file mode 100644 index 000000000..102860d7c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetConfigurationActivity.java @@ -0,0 +1,67 @@ +package it.niedermann.nextcloud.deck.ui.widget.filter; + +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProvider; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ActivityFilterWidgetBinding; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; + +public class FilterWidgetConfigurationActivity extends AppCompatActivity { + private int appWidgetId; + private ActivityFilterWidgetBinding binding; + private FilterWidgetViewModel viewModel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); + + binding = ActivityFilterWidgetBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(FilterWidgetViewModel.class); + + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.add_filter_widget); + } + + setResult(RESULT_CANCELED); + final Bundle args = getIntent().getExtras(); + + if (args != null) { + appWidgetId = args.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + } + + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + DeckLog.error("INVALID_APPWIDGET_ID"); + finish(); + } + binding.cancel.setOnClickListener((v) -> finish()); + binding.submit.setOnClickListener((v) -> { + final Bundle extras = new Bundle(); + + viewModel.updateFilterWidget(response -> DeckLog.verbose("Successfully updated", FilterWidget.class.getSimpleName(), "with id", appWidgetId)); + Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, getApplicationContext(), FilterWidget.class); + 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(FilterWidget.BUNDLE_KEY, extras); + setResult(RESULT_OK, updateIntent); + getApplicationContext().sendBroadcast(updateIntent); + + finish(); + }); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java new file mode 100644 index 000000000..fbc3900f7 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java @@ -0,0 +1,90 @@ +package it.niedermann.nextcloud.deck.ui.widget.filter; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; + +public class FilterWidgetFactory implements RemoteViewsService.RemoteViewsFactory { + private final Context context; + private final int appWidgetId; + private final SyncManager syncManager; + + @NonNull + private final List<FilterWidgetCard> data = new ArrayList<>(); + + FilterWidgetFactory(Context context, Intent intent) { + this.context = context; + this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + this.syncManager = new SyncManager(context); + } + + @Override + public void onCreate() { + } + + @Override + public void onDataSetChanged() { + + } + + + @Override + public void onDestroy() { + + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public RemoteViews getViewAt(int i) { + RemoteViews widget_entry; + + if (i > (data.size() - 1) || data.get(i) == null) { + DeckLog.error("Card not found at position", i); + return null; + } + + FullCard card = data.get(i).getCard(); + + widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_stack_entry); + widget_entry.setTextViewText(R.id.widget_entry_content_tv, card.card.getTitle()); + + 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; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetService.java new file mode 100644 index 000000000..20c4350ba --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetService.java @@ -0,0 +1,11 @@ +package it.niedermann.nextcloud.deck.ui.widget.filter; + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class FilterWidgetService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new FilterWidgetFactory(this.getApplicationContext(), intent); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetViewModel.java new file mode 100644 index 000000000..4f50465f2 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetViewModel.java @@ -0,0 +1,33 @@ +package it.niedermann.nextcloud.deck.ui.widget.filter; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import it.niedermann.nextcloud.deck.api.ResponseCallback; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; + +public class FilterWidgetViewModel extends AndroidViewModel { + + @NonNull + private final SyncManager syncManager; + @NonNull + private final MutableLiveData<FilterWidget> config$ = new MutableLiveData<>(new FilterWidget()); + + public FilterWidgetViewModel(@NonNull Application application) { + super(application); + this.syncManager = new SyncManager(application); + } + + public LiveData<FilterWidget> getFilterWidgetConfiguration() { + return this.config$; + } + + public void updateFilterWidget(@NonNull ResponseCallback<Integer> callback) { + syncManager.createFilterWidget(config$.getValue(), callback); + } +} 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 b44b80d1b..766948062 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 @@ -6,11 +6,11 @@ import android.appwidget.AppWidgetProvider; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.os.Build; +import android.net.Uri; +import android.text.TextUtils; import android.view.View; import android.widget.RemoteViews; -import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; import androidx.annotation.NonNull; @@ -36,11 +36,21 @@ public class SingleCardWidget extends AppWidgetProvider { final Intent intent = EditActivity.createEditCardIntent(context, fullModel.getAccount(), fullModel.getModel().getBoardId(), fullModel.getFullCard().getLocalId()); final PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT); final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_single_card); + final Intent serviceIntent = new Intent(context, SingleCardWidgetService.class); - views.setOnClickPendingIntent(R.id.widget_card, pendingIntent); + serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + serviceIntent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); + + if (TextUtils.isEmpty(SingleCardWidgetFactory.getDescriptionOrNull(fullModel))) { + views.setViewVisibility(R.id.description_lv, View.GONE); + } else { + views.setViewVisibility(R.id.description_lv, View.VISIBLE); + } + views.setOnClickPendingIntent(R.id.widget_card, pendingIntent); + views.setPendingIntentTemplate(R.id.description_lv, pendingIntent); views.setTextViewText(R.id.title, fullModel.getFullCard().getCard().getTitle()); - views.setTextViewText(R.id.description, fullModel.getFullCard().getCard().getDescription()); + views.setRemoteAdapter(R.id.description_lv, serviceIntent); if (fullModel.getFullCard().getCard().getDueDate() != null) { views.setTextViewText(R.id.card_due_date, DateUtil.getRelativeDateTimeString(context, fullModel.getFullCard().getCard().getDueDate().toEpochMilli())); @@ -49,7 +59,7 @@ public class SingleCardWidget extends AppWidgetProvider { // Because otherwise using Reflection is the only way views.setViewVisibility(R.id.card_due_date, View.VISIBLE); views.setViewVisibility(R.id.card_due_date_image, View.VISIBLE); - setImageDrawable(views, R.id.card_due_date_image, R.drawable.calendar_blank_grey600_24dp); + views.setImageViewResource(R.id.card_due_date_image, R.drawable.calendar_blank_grey600_24dp); } else { views.setViewVisibility(R.id.card_due_date, View.GONE); views.setViewVisibility(R.id.card_due_date_image, View.GONE); @@ -65,8 +75,8 @@ public class SingleCardWidget extends AppWidgetProvider { } else { views.setViewVisibility(R.id.card_count_attachments, View.VISIBLE); views.setViewVisibility(R.id.card_count_attachments_image, View.VISIBLE); + views.setImageViewResource(R.id.card_count_attachments_image, R.drawable.ic_attach_file_grey600_24dp); setupCounter(views, R.id.card_count_attachments, attachmentsCount, counterMaxValue); - setImageDrawable(views, R.id.card_count_attachments_image, R.drawable.ic_check_grey600_24dp); } final int commentsCount = fullModel.getFullCard().getCommentCount(); @@ -76,8 +86,8 @@ public class SingleCardWidget extends AppWidgetProvider { } else { views.setViewVisibility(R.id.card_count_comments, View.VISIBLE); views.setViewVisibility(R.id.card_count_comments_image, View.VISIBLE); + views.setImageViewResource(R.id.card_count_comments_image, R.drawable.ic_comment_white_24dp); setupCounter(views, R.id.card_count_comments, commentsCount, counterMaxValue); - setImageDrawable(views, R.id.card_count_comments_image, R.drawable.ic_comment_white_24dp); } final Card.TaskStatus taskStatus = fullModel.getFullCard().getCard().getTaskStatus(); @@ -85,13 +95,14 @@ public class SingleCardWidget extends AppWidgetProvider { views.setViewVisibility(R.id.card_count_tasks, View.VISIBLE); views.setViewVisibility(R.id.card_count_tasks_image, View.VISIBLE); views.setTextViewText(R.id.card_count_tasks, context.getResources().getString(R.string.task_count, String.valueOf(taskStatus.doneCount), String.valueOf(taskStatus.taskCount))); - setImageDrawable(views, R.id.card_count_tasks_image, R.drawable.ic_attach_file_grey600_24dp); + views.setImageViewResource(R.id.card_count_tasks_image, R.drawable.ic_check_grey600_24dp); } else { views.setViewVisibility(R.id.card_count_tasks, View.GONE); views.setViewVisibility(R.id.card_count_tasks_image, View.GONE); } awm.updateAppWidget(appWidgetId, views); + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.description_lv); } catch (NoSuchElementException e) { // onUpdate has been triggered before the user finished configuring the widget } @@ -102,19 +113,13 @@ public class SingleCardWidget extends AppWidgetProvider { private static void setupCounter(@NonNull RemoteViews views, @IdRes int textViewId, int count, String counterMaxValue) { if (count > 99) { views.setTextViewText(textViewId, counterMaxValue); - } else if (count > 1 || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + } else if (count > 1) { views.setTextViewText(textViewId, String.valueOf(count)); } else if (count == 1) { views.setTextViewText(textViewId, ""); } } - private static void setImageDrawable(@NonNull RemoteViews views, @IdRes int imageView, @DrawableRes int image) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - views.setImageViewResource(imageView, image); - } - } - @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); @@ -124,10 +129,9 @@ public class SingleCardWidget extends AppWidgetProvider { @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); - AppWidgetManager awm = AppWidgetManager.getInstance(context); + final AppWidgetManager awm = AppWidgetManager.getInstance(context); - updateAppWidget(context, AppWidgetManager.getInstance(context), - (awm.getAppWidgetIds(new ComponentName(context, SingleCardWidget.class)))); + updateAppWidget(context, AppWidgetManager.getInstance(context), (awm.getAppWidgetIds(new ComponentName(context, SingleCardWidget.class)))); } @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetFactory.java new file mode 100644 index 000000000..db24afa43 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetFactory.java @@ -0,0 +1,100 @@ +package it.niedermann.nextcloud.deck.ui.widget.singlecard; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.NoSuchElementException; + +import it.niedermann.android.markdown.MarkdownUtil; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; + +public class SingleCardWidgetFactory implements RemoteViewsService.RemoteViewsFactory { + private final Context context; + private final int appWidgetId; + private final SyncManager syncManager; + private FullSingleCardWidgetModel model; + + public SingleCardWidgetFactory(@NonNull Context context, @NonNull Intent intent) { + this.context = context; + this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + this.syncManager = new SyncManager(context); + } + + @Override + public void onCreate() { + + } + + @Override + public void onDataSetChanged() { + try { + this.model = syncManager.getSingleCardWidgetModelDirectly(appWidgetId); + } catch (NoSuchElementException e) { + this.model = null; + } + } + + @Override + public void onDestroy() { + + } + + @Override + public int getCount() { + return getDescriptionOrNull(model) == null ? 0 : 1; + } + + @Override + public RemoteViews getViewAt(int position) { + final String description = getDescriptionOrNull(model); + if (description == null) { + return null; + } + + final RemoteViews widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_single_card_content); + widget_entry.setTextViewText(R.id.description, MarkdownUtil.renderForRemoteView(context, description)); + + final Intent intent = EditActivity.createEditCardIntent(context, model.getAccount(), model.getModel().getBoardId(), model.getFullCard().getCard().getLocalId()); + widget_entry.setOnClickFillInIntent(R.id.description, intent); + + return widget_entry; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Nullable + public static String getDescriptionOrNull(@Nullable FullSingleCardWidgetModel model) { + if (model == null || model.getFullCard() == null || model.getFullCard().getCard() == null || TextUtils.isEmpty(model.getFullCard().getCard().getDescription())) { + return null; + } + return model.getFullCard().getCard().getDescription(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetService.java new file mode 100644 index 000000000..d938e383a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetService.java @@ -0,0 +1,11 @@ +package it.niedermann.nextcloud.deck.ui.widget.singlecard; + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class SingleCardWidgetService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new SingleCardWidgetFactory(this.getApplicationContext(), intent); + } +} 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 index cb179953c..5b0907ef3 100644 --- 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 @@ -7,14 +7,13 @@ 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 androidx.annotation.ColorInt; +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.appwidgets.StackWidgetModel; +import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.MainActivity; import it.niedermann.nextcloud.deck.ui.card.EditActivity; @@ -22,82 +21,29 @@ 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); + updateAppWidget(context, appWidgetManager, appWidgetIds); } @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); - } - } + final AppWidgetManager awm = AppWidgetManager.getInstance(context); + + if (ACTION_APPWIDGET_UPDATE.equals(intent.getAction())) { + if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) { + final int appWidgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + DeckLog.verbose(ACTION_APPWIDGET_UPDATE, "for", StackWidget.class.getSimpleName(), "with id", appWidgetId, "→ perform update."); + updateAppWidget(context, awm, new int[]{appWidgetId}); + } else { + DeckLog.verbose(ACTION_APPWIDGET_UPDATE, "→ Triggering update for all widgets of type", StackWidget.class.getSimpleName()); + updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, StackWidget.class))); } } } @@ -108,14 +54,47 @@ public class StackWidget extends AppWidgetProvider { final SyncManager syncManager = new SyncManager(context); for (int appWidgetId : appWidgetIds) { - syncManager.deleteStackWidgetModel(appWidgetId); + DeckLog.info("Delete", StackWidget.class.getSimpleName(), "with id", appWidgetId); + syncManager.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + StackWidget.class.getSimpleName() + " with id " + 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)); + private static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { + final SyncManager syncManager = new SyncManager(context); + for (int appWidgetId : appWidgetIds) { + new Thread(() -> { + if (syncManager.filterWidgetExists(appWidgetId)) { + final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack); + + final Intent serviceIntent = new Intent(context, StackWidgetService.class); + serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); + + final Intent intent = new Intent(Intent.ACTION_MAIN).setComponent(new ComponentName(context.getPackageName(), MainActivity.class.getName())); + final PendingIntent pendingIntent = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_APP_RQ, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + final PendingIntent templatePI = PendingIntent.getActivity(context, PENDING_INTENT_EDIT_CARD_RQ, + new Intent(context, EditActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + + views.setOnClickPendingIntent(R.id.widget_stack_header_rl, pendingIntent); + + 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); + + syncManager.getFilterWidget(appWidgetId, response -> { + final Stack stack = syncManager.getStackDirectly(response.getAccounts().get(0).getBoards().get(0).getStacks().get(0).getStackId()); + @ColorInt final Integer boardColor = syncManager.getBoardColorDirectly(response.getAccounts().get(0).getAccountId(), response.getAccounts().get(0).getBoards().get(0).getBoardId()); + views.setTextViewText(R.id.widget_stack_title_tv, stack.getTitle()); + views.setInt(R.id.widget_stack_header_icon, "setColorFilter", boardColor); + + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.stack_widget_lv); + awm.updateAppWidget(appWidgetId, views); + }); + } else { + DeckLog.warn("Does not yet exist"); + } + }).start(); + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java index 96b5cf672..a34123e29 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java @@ -7,10 +7,19 @@ import android.os.Bundle; import androidx.appcompat.app.ActionBar; import androidx.lifecycle.ViewModelProvider; +import java.util.Collections; + import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.widget.filter.EWidgetType; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetAccount; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetBoard; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetStack; import it.niedermann.nextcloud.deck.ui.PickStackActivity; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; public class StackWidgetConfigurationActivity extends PickStackActivity { private int appWidgetId; @@ -43,22 +52,35 @@ public class StackWidgetConfigurationActivity extends PickStackActivity { @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(); + final FilterWidget config = new FilterWidget(appWidgetId, EWidgetType.STACK_WIDGET); + final FilterWidgetAccount filterWidgetAccount = new FilterWidgetAccount(account.getId(), false); + filterWidgetAccount.setIncludeNoProject(false); + FilterWidgetBoard filterWidgetBoard = new FilterWidgetBoard(boardId, Collections.singletonList(new FilterWidgetStack(stackId))); + filterWidgetBoard.setIncludeNoLabel(false); + filterWidgetAccount.setBoards( + Collections.singletonList(filterWidgetBoard)); + config.setAccounts(Collections.singletonList(filterWidgetAccount)); + stackWidgetConfigurationViewModel.addStackWidget(config, new IResponseCallback<Integer>(account) { + + @Override + public void onResponse(Integer response) { + final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, + getApplicationContext(), StackWidget.class) + .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + setResult(RESULT_OK, updateIntent); + getApplicationContext().sendBroadcast(updateIntent); + + finish(); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + ExceptionDialogFragment + .newInstance(throwable, account) + .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override 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 index cc669accf..54ae5635e 100644 --- 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 @@ -5,6 +5,8 @@ import android.app.Application; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; @SuppressWarnings("WeakerAccess") @@ -17,7 +19,7 @@ public class StackWidgetConfigurationViewModel extends AndroidViewModel { this.syncManager = new SyncManager(application); } - public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { - syncManager.addStackWidget(appWidgetId, accountId, stackId, darkTheme); + public void addStackWidget(@NonNull FilterWidget config, @NonNull IResponseCallback<Integer> callback) { + syncManager.createFilterWidget(config, callback); } } 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 index 87f357e20..e44fd9876 100644 --- 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 @@ -1,76 +1,58 @@ 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 androidx.annotation.NonNull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.NoSuchElementException; 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.model.widget.filter.dto.FilterWidgetCard; 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 final SyncManager syncManager; - private Account account; - private FullStack stack; - private List<FullCard> cardList; + @NonNull + private final List<FilterWidgetCard> data = new ArrayList<>(); 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); - } + this.syncManager = new SyncManager(context); } @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); - } - }); + // Nothing to do here... } @Override public void onDataSetChanged() { - + try { + final List<FilterWidgetCard> response = syncManager.getCardsForFilterWidget(appWidgetId); + DeckLog.verbose(StackWidget.class.getSimpleName(), "with id", appWidgetId, "fetched", response.size(), "cards from the database."); + data.clear(); + Collections.sort(response, Comparator.comparingLong(value -> value.getCard().getCard().getOrder())); + data.addAll(response); + } catch (NoSuchElementException e) { + DeckLog.error("No", StackWidget.class.getSimpleName(), "for appWidgetId", appWidgetId, "found."); + DeckLog.logError(e); + } } @@ -81,24 +63,22 @@ public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory @Override public int getCount() { - return stack == null ? 0 : stack.getCards().size(); + return data.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); + if (i > (data.size() - 1) || data.get(i) == null) { + DeckLog.error("No card or separator not found at position", i); return null; } - - FullCard card = cardList.get(i); + final RemoteViews widget_entry; + final FilterWidgetCard filterWidgetCard = data.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()); + widget_entry.setTextViewText(R.id.widget_entry_content_tv, filterWidgetCard.getCard().getCard().getTitle()); - final Intent intent = EditActivity.createEditCardIntent(context, account, stack.getStack().getBoardId(), card.getCard().getLocalId()); + final Intent intent = EditActivity.createEditCardIntent(context, syncManager.readAccountDirectly(filterWidgetCard.getCard().getAccountId()), filterWidgetCard.getStack().getBoardId(), filterWidgetCard.getCard().getLocalId()); intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); widget_entry.setOnClickFillInIntent(R.id.widget_stack_entry, intent); @@ -124,11 +104,4 @@ public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory 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/upcoming/UpcomingWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidget.java new file mode 100644 index 000000000..bedf5253f --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidget.java @@ -0,0 +1,124 @@ +package it.niedermann.nextcloud.deck.ui.widget.upcoming; + +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.widget.RemoteViews; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.stream.Collectors; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.ResponseCallback; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.enums.ESortCriteria; +import it.niedermann.nextcloud.deck.model.widget.filter.EWidgetType; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetAccount; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetSort; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetUser; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; + +import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE; + +public class UpcomingWidget extends AppWidgetProvider { + private static final int PENDING_INTENT_OPEN_APP_RQ = 0; + private static final int PENDING_INTENT_EDIT_CARD_RQ = 1; + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.onUpdate(context, appWidgetManager, appWidgetIds); + final SyncManager syncManager = new SyncManager(context); + + for (int appWidgetId : appWidgetIds) { + new Thread(() -> { + if (syncManager.filterWidgetExists(appWidgetId)) { + DeckLog.warn(UpcomingWidget.class.getSimpleName(), "with id", appWidgetId, "already exists, perform update instead."); + updateAppWidget(context, appWidgetManager, appWidgetIds); + } else { + final List<Account> accountsList = syncManager.readAccountsDirectly(); + final FilterWidget config = new FilterWidget(appWidgetId, EWidgetType.UPCOMING_WIDGET); + config.setSorts(new FilterWidgetSort(ESortCriteria.DUE_DATE, true)); + config.setAccounts(accountsList.stream().map(account -> { + final FilterWidgetAccount fwa = new FilterWidgetAccount(account.getId(), false); + fwa.setUsers(new FilterWidgetUser(syncManager.getUserByUidDirectly(account.getId(), account.getUserName()).getLocalId())); + return fwa; + }).collect(Collectors.toList())); + syncManager.createFilterWidget(config, new ResponseCallback<Integer>() { + @Override + public void onResponse(Integer response) { + DeckLog.verbose("Successfully created", UpcomingWidget.class.getSimpleName(), "with id", appWidgetId); + updateAppWidget(context, appWidgetManager, appWidgetIds); + } + + @Override + public void onError(Throwable throwable) { + DeckLog.error("Error while creating", UpcomingWidget.class.getSimpleName(), "with id", appWidgetId); + ResponseCallback.super.onError(throwable); + onDeleted(context, appWidgetIds); + } + }); + } + }).start(); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + + final AppWidgetManager awm = AppWidgetManager.getInstance(context); + + if (ACTION_APPWIDGET_UPDATE.equals(intent.getAction())) { + if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) { + final int appWidgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + DeckLog.verbose(ACTION_APPWIDGET_UPDATE, "for", UpcomingWidget.class.getSimpleName(), "with id", appWidgetId, "→ perform update."); + updateAppWidget(context, awm, new int[]{appWidgetId}); + } else { + DeckLog.verbose(ACTION_APPWIDGET_UPDATE, "→ Triggering update for all widgets of type", UpcomingWidget.class.getSimpleName()); + updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, UpcomingWidget.class))); + } + } + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + super.onDeleted(context, appWidgetIds); + final SyncManager syncManager = new SyncManager(context); + + for (int appWidgetId : appWidgetIds) { + DeckLog.info("Delete", UpcomingWidget.class.getSimpleName(), "with id", appWidgetId); + syncManager.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + UpcomingWidget.class.getSimpleName() + " with id " + appWidgetId)); + } + } + + private static void updateAppWidget(@NonNull Context context, @NonNull AppWidgetManager awm, int[] appWidgetIds) { + for (int appWidgetId : appWidgetIds) { + new Thread(() -> { + final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_upcoming); + + final Intent serviceIntent = new Intent(context, UpcomingWidgetService.class); + serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); + + final PendingIntent templatePI = PendingIntent.getActivity(context, PENDING_INTENT_EDIT_CARD_RQ, + new Intent(context, EditActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + + views.setPendingIntentTemplate(R.id.upcoming_widget_lv, templatePI); + views.setRemoteAdapter(R.id.upcoming_widget_lv, serviceIntent); + views.setEmptyView(R.id.upcoming_widget_lv, R.id.widget_upcoming_placeholder_iv); + + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.upcoming_widget_lv); + awm.updateAppWidget(appWidgetId, views); + }).start(); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetFactory.java new file mode 100644 index 000000000..964c21ffa --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetFactory.java @@ -0,0 +1,224 @@ +package it.niedermann.nextcloud.deck.ui.widget.upcoming; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; + +import it.niedermann.android.util.DimensionUtil; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; + +import static java.time.temporal.ChronoUnit.DAYS; + +public class UpcomingWidgetFactory implements RemoteViewsService.RemoteViewsFactory { + private final Context context; + private final int appWidgetId; + private final SyncManager syncManager; + private final int headerHorizontalPadding; + private final int headerVerticalPaddingNth; + + @NonNull + private final List<Object> data = new ArrayList<>(); + + UpcomingWidgetFactory(@NonNull Context context, Intent intent) { + this.context = context; + this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + this.syncManager = new SyncManager(context); + this.headerHorizontalPadding = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1hx); + this.headerVerticalPaddingNth = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_2x); + } + + @Override + public void onCreate() { + // Nothing to do here... + } + + @Override + public void onDataSetChanged() { + try { + final List<FilterWidgetCard> response = syncManager.getCardsForFilterWidget(appWidgetId); + DeckLog.verbose(UpcomingWidgetFactory.class.getSimpleName(), "with id", appWidgetId, "fetched", response.size(), "cards from the database."); + data.clear(); + final Comparator<FilterWidgetCard> comparator = Comparator.comparing((card -> { + if (card != null && + card.getCard() != null && + card.getCard().getCard() != null && + card.getCard().getCard().getDueDate() != null) { + return card.getCard().getCard().getDueDate(); + } + return null; + }), Comparator.nullsLast(Comparator.naturalOrder())); + comparator.thenComparing(card -> { + if (card != null && + card.getCard() != null && + card.getCard().getCard().getDueDate() != null) { + + Card c = card.getCard().getCard(); + + if (c.getLastModified() == null && c.getLastModifiedLocal() != null) { + return c.getLastModifiedLocal(); + } else if (c.getLastModified() != null && c.getLastModifiedLocal() == null) { + return c.getLastModified(); + } else { + return c.getLastModifiedLocal().toEpochMilli() > c.getLastModified().toEpochMilli() ? + c.getLastModifiedLocal() : c.getLastModified(); + } + } + return null; + }, Comparator.nullsLast(Comparator.naturalOrder())); + + Collections.sort( + response, + comparator + ); + EUpcomingDueType lastDueType = null; + for (FilterWidgetCard filterWidgetCard : response) { + final EUpcomingDueType nextDueType = getDueType(filterWidgetCard.getCard().getCard().getDueDate()); + DeckLog.info(filterWidgetCard.getCard().getCard().getTitle() + ":", nextDueType.name()); + if (!nextDueType.equals(lastDueType)) { + data.add(new Separator(nextDueType.toString(context))); + lastDueType = nextDueType; + } + data.add(filterWidgetCard); + } + } catch (NoSuchElementException e) { + DeckLog.error("No", UpcomingWidget.class.getSimpleName(), "for appWidgetId", appWidgetId, "found."); + DeckLog.logError(e); + } + } + + @Override + public void onDestroy() { + + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public RemoteViews getViewAt(int i) { + if (i > (data.size() - 1) || data.get(i) == null) { + DeckLog.error("No card or separator not found at position", i); + return null; + } + final RemoteViews widget_entry; + if (data.get(i).getClass() == Separator.class) { + final Separator separator = (Separator) data.get(i); + widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_separator); + widget_entry.setTextViewText(R.id.widget_entry_content_tv, separator.title); + if(i == 0) { + widget_entry.setViewPadding(R.id.widget_entry_content_tv, headerHorizontalPadding, 0, headerHorizontalPadding, 0); + } else { + widget_entry.setViewPadding(R.id.widget_entry_content_tv, headerHorizontalPadding, headerVerticalPaddingNth, headerHorizontalPadding, 0); + } + } else { + final FullCard card = ((FilterWidgetCard) data.get(i)).getCard(); + widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_stack_entry); + widget_entry.setTextViewText(R.id.widget_entry_content_tv, card.getCard().getTitle()); + + final Long localCardId = card.getCard().getLocalId(); + final Intent intent = EditActivity.createEditCardIntent(context, syncManager.readAccountDirectly(card.getAccountId()), syncManager.getBoardLocalIdByLocalCardIdDirectly(localCardId), localCardId); + 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 2; + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @NonNull + private static EUpcomingDueType getDueType(@Nullable Instant dueDate) { + if (dueDate == null) { + return EUpcomingDueType.NO_DUE; + } + + long diff = DAYS.between(LocalDate.now(), dueDate.atZone(ZoneId.systemDefault()).toLocalDate()); + + if (diff > 7) { + return EUpcomingDueType.LATER; + } else if (diff > 1) { + return EUpcomingDueType.WEEK; + } else if (diff > 0) { + return EUpcomingDueType.TOMORROW; + } else if (diff == 0) { + return EUpcomingDueType.TODAY; + } else { + return EUpcomingDueType.OVERDUE; + } + } + + private enum EUpcomingDueType { + OVERDUE(1, R.string.filter_overdue), + TODAY(2, R.string.filter_today), + TOMORROW(3, R.string.filter_tomorrow), + WEEK(4, R.string.filter_week), + LATER(5, R.string.filter_later), + NO_DUE(6, R.string.filter_no_due); + + private final int value; + private final int id; + + EUpcomingDueType(int id, @StringRes int value) { + this.value = value; + this.id = id; + } + + public int getId() { + return id; + } + + @NonNull + public String toString(Context context) { + return context.getString(this.value); + } + } + + private static class Separator { + public String title; + + private Separator(String title) { + this.title = title; + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetService.java new file mode 100644 index 000000000..6cbd04549 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetService.java @@ -0,0 +1,11 @@ +package it.niedermann.nextcloud.deck.ui.widget.upcoming; + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class UpcomingWidgetService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new UpcomingWidgetFactory(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 ef3e14b37..a4bd5d6d9 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 @@ -12,15 +12,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -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.Account; import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.model.enums.EAttachmentType; import it.niedermann.nextcloud.deck.model.ocs.Version; /** @@ -37,90 +33,63 @@ public class AttachmentUtil { * 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); + public static String getThumbnailUrl(@NonNull Account account, @NonNull Long cardRemoteId, @NonNull Attachment attachment, @Px int previewSize) { + return getThumbnailUrl(account, cardRemoteId, attachment, previewSize, previewSize); + } + + public static String getThumbnailUrl(@NonNull Account account, @NonNull Long cardRemoteId, @NonNull Attachment attachment, @Px int previewWidth, @Px int previewHeight) { + return account.getServerDeckVersionAsObject().supportsFileAttachments() && + EAttachmentType.FILE.equals(attachment.getType()) && + attachment.getFileId() != null + ? account.getUrl() + "/index.php/core/preview?fileId=" + attachment.getFileId() + "&x=" + previewWidth + "&y=" + previewHeight + "&a=true" + : getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment); } /** - * @return {@link AttachmentUtil#getRemoteUrl} or {@link Attachment#getLocalPath()} as fallback + * @return {@link AttachmentUtil#getDeck_1_0_RemoteUrl} 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) { + private 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()); + : getDeck_1_0_RemoteUrl(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) { + public static void openAttachmentInBrowser(@NonNull Account account, @NonNull Context context, Long cardRemoteId, Attachment attachment) { 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) { + + try { + context.startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(getCopyDownloadUrl(account, cardRemoteId, attachment)))); + } catch (IllegalArgumentException e) { 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 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()); + public static String getCopyDownloadUrl(@NonNull Account account, @NonNull Long cardRemoteId, @NonNull Attachment attachment) { + if (attachment.getId() == null) { + throw new IllegalArgumentException("attachment id must not be null"); } - 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; + return (attachment.getFileId() != null) + ? account.getUrl() + "/f/" + attachment.getFileId() + : getDeck_1_0_RemoteUrl(account.getUrl(), cardRemoteId, attachment.getId()); } /** - * Creates a new {@link File} + * Attention! This does only work for attachments of type "deck_file" which are a legacy of Deck API 1.0 */ - 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.getParentFile()"); - } - if (!tempDir.exists()) { - 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."); - } - } - - 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"); - } - - return cacheFile; + @Deprecated + private static String getDeck_1_0_RemoteUrl(@NonNull String accountUrl, @NonNull Long cardRemoteId, @NonNull Long attachmentRemoteId) { + return accountUrl + "/index.php/apps/deck/cards/" + cardRemoteId + "/attachment/" + attachmentRemoteId; } @DrawableRes diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/CardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/CardUtil.java index f89581c95..4f31b7250 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/CardUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/CardUtil.java @@ -1,9 +1,21 @@ package it.niedermann.nextcloud.deck.util; +import android.content.Context; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.model.full.FullCard; public class CardUtil { private static final Pattern pLists = Pattern.compile("^\\s*[*+-]\\s+", Pattern.MULTILINE); @@ -17,8 +29,34 @@ public class CardUtil { // You shall not pass } + /** + * @return a human readable String containing the description, due date and tags of the given {@param fullCard} + */ + @NonNull + public static String getCardContentAsString(@NonNull Context context, @NonNull FullCard fullCard) { + final Card card = fullCard.getCard(); + String text = card.getDescription(); + if(card.getDueDate() != null) { + if(!TextUtils.isEmpty(text)) { + text += "\n"; + } + text += context.getString(R.string.share_content_duedate, card.getDueDate().atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))); + } + if(fullCard.getLabels() != null && !fullCard.getLabels().isEmpty()) { + if(!TextUtils.isEmpty(text)) { + text += "\n"; + } + text += context.getString(R.string.share_content_labels, fullCard.getLabels().stream().map(Label::getTitle).collect(Collectors.joining(", "))); + } + return text; + } + + public static boolean cardHasCommentsOrAttachments(@NonNull FullCard fullCard) { + return fullCard.getCommentCount() > 0 || (fullCard.getAttachments() != null && !fullCard.getAttachments().isEmpty()); + } + @NonNull - public static String generateTitleFromDescription(String description) { + public static String generateTitleFromDescription(@Nullable String description) { if(description == null) return ""; return getLineWithoutMarkDown(description, 0); } @@ -32,20 +70,19 @@ public class CardUtil { */ @NonNull private static String getLineWithoutMarkDown(@NonNull String content, @SuppressWarnings("SameParameterValue") int lineNumber) { - String line = ""; if (content.contains("\n")) { - String[] lines = content.split("\n"); + final String[] lines = content.split("\n"); int currentLine = lineNumber; while (currentLine < lines.length && isEmptyLine(lines[currentLine])) { currentLine++; } if (currentLine < lines.length) { - line = removeMarkDown(lines[currentLine]); + return removeMarkDown(lines[currentLine]); } } else { - line = content; + return content; } - return line; + return ""; } /** @@ -61,7 +98,7 @@ public class CardUtil { * @param line String - a single Line which ends with \n * @return boolean isEmpty */ - private static boolean isEmptyLine(@Nullable String line) { + private static boolean isEmptyLine(@NonNull String line) { return removeMarkDown(line).trim().length() == 0; } @@ -72,9 +109,7 @@ public class CardUtil { * @return Plain Text-String */ @NonNull - private static String removeMarkDown(@Nullable String s) { - if (s == null) - return ""; + private static String removeMarkDown(@NonNull String s) { s = pLists.matcher(s).replaceAll(""); s = pHeadings.matcher(s).replaceAll("$1"); s = pHeadingLine.matcher(s).replaceAll(""); 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 ac88519ff..6fbb9e30e 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 @@ -22,14 +22,14 @@ public final class DateUtil { return context.getString(R.string.seconds_ago); } else { // in the future or past (larger than 60 seconds) - CharSequence dateString = DateUtils.getRelativeDateTimeString( + final CharSequence dateString = DateUtils.getRelativeDateTimeString( context, time, DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0 ); - String[] parts = dateString.toString().split(","); + final String[] parts = dateString.toString().split(","); if (parts.length == DATE_TIME_PARTS_SIZE) { if (parts[1].contains(":") && !parts[0].contains(":")) { return parts[0]; 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 1a88bec97..43a9e6299 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 @@ -39,7 +39,9 @@ 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())) + .setCheckable(true); if (currentServerVersionIsSupported) { if (board.isPermissionManage()) { AppCompatImageButton contextMenu = new AppCompatImageButton(context); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/FilesUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/FilesUtil.java new file mode 100644 index 000000000..6feb4d80c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/FilesUtil.java @@ -0,0 +1,78 @@ +package it.niedermann.nextcloud.deck.util; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import it.niedermann.nextcloud.deck.DeckLog; + +/** + * Created by stefan on 07.03.20. + */ + +public class FilesUtil { + + private FilesUtil() { + } + + /** + * https://help.nextcloud.com/t/android-app-select-file-with-nextcloud-app-file-cant-be-read/103706 + * Must not be called from the UI thread because the {@param currentUri} might refer to a not yet locally available file. + */ + @WorkerThread + 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()); + } + 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.getParentFile()"); + } + if (!tempDir.exists()) { + 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."); + } + } + + 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"); + } + + return cacheFile; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/MarkDownUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/MarkDownUtil.java deleted file mode 100644 index 9e1e5b147..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/MarkDownUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.content.Context; - -import androidx.core.content.ContextCompat; -import androidx.core.content.res.ResourcesCompat; - -import com.yydcdut.rxmarkdown.RxMDConfiguration.Builder; - -import it.niedermann.nextcloud.deck.R; - -/** - * Created by stefan on 07.12.16. - */ - -public class MarkDownUtil { - - private MarkDownUtil() {} - - /** - * Ensures every instance of RxMD uses the same configuration - * - * @param context Context - * @return RxMDConfiguration - */ - public static Builder getMarkDownConfiguration(Context context) { - return new Builder(context) - .setHeader2RelativeSize(1.35f) - .setHeader3RelativeSize(1.25f) - .setHeader4RelativeSize(1.15f) - .setHeader5RelativeSize(1.1f) - .setHeader6RelativeSize(1.05f) - .setHorizontalRulesHeight(2) - .setLinkFontColor(ContextCompat.getColor(context, R.color.primary)); - } - - public static Builder getMarkDownConfiguration(Context context, Boolean darkTheme) { - return new Builder(context) - .setHeader2RelativeSize(1.35f) - .setHeader3RelativeSize(1.25f) - .setHeader4RelativeSize(1.15f) - .setHeader5RelativeSize(1.1f) - .setHeader6RelativeSize(1.05f) - .setHorizontalRulesHeight(2) - .setLinkFontColor(ResourcesCompat.getColor(context.getResources(), R.color.primary, null)); - } -} 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 c2ba6f46a..d11631fde 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 @@ -5,7 +5,6 @@ import android.content.res.Resources; import android.graphics.Typeface; import android.text.Spannable; import android.text.SpannableString; -import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.text.style.ForegroundColorSpan; @@ -42,9 +41,9 @@ public class SpannableUtil { public static void setTextWithURL(@NonNull TextView textView, @NonNull Resources resources, @StringRes int containerTextId, @StringRes int linkLabelId, @StringRes int urlId) { 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); + final SpannableString finalSpannable = new SpannableString(finalText); + finalSpannable.setSpan(new URLSpan(resources.getString(urlId)), finalText.indexOf(linkLabel), finalText.indexOf(linkLabel) + linkLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + textView.setText(finalSpannable); textView.setMovementMethod(new LinkMovementMethod()); } } 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 fbc7fc3fc..d90ce297c 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 @@ -2,12 +2,8 @@ package it.niedermann.nextcloud.deck.util; import android.content.Context; import android.content.res.ColorStateList; -import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.style.ImageSpan; import android.widget.ImageView; import android.widget.TextView; @@ -15,7 +11,6 @@ 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; @@ -23,19 +18,12 @@ import androidx.core.widget.TextViewCompat; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.request.target.CustomTarget; -import com.bumptech.glide.request.transition.Transition; 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; @@ -88,63 +76,7 @@ public final class ViewUtil { return drawable; } - /** - * Replaces all mentions in the textView with an avatar and the display name - * - * @param account {@link Account} where the users of those mentions belong to - * @param mentions {@link List} of all mentions that should be substituted - * @param textView target {@link TextView} - */ - public static void setupMentions(@NonNull Account account, @NonNull List<Mention> mentions, TextView textView) { - Context context = textView.getContext(); - SpannableStringBuilder messageBuilder = new SpannableStringBuilder(textView.getText()); - - // Step 1 - // Add avatar icons and display names - for (Mention m : mentions) { - final String mentionId = "@" + m.getMentionId(); - final String mentionDisplayName = " " + m.getMentionDisplayName(); - int index = messageBuilder.toString().lastIndexOf(mentionId); - while (index >= 0) { - messageBuilder.setSpan(new ImageSpan(context, R.drawable.ic_person_grey600_24dp), index, index + mentionId.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - messageBuilder.insert(index + mentionId.length(), mentionDisplayName); - index = messageBuilder.toString().substring(0, index).lastIndexOf(mentionId); - } - } - textView.setText(messageBuilder); - - // Step 2 - // Replace avatar icons with real avatars - final ImageSpan[] list = messageBuilder.getSpans(0, messageBuilder.length(), ImageSpan.class); - for (ImageSpan span : list) { - final int spanStart = messageBuilder.getSpanStart(span); - final int spanEnd = messageBuilder.getSpanEnd(span); - Glide.with(context) - .asBitmap() - .placeholder(R.drawable.ic_person_grey600_24dp) - .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 - public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { - messageBuilder.removeSpan(span); - messageBuilder.setSpan(new ImageSpan(context, resource), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - // silence is gold - } - }); - } - 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)); - } + imageView.setImageTintList(ColorStateList.valueOf(ContextCompat.getColor(context, colorRes))); } } |