diff options
author | Stefan Niedermann <info@niedermann.it> | 2023-03-01 14:43:53 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2023-03-09 11:53:19 +0300 |
commit | 3ea462ca9e2ae18ba9d869125da8d8d07f2c7854 (patch) | |
tree | 30257c67768325d5972ec499a6eb41e11017ac6d | |
parent | bfab286b0bc6dbfac1211eec64d74b66b2ce1e6d (diff) |
refactor: Unidirectional data flow and single point of truth for current state
Signed-off-by: Stefan Niedermann <info@niedermann.it>
222 files changed, 7227 insertions, 4993 deletions
diff --git a/app/build.gradle b/app/build.gradle index 670758f59..7f28e2b9e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ android { buildToolsVersion "31.0.0" defaultConfig { applicationId "it.niedermann.nextcloud.deck" - minSdkVersion 23 + minSdkVersion 24 targetSdkVersion 33 versionCode 1021008 versionName "1.21.8" @@ -71,6 +71,7 @@ dependencies { implementation project(path: ':cross-tab-drag-and-drop') // TabLayoutHelper implementation project(path: ':tab-layout-helper') + implementation project(path: ':reactive-livedata') coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e8e3ae6e5..c05e81516 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,8 +29,9 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" + android:enableOnBackInvokedCallback="true" tools:ignore="GoogleAppIndexingWarning" - tools:targetApi="q"> + tools:targetApi="tiramisu"> <provider android:name="androidx.core.content.FileProvider" @@ -43,7 +44,7 @@ </provider> <activity - android:name=".ui.MainActivity" + android:name=".ui.main.MainActivity" android:label="@string/app_name_short" android:theme="@style/SplashTheme" android:exported="true"> @@ -67,7 +68,7 @@ <activity android:name=".ui.manageaccounts.ManageAccountsActivity" android:label="@string/manage_accounts" - android:parentActivityName=".ui.MainActivity" + android:parentActivityName=".ui.main.MainActivity" android:windowSoftInputMode="stateHidden" /> <activity @@ -94,24 +95,19 @@ </activity> <activity - android:name=".ui.archivedcards.ArchivedCardsActvitiy" - android:label="@string/archived_cards" - android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" /> - - <activity - android:name=".ui.archivedboards.ArchivedBoardsActvitiy" + android:name=".ui.archivedboards.ArchivedBoardsActivity" android:label="@string/archived_boards" - android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> <activity android:name=".ui.upcomingcards.UpcomingCardsActivity" android:label="@string/widget_upcoming_title" - android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> <activity android:name=".ui.card.EditActivity" android:label="@string/edit" - android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> <activity android:name=".ui.attachments.AttachmentsActivity" @@ -122,7 +118,7 @@ <activity android:name=".ui.settings.SettingsActivity" android:label="@string/simple_settings" - android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> <activity android:name=".ui.ImportAccountActivity" @@ -144,7 +140,7 @@ <activity android:name=".ui.about.AboutActivity" android:label="@string/about" - android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> <activity android:name=".ui.PushNotificationActivity" 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 9902e011d..b7711502c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java @@ -1,79 +1,46 @@ package it.niedermann.nextcloud.deck; -import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode; -import static androidx.lifecycle.Transformations.distinctUntilChanged; - import android.app.Application; -import android.content.Context; import android.os.StrictMode; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.lifecycle.LiveData; -import androidx.preference.PreferenceManager; - -import com.nextcloud.android.common.ui.util.PlatformThemeUtil; -import com.nextcloud.android.sso.helper.SingleAccountHelper; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; -import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData; -import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.persistence.PreferencesRepository; +import it.niedermann.nextcloud.deck.util.CustomAppGlideModule; public class DeckApplication extends Application { - public static final long NO_ACCOUNT_ID = -1L; - 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$; - private static LiveData<Integer> currentBoardColor$; + private final ExecutorService executor = new ThreadPoolExecutor(0, 2, 0L, TimeUnit.SECONDS, new SynchronousQueue<>()); @Override public void onCreate() { + final var repo = new PreferencesRepository(this); + if (BuildConfig.DEBUG) { enableStrictModeLogging(); } - PREF_KEY_THEME = getString(R.string.pref_key_dark_theme); - PREF_KEY_DEBUGGING = getString(R.string.pref_key_debugging); - setAppTheme(getAppThemeSetting(this)); - DeckLog.enablePersistentLogs(isPersistentLoggingEnabled(this)); - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - currentAccountColor$ = distinctUntilChanged(new SharedPreferenceIntLiveData(sharedPreferences, - getString(R.string.shared_preference_last_account_color), - ContextCompat.getColor(this, R.color.defaultBrand))); - currentBoardColor$ = distinctUntilChanged(new SharedPreferenceIntLiveData(sharedPreferences, - getString(R.string.shared_preference_theme_main), - ContextCompat.getColor(this, R.color.defaultBrand))); + repo.getAppThemeSetting().thenAcceptAsync(repo::setAppTheme, executor); + repo.isDebugModeEnabled().thenAcceptAsync(DeckLog::enablePersistentLogs, executor); + super.onCreate(); } @Override public void onLowMemory() { super.onLowMemory(); + DeckLog.error("--- Low memory: Clear Glide cache ---"); + CustomAppGlideModule.clearCache(this); + DeckLog.error("--- Low memory: Clear debug log ---"); DeckLog.clearDebugLog(); - DeckLog.error("--- cleared log because of low memory ---"); - } - - // --------- - // Debugging - // --------- - - private static boolean isPersistentLoggingEnabled(@NonNull Context context) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final boolean enabled = sharedPreferences.getBoolean(PREF_KEY_DEBUGGING, false); - DeckLog.log("--- Read:", PREF_KEY_DEBUGGING, "→", enabled); - return enabled; } - private static void enableStrictModeLogging() { + private void enableStrictModeLogging() { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() - // SSO library: setCurrentSingleSignOnAccount works synchronously - // Deck app: Handling of current account / board / stack works synchronously .permitDiskReads() .penaltyLog() .build()); @@ -82,95 +49,4 @@ public class DeckApplication extends Application { .penaltyLog() .build()); } - - // ----------------- - // Day / Night theme - // ----------------- - - public static void setAppTheme(int setting) { - setDefaultNightMode(setting); - } - - public static int getAppThemeSetting(@NonNull Context context) { - final var prefs = PreferenceManager.getDefaultSharedPreferences(context); - String mode; - try { - mode = prefs.getString(PREF_KEY_THEME, context.getString(R.string.pref_value_theme_system_default)); - } catch (ClassCastException e) { - mode = prefs.getBoolean(PREF_KEY_THEME, false) ? context.getString(R.string.pref_value_theme_dark) : context.getString(R.string.pref_value_theme_light); - } - return Integer.parseInt(mode); - } - - public static boolean isDarkTheme(@NonNull Context context) { - final var darkModeSetting = getAppThemeSetting(context); - return darkModeSetting == Integer.parseInt(context.getString(R.string.pref_value_theme_system_default)) - ? PlatformThemeUtil.isDarkMode(context) - : darkModeSetting == Integer.parseInt(context.getString(R.string.pref_value_theme_dark)); - } - - // -------------------------------------- - // Current account / board / stack states - // -------------------------------------- - - public static void saveCurrentAccount(@NonNull Context context, @NonNull Account account) { - SingleAccountHelper.setCurrentAccount(context, account.getName()); - final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_account), "→", account.getId()); - editor.putLong(context.getString(R.string.shared_preference_last_account), account.getId()); - DeckLog.log("--- Write:", context.getString(R.string.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$; - } - - public static LiveData<Integer> readCurrentBoardColor() { - return currentBoardColor$; - } - - @ColorInt - public static int readCurrentAccountColor(@NonNull Context context) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - @ColorInt final int accountColor = sharedPreferences.getInt(context.getString(R.string.shared_preference_last_account_color), context.getApplicationContext().getResources().getColor(R.color.defaultBrand)); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account_color), "→", accountColor); - return accountColor; - } - - public static long readCurrentAccountId(@NonNull Context context) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final long accountId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_account), NO_ACCOUNT_ID); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account), "→", accountId); - return accountId; - } - - public static void saveCurrentBoardId(@NonNull Context context, long accountId, long boardId) { - final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId); - editor.putLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, boardId); - editor.apply(); - } - - public static long readCurrentBoardId(@NonNull Context context, long accountId) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final long boardId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, NO_BOARD_ID); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId); - return boardId; - } - - public static void saveCurrentStackId(@NonNull Context context, long accountId, long boardId, long stackId) { - final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write:", context.getString(R.string.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(); - } - - public static long readCurrentStackId(@NonNull Context context, long accountId, long boardId) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final long savedStackId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, NO_STACK_ID); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", savedStackId); - return savedStackId; - } } 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 aa5733044..2118fc068 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 @@ -3,16 +3,10 @@ package it.niedermann.nextcloud.deck.api; import android.content.Context; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.api.NextcloudAPI; -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; -import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; -import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; -import it.niedermann.nextcloud.deck.DeckLog; import retrofit2.NextcloudRetrofitApiBuilder; /** @@ -27,36 +21,21 @@ public class ApiProvider { private NextcloudServerAPI nextcloudAPI; @NonNull private final Context context; - @Nullable - private final String ssoAccountName; - private SingleSignOnAccount ssoAccount; + private final SingleSignOnAccount ssoAccount; - public ApiProvider(@NonNull Context context, @Nullable String ssoAccountName) { + public ApiProvider(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { this.context = context; - this.ssoAccountName = ssoAccountName; - setAccount(); + this.ssoAccount = ssoAccount; } public synchronized void initSsoApi(@NonNull final NextcloudAPI.ApiConnectedListener callback) { - if(this.deckAPI == null) { + if (this.deckAPI == null) { final NextcloudAPI nextcloudAPI = new NextcloudAPI(context, ssoAccount, GsonConfig.getGson(), callback); this.deckAPI = new NextcloudRetrofitApiBuilder(nextcloudAPI, DECK_API_ENDPOINT).create(DeckAPI.class); this.nextcloudAPI = new NextcloudRetrofitApiBuilder(nextcloudAPI, NC_API_ENDPOINT).create(NextcloudServerAPI.class); } } - private void setAccount() { - try { - if (ssoAccountName == null) { - this.ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context); - } else { - this.ssoAccount = AccountImporter.getSingleSignOnAccount(context, ssoAccountName); - } - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - DeckLog.logError(e); - } - } - public DeckAPI getDeckAPI() { return deckAPI; } @@ -65,18 +44,4 @@ public class ApiProvider { return nextcloudAPI; } - public String getServerUrl(){ - if (ssoAccount == null) { - setAccount(); - } - return ssoAccount.url; - } - - public String getApiPath() { - return DECK_API_ENDPOINT; - } - - public String getApiUrl() { - return getServerUrl() + getApiPath(); - } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java index 11fbf8e14..a5d3e3155 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java @@ -7,8 +7,8 @@ import com.google.gson.JsonSyntaxException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; public enum HandledServerErrors { - UNKNOWN(1337, "hopefully won't occurr"), - LABELS_TITLE_MUST_BE_UNIQUE(400, "title must be unique"), + UNKNOWN(1337, "hopefully won't occur"), + LABELS_TITLE_MUST_BE_UNIQUE(400, "Title must be unique"), ATTACHMENTS_FILE_ALREADY_EXISTS(409, "File already exists."), ; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java index d7c4e8e8f..a54ad0986 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java @@ -12,7 +12,7 @@ import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; -import com.nextcloud.android.sso.model.SingleSignOnAccount; +import com.bumptech.glide.Glide; import java.io.Serializable; import java.util.Objects; @@ -20,7 +20,7 @@ import java.util.Objects; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.Version; -import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; @Entity(indices = {@Index(value = "name", unique = true)}) public class Account implements Serializable { @@ -218,12 +218,17 @@ public class Account implements Serializable { } /** - * A cache buster parameter is added for duplicate account names on different hosts which shall be fetched from the same {@link SingleSignOnAccount} (e. g. {@link AccountSwitcherDialog}) - * - * @return an {@link String} to fetch the avatar for this account. + * @return The {@link #getAvatarUrl(int, String)} of this {@link Account} */ - public String getAvatarUrl(@Px int size) { - return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size; + public SingleSignOnUrl getAvatarUrl(@Px int size) { + return getAvatarUrl(size, getUserName()); + } + + /** + * @return a {@link SingleSignOnUrl} to fetch the avatar of the given <code>userName</code> from the instance of this {@link Account} via {@link Glide}. + */ + public SingleSignOnUrl getAvatarUrl(@Px int size, @NonNull String userName) { + return new SingleSignOnUrl(getName(), getUrl() + "/index.php/avatar/" + Uri.encode(userName) + "/" + size); } @Override @@ -239,9 +244,7 @@ public class Account implements Serializable { url.equals(account.url) && color.equals(account.color) && textColor.equals(account.textColor) && - serverDeckVersion.equals(account.serverDeckVersion) && - Objects.equals(etag, account.etag) && - Objects.equals(boardsEtag, account.boardsEtag); + serverDeckVersion.equals(account.serverDeckVersion); } @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java index ebe9805ed..d27c35545 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java @@ -12,6 +12,7 @@ import com.google.gson.annotations.JsonAdapter; import java.io.Serializable; import java.time.Instant; +import java.util.Objects; import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; @@ -218,9 +219,9 @@ public class Board extends AbstractRemoteEntity implements Serializable { if (permissionEdit != board.permissionEdit) return false; if (permissionManage != board.permissionManage) return false; if (permissionShare != board.permissionShare) return false; - if (title != null ? !title.equals(board.title) : board.title != null) return false; - if (color != null ? !color.equals(board.color) : board.color != null) return false; - return deletedAt != null ? deletedAt.equals(board.deletedAt) : board.deletedAt == null; + if (!Objects.equals(title, board.title)) return false; + if (!Objects.equals(color, board.color)) return false; + return Objects.equals(deletedAt, board.deletedAt); } @Override 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 4f4698a5a..355307ce0 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 @@ -19,9 +19,9 @@ public class Version implements Comparable<Version> { private static final Version VERSION_1_3_0 = new Version("1.3.0", 1, 3, 0); private String originalVersion = "?"; - private int major; - private int minor; - private int patch; + private final int major; + private final int minor; + private final int patch; public Version(String originalVersion, int major, int minor, int patch) { this(major, minor, patch); @@ -54,12 +54,8 @@ public class Version implements Comparable<Version> { return originalVersion; } - public void setOriginalVersion(String originalVersion) { - this.originalVersion = originalVersion; - } - public static Version of(String versionString) { - int major = 0, minor = 0, micro = 0; + int major = 0, minor = 0, patch = 0; if (versionString != null) { final String[] split = versionString.split("\\."); if (split.length > 0) { @@ -67,12 +63,12 @@ public class Version implements Comparable<Version> { if (split.length > 1) { minor = extractNumber(split[1]); if (split.length > 2) { - micro = extractNumber(split[2]); + patch = extractNumber(split[2]); } } } } - return new Version(versionString, major, minor, micro); + return new Version(versionString, major, minor, patch); } private static int extractNumber(String containsNumbers) { @@ -187,6 +183,17 @@ public class Version implements Comparable<Version> { } /** + * The first response structure of the very first call to at least the <code>/boards</code> endpoint of the Deck API can be different compared to all following calls. + * This behavior is tracked in an upstream issue and might be resolved in the future with a specific version. + * + * @return whether or not it is needed to make one request that must be ignored due to a different data structure before performing any other requests. + * @see <a href="https://github.com/nextcloud/deck/issues/3229">issue</a> + */ + public boolean firstCallHasDifferentResponseStructure() { + return true; + } + + /** * URL to view a card in the web interface has been changed in {@link Version} 1.0.0 * * @return the id of the string resource which contains the partial URL to open a card in the web UI diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java new file mode 100644 index 000000000..570b36b13 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java @@ -0,0 +1,575 @@ +package it.niedermann.nextcloud.deck.persistence; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.AnyThread; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; +import androidx.lifecycle.LiveData; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +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.model.AccessControl; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; +import it.niedermann.nextcloud.deck.model.enums.DBStatus; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; +import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; +import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.internal.FilterInformation; +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.db.DataBaseAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil; +import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem; + +/** + * Allows basic local access to the {@link DataBaseAdapter} layer but also to some app states which are stored in {@link SharedPreferences}. + * <p> + * This repository does not know anything about remote synchronization. + */ +@SuppressWarnings("WeakerAccess") +public class BaseRepository { + + @NonNull + protected final Context context; + @NonNull + protected final DataBaseAdapter dataBaseAdapter; + @NonNull + protected final ExecutorService executor; + @NonNull + protected final ConnectivityUtil connectivityUtil; + @NonNull + protected final ReactiveLiveData<Long> currentAccountId$; + + public BaseRepository(@NonNull Context context) { + this(context, new ConnectivityUtil(context)); + } + + protected BaseRepository(@NonNull Context context, @NonNull ConnectivityUtil connectivityUtil) { + this(context, connectivityUtil, new DataBaseAdapter(context.getApplicationContext()), Executors.newCachedThreadPool()); + } + + protected BaseRepository(@NonNull Context context, + @NonNull ConnectivityUtil connectivityUtil, + @NonNull DataBaseAdapter databaseAdapter, + @NonNull ExecutorService executor) { + this.context = context.getApplicationContext(); + this.connectivityUtil = connectivityUtil; + this.dataBaseAdapter = databaseAdapter; + this.executor = executor; + this.currentAccountId$ = new ReactiveLiveData<>(dataBaseAdapter.getCurrentAccountId$()).distinctUntilChanged(); + LastSyncUtil.init(context.getApplicationContext()); + } + + public void saveCurrentAccount(@NonNull Account account) { + dataBaseAdapter.saveCurrentAccount(account); + } + + public LiveData<Long> getCurrentAccountId$() { + return this.currentAccountId$; + } + + public CompletableFuture<Long> getCurrentAccountId() { + return dataBaseAdapter.getCurrentAccountId(); + } + + public LiveData<Integer> getAccountColor(long accountId) { + return dataBaseAdapter.getAccountColor(accountId); + } + + @ColorInt + public CompletableFuture<Integer> getCurrentAccountColor(long accountId) { + return dataBaseAdapter.getCurrentAccountColor(accountId); + } + + // ------------- + // Current board + // ------------- + + public void saveCurrentBoardId(long accountId, long boardId) { + dataBaseAdapter.saveCurrentBoardId(accountId, boardId); + } + + public LiveData<Long> getCurrentBoardId$(long accountId) { + return dataBaseAdapter.getCurrentBoardId$(accountId); + } + + public LiveData<Integer> getBoardColor$(long accountId, long boardId) { + return dataBaseAdapter.getBoardColor$(accountId, boardId); + } + + public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) { + return dataBaseAdapter.getCurrentBoardColor(accountId, boardId); + } + + // ------------- + // Current stack + // ------------- + + public void saveCurrentStackId(long accountId, long boardId, long stackId) { + dataBaseAdapter.saveCurrentStackId(accountId, boardId, stackId); + } + + public LiveData<Long> getCurrentStackId$(long accountId, long boardId) { + return dataBaseAdapter.getCurrentStackId$(accountId, boardId); + } + + // ================================================================================================================================== + + @AnyThread + public void createAccount(@NonNull Account account, @NonNull IResponseCallback<Account> callback) { + executor.submit(() -> { + try { + callback.onResponse(dataBaseAdapter.createAccountDirectly(account)); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void deleteAccount(long id) { + executor.submit(() -> { + dataBaseAdapter.saveNeighbourOfAccount(id); + dataBaseAdapter.removeCurrentBoardId(id); + dataBaseAdapter.deleteAccount(id); + LastSyncUtil.resetLastSyncDate(id); + }); + } + + @AnyThread + public LiveData<Boolean> hasAccounts() { + return dataBaseAdapter.hasAccounts(); + } + + @UiThread + public LiveData<Account> readAccount(long id) { + return dataBaseAdapter.readAccount(id); + } + + @WorkerThread + public Account readAccountDirectly(long id) { + return dataBaseAdapter.readAccountDirectly(id); + } + + @WorkerThread + public Account readAccountDirectly(@Nullable String name) { + return dataBaseAdapter.readAccountDirectly(name); + } + + @UiThread + public LiveData<Account> readAccount(@Nullable String name) { + return dataBaseAdapter.readAccount(name); + } + + @WorkerThread + public Long getBoardLocalIdByAccountAndCardRemoteIdDirectly(long accountId, long cardRemoteId) { + return dataBaseAdapter.getBoardLocalIdByAccountAndCardRemoteIdDirectly(accountId, cardRemoteId); + } + + @UiThread + public LiveData<List<Account>> readAccounts() { + return dataBaseAdapter.readAccounts(); + } + + @WorkerThread + public List<Account> readAccountsDirectly() { + return dataBaseAdapter.getAllAccountsDirectly(); + } + + /** + * @param localProjectId LocalId of the OcsProject + * @return all {@link OcsProjectResource}s of the Project + */ + @AnyThread + public LiveData<List<OcsProjectResource>> getResourcesForProject(long localProjectId) { + return dataBaseAdapter.getResourcesByLocalProjectId(localProjectId); + } + + /** + * @param accountId ID of the account + * @param archived Decides whether only archived or not-archived boards for the specified account will be returned + * @return all archived or non-archived <code>Board</code>s depending on <code>archived</code> parameter + */ + @AnyThread + public LiveData<List<Board>> getBoards(long accountId, boolean archived) { + return dataBaseAdapter.getBoards(accountId, archived); + } + + /** + * @param accountId ID of the account + * @param archived Decides whether only archived or not-archived boards for the specified account will be returned + * @return all archived or non-archived <code>FullBoard</code>s depending on <code>archived</code> parameter + */ + @AnyThread + public LiveData<List<FullBoard>> getFullBoards(long accountId, boolean archived) { + return dataBaseAdapter.getFullBoards(accountId, archived); + } + + /** + * Get all non-archived <code>FullBoard</code>s with edit permissions for the specified account. + * + * @param accountId ID of the account + * @return all non-archived <code>Board</code>s with edit permission + */ + @AnyThread + public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) { + return dataBaseAdapter.getBoardsWithEditPermission(accountId); + } + + @AnyThread + public LiveData<Boolean> hasArchivedBoards(long accountId) { + return dataBaseAdapter.hasArchivedBoards(accountId); + } + + public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { + return dataBaseAdapter.getFullBoardById(accountId, localId); + } + + public Board getBoardById(Long localId) { + return dataBaseAdapter.getBoardByLocalIdDirectly(localId); + } + + public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) { + return dataBaseAdapter.getFullCommentsForLocalCardId(localCardId); + } + + public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { + return dataBaseAdapter.getStacksForBoard(accountId, localBoardId); + } + + public LiveData<FullStack> getStack(long accountId, long localStackId) { + return dataBaseAdapter.getStack(accountId, localStackId); + } + + public void countCardsInStackDirectly(long accountId, long localStackId, @NonNull IResponseCallback<Integer> callback) { + executor.submit(() -> dataBaseAdapter.countCardsInStackDirectly(accountId, localStackId, callback)); + } + + public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback<Integer> callback) { + executor.submit(() -> dataBaseAdapter.countCardsWithLabel(localLabelId, callback)); + } + + public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { + return dataBaseAdapter.getCardWithProjectsByLocalId(accountId, cardLocalId); + } + + public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { + return dataBaseAdapter.getFullCardsForStack(accountId, localStackId, filter); + } + + @WorkerThread + public Long getBoardLocalIdByLocalCardIdDirectly(long localCardId) { + return dataBaseAdapter.getBoardLocalIdByLocalCardIdDirectly(localCardId); + } + + @WorkerThread + public AccessControl getAccessControlByRemoteIdDirectly(long accountId, Long id) { + return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, id); + } + + public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) { + return dataBaseAdapter.getAccessControlByLocalBoardId(accountId, id); + } + + // --- User search --- + + public LiveData<List<User>> findProposalsForUsersToAssignForACL(final long accountId, long boardId, final int topX) { + return dataBaseAdapter.findProposalsForUsersToAssignForACL(accountId, boardId, topX); + } + + public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedToACL, final String constraint) { + return dataBaseAdapter.searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedToACL, constraint); + } + + public LiveData<List<User>> findProposalsForUsersToAssignForCards(final long accountId, long boardId, long notAssignedToLocalCardId, final int topX) { + return dataBaseAdapter.findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX); + } + + public LiveData<List<User>> searchUserByUidOrDisplayNameForCards(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, final String searchTerm) { + return dataBaseAdapter.searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, searchTerm); + } + + // --- Label search --- + + public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) { + return findProposalsForLabelsToAssign(accountId, boardId, -1L); + } + + public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId, long notAssignedToLocalCardId) { + return dataBaseAdapter.findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId); + } + + public LiveData<List<Label>> searchNotYetAssignedLabelsByTitle(@NonNull Account account, final long boardId, final long notYetAssignedToLocalCardId, @NonNull String searchTerm) { + return dataBaseAdapter.searchNotYetAssignedLabelsByTitle(account.getId(), boardId, notYetAssignedToLocalCardId, searchTerm); + } + + public LiveData<User> getUserByLocalId(long accountId, long localId) { + return dataBaseAdapter.getUserByLocalId(accountId, localId); + } + + public LiveData<User> getUserByUid(long accountId, String uid) { + return dataBaseAdapter.getUserByUid(accountId, uid); + } + + @WorkerThread + public User getUserByUidDirectly(long accountId, String uid) { + return dataBaseAdapter.getUserByUidDirectly(accountId, uid); + } + + public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { + return dataBaseAdapter.getBoardByRemoteId(accountId, remoteId); + } + + @WorkerThread + public Board getBoardByRemoteIdDirectly(long accountId, long remoteId) { + return dataBaseAdapter.getBoardByRemoteIdDirectly(accountId, remoteId); + } + + public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) { + return dataBaseAdapter.getStackByRemoteId(accountId, localBoardId, remoteId); + } + + public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) { + return dataBaseAdapter.getCardByRemoteID(accountId, remoteId); + } + + @WorkerThread + public Optional<Card> getCardByRemoteIDDirectly(long accountId, long remoteId) { + return Optional.ofNullable(dataBaseAdapter.getCardByRemoteIDDirectly(accountId, remoteId)); + } + + public long createUser(long accountId, User user) { + return dataBaseAdapter.createUser(accountId, user); + } + + public void updateUser(long accountId, @NonNull User user) { + dataBaseAdapter.updateUser(accountId, user, true); + } + + protected void reorderLocally(List<FullCard> cardsOfNewStack, @NonNull FullCard movedCard, long newStackId, int newOrder) { + // set new stack and order + Card movedInnerCard = movedCard.getCard(); + int oldOrder = movedInnerCard.getOrder(); + long oldStackId = movedInnerCard.getStackId(); + + + List<Card> changedCards = new ArrayList<>(); + + int startingAtOrder = newOrder; + if (oldStackId == newStackId) { + // card was only reordered in the same stack + movedInnerCard.setStatusEnum(movedInnerCard.getStatus() == DBStatus.LOCAL_MOVED.getId() ? DBStatus.LOCAL_MOVED : DBStatus.LOCAL_EDITED); + // move direction? + if (oldOrder > newOrder) { + // up + changedCards.add(movedCard.getCard()); + for (FullCard cardToUpdate : cardsOfNewStack) { + Card cardEntity = cardToUpdate.getCard(); + if (cardEntity.getOrder() < newOrder) { + continue; + } + if (cardEntity.getOrder() >= oldOrder) { + break; + } + changedCards.add(cardEntity); + } + } else { + // down + startingAtOrder = oldOrder; + for (FullCard cardToUpdate : cardsOfNewStack) { + Card cardEntity = cardToUpdate.getCard(); + if (cardEntity.getOrder() <= oldOrder) { + continue; + } + if (cardEntity.getOrder() > newOrder) { + break; + } + changedCards.add(cardEntity); + } + changedCards.add(movedCard.getCard()); + } + } else { + // card was moved to an other stack + movedInnerCard.setStackId(newStackId); + movedInnerCard.setStatusEnum(DBStatus.LOCAL_MOVED); + changedCards.add(movedCard.getCard()); + for (FullCard fullCard : cardsOfNewStack) { + // skip unchanged cards + if (fullCard.getCard().getOrder() < newOrder) { + continue; + } + changedCards.add(fullCard.getCard()); + } + } + reorderAscending(movedInnerCard, changedCards, startingAtOrder); + } + + private void reorderAscending(@NonNull Card movedCard, @NonNull List<Card> cardsToReorganize, int startingAtOrder) { + final Instant now = Instant.now(); + for (Card card : cardsToReorganize) { + card.setOrder(startingAtOrder); + if (card.getStatus() == DBStatus.UP_TO_DATE.getId()) { + card.setStatusEnum(DBStatus.LOCAL_EDITED_SILENT); + card.setLastModifiedLocal(now); + } + startingAtOrder++; + } + //update the moved one first, because otherwise a bunch of livedata is fired, leading the card to dispose and reappear + cardsToReorganize.remove(movedCard); + dataBaseAdapter.updateCard(movedCard, false); + for (Card card : cardsToReorganize) { + dataBaseAdapter.updateCard(card, false); + } + } + + // ------------------- + // Widgets + // ------------------- + + // # filter widget + + @AnyThread + public void createFilterWidget(@NonNull FilterWidget filterWidget, @NonNull IResponseCallback<Integer> callback) { + executor.submit(() -> { + try { + int filterWidgetId = dataBaseAdapter.createFilterWidgetDirectly(filterWidget); + callback.onResponse(filterWidgetId); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void updateFilterWidget(@NonNull FilterWidget filterWidget, @NonNull ResponseCallback<Boolean> callback) { + executor.submit(() -> { + try { + dataBaseAdapter.updateFilterWidgetDirectly(filterWidget); + callback.onResponse(Boolean.TRUE); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void getFilterWidget(@NonNull Integer filterWidgetId, @NonNull IResponseCallback<FilterWidget> callback) { + executor.submit(() -> { + try { + callback.onResponse(dataBaseAdapter.getFilterWidgetByIdDirectly(filterWidgetId)); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void deleteFilterWidget(int filterWidgetId, @NonNull IResponseCallback<Boolean> callback) { + executor.submit(() -> { + 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); + } + + @WorkerThread + public LiveData<List<UpcomingCardsAdapterItem>> getCardsForUpcomingCards() { + return dataBaseAdapter.getCardsForUpcomingCard(); + } + + @WorkerThread + public List<UpcomingCardsAdapterItem> getCardsForUpcomingCardsForWidget() { + return dataBaseAdapter.getCardsForUpcomingCardForWidget(); + } + + // # 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. + */ + @AnyThread + public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) { + executor.submit(() -> dataBaseAdapter.createSingleCardWidget(widgetId, accountId, boardId, localCardId)); + } + + @WorkerThread + public FullSingleCardWidgetModel getSingleCardWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { + final FullSingleCardWidgetModel model = dataBaseAdapter.getFullSingleCardWidgetModel(appWidgetId); + if (model == null) { + throw new NoSuchElementException("There is no " + FullSingleCardWidgetModel.class.getSimpleName() + " with the given appWidgetId " + appWidgetId); + } + return model; + } + + @AnyThread + public void deleteSingleCardWidgetModel(int widgetId) { + executor.submit(() -> dataBaseAdapter.deleteSingleCardWidget(widgetId)); + } + + public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { + executor.submit(() -> dataBaseAdapter.createStackWidget(appWidgetId, accountId, stackId, darkTheme)); + } + + @WorkerThread + public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { + final StackWidgetModel model = dataBaseAdapter.getStackWidgetModelDirectly(appWidgetId); + if (model == null) { + throw new NoSuchElementException(); + } + return model; + } + + public void deleteStackWidgetModel(int appWidgetId) { + executor.submit(() -> dataBaseAdapter.deleteStackWidget(appWidgetId)); + } + + @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/PreferencesRepository.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java new file mode 100644 index 000000000..83101ad3d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java @@ -0,0 +1,79 @@ +package it.niedermann.nextcloud.deck.persistence; + +import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode; +import static java.util.concurrent.CompletableFuture.supplyAsync; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.preference.PreferenceManager; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; + +public class PreferencesRepository { + + private final ExecutorService executor; + private final String PREF_KEY_THEME; + private final String PREF_KEY_DEBUGGING; + private final SharedPreferences sharedPreferences; + private final Context context; + + public PreferencesRepository(@NonNull Context context) { + this(context, new ThreadPoolExecutor(0, 2, 0L, TimeUnit.SECONDS, new SynchronousQueue<>())); + } + + public PreferencesRepository(@NonNull Context context, @NonNull ExecutorService executor) { + this.context = context.getApplicationContext(); + this.executor = executor; + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + + PREF_KEY_THEME = this.context.getString(R.string.pref_key_dark_theme); + PREF_KEY_DEBUGGING = this.context.getString(R.string.pref_key_debugging); + } + + // --------- + // Debugging + // --------- + + public CompletableFuture<Boolean> isDebugModeEnabled() { + return supplyAsync(() -> { + final boolean enabled = sharedPreferences.getBoolean(PREF_KEY_DEBUGGING, false); + DeckLog.log("--- Read:", PREF_KEY_DEBUGGING, "→", enabled); + return enabled; + }, executor); + } + + public LiveData<Boolean> isDebugModeEnabled$() { + return new SharedPreferenceBooleanLiveData(sharedPreferences, PREF_KEY_DEBUGGING, false); + } + + // ----- + // Theme + // ----- + + public void setAppTheme(int setting) { + setDefaultNightMode(setting); + } + + public CompletableFuture<Integer> getAppThemeSetting() { + return supplyAsync(() -> { + String mode; + try { + mode = sharedPreferences.getString(PREF_KEY_THEME, context.getString(R.string.pref_value_theme_system_default)); + } catch (ClassCastException e) { + mode = sharedPreferences.getBoolean(PREF_KEY_THEME, false) ? context.getString(R.string.pref_value_theme_dark) : context.getString(R.string.pref_value_theme_light); + } + return Integer.parseInt(mode); + }, executor); + } +} 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 50de4d2e4..9067809ce 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 @@ -12,15 +12,16 @@ import androidx.annotation.AnyThread; import androidx.annotation.ColorInt; import androidx.annotation.MainThread; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import androidx.annotation.WorkerThread; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; +import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.api.ParsedResponse; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; +import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.File; import java.time.Instant; @@ -29,12 +30,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -54,25 +52,18 @@ import it.niedermann.nextcloud.deck.model.JoinCardWithUser; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; -import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; -import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.internal.FilterInformation; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment; -import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; -import it.niedermann.nextcloud.deck.model.ocs.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.model.ocs.user.OcsUserList; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; 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.WrappedLiveData; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData; import it.niedermann.nextcloud.deck.persistence.sync.helpers.DataPropagationHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AbstractSyncDataProvider; @@ -87,146 +78,81 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.LabelData import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.StackDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithAclDownSyncDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithStacksAndLabelsUpSyncDataProvider; -import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem; -import it.niedermann.nextcloud.deck.util.ExecutorServiceProvider; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil; +/** + * Extends {@link BaseRepository} by synchronization capabilities. + * Therefore it always requires an {@link Account} to choose the correct {@link SingleSignOnAccount} for network operations. + */ @SuppressWarnings("WeakerAccess") -public class SyncManager { +public class SyncManager extends BaseRepository { @NonNull - private final Context appContext; - @NonNull - private final DataBaseAdapter dataBaseAdapter; - @NonNull private final ServerAdapter serverAdapter; @NonNull - private final ExecutorService executor; - @NonNull private final SyncHelper.Factory syncHelperFactory; @AnyThread - public SyncManager(@NonNull Context context) { - this(context, null); + public SyncManager(@NonNull Context context, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + this(context, AccountImporter.getSingleSignOnAccount(context, account.getName()), new ConnectivityUtil(context)); } - @AnyThread - public SyncManager(@NonNull Context context, @Nullable String ssoAccountName) { - this(context, - new DataBaseAdapter(context.getApplicationContext()), - new ServerAdapter(context.getApplicationContext(), ssoAccountName), - ExecutorServiceProvider.getExecutorService(), - SyncHelper::new); - LastSyncUtil.init(context.getApplicationContext()); + private SyncManager(@NonNull Context context, + @NonNull SingleSignOnAccount ssoAccount, + @NonNull ConnectivityUtil connectivityUtil) { + this(context, new ServerAdapter(context.getApplicationContext(), ssoAccount, connectivityUtil), connectivityUtil, SyncHelper::new); } - private SyncManager(@NonNull Context context, - @NonNull DataBaseAdapter databaseAdapter, - @NonNull ServerAdapter serverAdapter, - @NonNull ExecutorService executor, - @NonNull SyncHelper.Factory syncHelperFactory) { - this.appContext = context.getApplicationContext(); - this.dataBaseAdapter = databaseAdapter; + protected SyncManager(@NonNull Context context, + @NonNull ServerAdapter serverAdapter, + @NonNull ConnectivityUtil connectivityUtil, + @NonNull SyncHelper.Factory syncHelperFactory) { + super(context, connectivityUtil); this.serverAdapter = serverAdapter; - this.executor = executor; this.syncHelperFactory = syncHelperFactory; + LastSyncUtil.init(context.getApplicationContext()); } - @WorkerThread - public Long getBoardLocalIdByAccountAndCardRemoteIdDirectly(long accountId, long cardRemoteId) { - return dataBaseAdapter.getBoardLocalIdByAccountAndCardRemoteIdDirectly(accountId, cardRemoteId); - } - - @WorkerThread - public boolean synchronizeEverything() { - List<Account> accounts = dataBaseAdapter.getAllAccountsDirectly(); - if (accounts.size() > 0) { - final AtomicBoolean success = new AtomicBoolean(true); - CountDownLatch latch = new CountDownLatch(accounts.size()); - try { - for (Account account : accounts) { - new SyncManager(dataBaseAdapter.getContext(), account.getName()).synchronize(new ResponseCallback<>(account) { - @Override - public void onResponse(Boolean response) { - success.set(success.get() && Boolean.TRUE.equals(response)); - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - success.set(false); - super.onError(throwable); - latch.countDown(); - } - }); - } - latch.await(); - return success.get(); - } catch (InterruptedException e) { - DeckLog.logError(e); - return false; - } - } - return true; - } - - @AnyThread - public void synchronizeBoard(long localBoardId, @NonNull ResponseCallback<Boolean> responseCallback) { - executor.submit(() -> { - FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoardId); - try { - syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) - .setResponseCallback(responseCallback) - .doSyncFor(new StackDataProvider(null, board)); - } catch (OfflineException e) { - responseCallback.onError(e); - } - }); + @VisibleForTesting + protected SyncManager(@NonNull Context context, + @NonNull ServerAdapter serverAdapter, + @NonNull ConnectivityUtil connectivityUtil, + @NonNull SyncHelper.Factory syncHelperFactory, + @NonNull DataBaseAdapter databaseAdapter, + @NonNull ExecutorService executor) { + super(context, connectivityUtil, databaseAdapter, executor); + this.serverAdapter = serverAdapter; + this.syncHelperFactory = syncHelperFactory; + LastSyncUtil.init(context.getApplicationContext()); } @AnyThread - public void synchronizeCard(@NonNull ResponseCallback<Boolean> responseCallback, @NonNull Card card) { - executor.submit(() -> { - FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); - Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - try { - syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) - .setResponseCallback(responseCallback) - .doSyncFor(new CardDataProvider(null, board, stack)); - } catch (OfflineException e) { - responseCallback.onError(e); - } - }); + public void fetchBoardsFromServer(@NonNull ResponseCallback<ParsedResponse<List<FullBoard>>> callback) { + executor.submit(() -> serverAdapter.getBoards(callback)); } @AnyThread public LiveData<Pair<Integer, Integer>> synchronize(@NonNull ResponseCallback<Boolean> responseCallback) { - MutableLiveData<Pair<Integer, Integer>> progress$ = new MutableLiveData<>(); - Account callbackAccount = responseCallback.getAccount(); - if (callbackAccount == null) { - throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + ResponseCallback.class.getSimpleName() + " must not be null."); - } - Long callbackAccountId = callbackAccount.getId(); - if (callbackAccountId == null) { - throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + ResponseCallback.class.getSimpleName() + " must contain a valid id, but given id was null."); - } + final var progress$ = new MutableLiveData<Pair<Integer, Integer>>(); + final var callbackAccount = responseCallback.getAccount(); + final long callbackAccountId = callbackAccount.getId(); + executor.submit(() -> { refreshCapabilities(new ResponseCallback<>(responseCallback.getAccount()) { @Override public void onResponse(Capabilities response) { if (response != null && !response.isMaintenanceEnabled()) { if (response.getDeckVersion().isSupported()) { - long accountId = callbackAccountId; - Instant lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId); + final var lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId); + final var syncHelper = syncHelperFactory.create(serverAdapter, dataBaseAdapter, lastSyncDate); - final SyncHelper syncHelper = syncHelperFactory.create(serverAdapter, dataBaseAdapter, lastSyncDate); - - ResponseCallback<Boolean> callback = new ResponseCallback<>(callbackAccount) { + final var callback = new ResponseCallback<Boolean>(callbackAccount) { @Override public void onResponse(Boolean response) { syncHelper.setResponseCallback(new ResponseCallback<>(account) { @Override public void onResponse(Boolean response) { - LastSyncUtil.setLastSyncDate(accountId, Instant.now()); + LastSyncUtil.setLastSyncDate(callbackAccountId, Instant.now()); responseCallback.onResponse(response); } @@ -284,114 +210,44 @@ public class SyncManager { return progress$; } -// -// private <T> IResponseCallback<T> wrapCallForUi(IResponseCallback<T> responseCallback) { -// Account account = responseCallback.getAccount(); -// if (account == null || account.getId() == null) { -// throw new IllegalArgumentException("Bro. Please just give me a damn Account!"); -// } -// return new IResponseCallback<T>(responseCallback.getAccount()) { -// @Override -// public void onResponse(T response) { -// sourceActivity.runOnUiThread(() -> { -// fillAccountIDs(response); -// responseCallback.onResponse(response); -// }); -// } -// -// @Override -// public void onError(Throwable throwable) { -// responseCallback.onError(throwable); -// } -// }; -// } - -// private <T extends AbstractRemoteEntity> T applyUpdatesFromRemote(T localEntity, T remoteEntity, Long accountId) { -// if (!localEntity.getId().equals(remoteEntity.getId()) -// || !accountId.equals(localEntity.getAccountId())) { -// throw new IllegalArgumentException("IDs of Account or Entity are not matching! WTF are you doin?!"); -// } -// remoteEntity.setLastModifiedLocal(remoteEntity.getLastModified()); // not an error! local-modification = remote-mod -// remoteEntity.setLocalId(localEntity.getLocalId()); -// return remoteEntity; -// } - - @AnyThread - public LiveData<Boolean> hasAccounts() { - return dataBaseAdapter.hasAccounts(); - } - @AnyThread - public void createAccount(@NonNull Account account, @NonNull IResponseCallback<Account> callback) { + public void synchronizeBoard(long localBoardId, @NonNull ResponseCallback<Boolean> responseCallback) { executor.submit(() -> { + FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoardId); try { - final Account createdAccount = dataBaseAdapter.createAccountDirectly(account); - if (createdAccount == null) { - throw new RuntimeException("Created account is null. Source: " + account); - } - // TODO: throw this shit away - // that's why we do this: https://github.com/nextcloud/deck/issues/3229 - serverAdapter.getBoards(new ResponseCallback<>(createdAccount) { - @Override - public void onResponse(ParsedResponse<List<FullBoard>> response) { - callback.onResponse(createdAccount); - } - - @SuppressLint("MissingSuperCall") - @Override - public void onError(Throwable throwable) { - callback.onResponse(createdAccount); - } - }); - // TODO: and replace with this line: -// callback.onResponse(createdAccount); - } catch (Throwable t) { - callback.onError(t); + syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) + .setResponseCallback(responseCallback) + .doSyncFor(new StackDataProvider(null, board)); + } catch (OfflineException e) { + responseCallback.onError(e); } }); } - public boolean hasInternetConnection() { - return serverAdapter.hasInternetConnection(); - } - @AnyThread - public void deleteAccount(long id) { + public void synchronizeCard(@NonNull ResponseCallback<Boolean> responseCallback, @NonNull Card card) { executor.submit(() -> { - dataBaseAdapter.deleteAccount(id); - LastSyncUtil.resetLastSyncDate(id); + FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); + Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); + try { + syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) + .setResponseCallback(responseCallback) + .doSyncFor(new CardDataProvider(null, board, stack)); + } catch (OfflineException e) { + responseCallback.onError(e); + } }); } - @UiThread - public LiveData<Account> readAccount(long id) { - return dataBaseAdapter.readAccount(id); - } - - @WorkerThread - public Account readAccountDirectly(long id) { - return dataBaseAdapter.readAccountDirectly(id); - } - - @WorkerThread - public Account readAccountDirectly(@Nullable String name) { - return dataBaseAdapter.readAccountDirectly(name); - } - - @UiThread - public LiveData<Account> readAccount(@Nullable String name) { - return dataBaseAdapter.readAccount(name); - } - - @UiThread - public LiveData<List<Account>> readAccounts() { - return dataBaseAdapter.readAccounts(); - } - - @WorkerThread - public List<Account> readAccountsDirectly() { - return dataBaseAdapter.getAllAccountsDirectly(); - } +// private <T extends AbstractRemoteEntity> T applyUpdatesFromRemote(T localEntity, T remoteEntity, Long accountId) { +// if (!localEntity.getId().equals(remoteEntity.getId()) +// || !accountId.equals(localEntity.getAccountId())) { +// throw new IllegalArgumentException("IDs of Account or Entity are not matching! WTF are you doin?!"); +// } +// remoteEntity.setLastModifiedLocal(remoteEntity.getLastModified()); // not an error! local-modification = remote-mod +// remoteEntity.setLocalId(localEntity.getLocalId()); +// return remoteEntity; +// } /** * <p> @@ -487,66 +343,10 @@ public class SyncManager { }); } - /** - * @param accountId ID of the account - * @return all {@link Board}s no matter if {@link Board#archived} or not. - */ - @SuppressWarnings("JavadocReference") - @AnyThread - public LiveData<List<Board>> getBoards(long accountId) { - return dataBaseAdapter.getBoards(accountId); - } - - /** - * @param localProjectId LocalId of the OcsProject - * @return all {@link OcsProjectResource}s of the Project - */ - @AnyThread - public LiveData<List<OcsProjectResource>> getResourcesForProject(long localProjectId) { - return dataBaseAdapter.getResourcesByLocalProjectId(localProjectId); - } - - /** - * @param accountId ID of the account - * @param archived Decides whether only archived or not-archived boards for the specified account will be returned - * @return all archived or non-archived <code>Board</code>s depending on <code>archived</code> parameter - */ - @AnyThread - public LiveData<List<Board>> getBoards(long accountId, boolean archived) { - return dataBaseAdapter.getBoards(accountId, archived); - } - - /** - * @param accountId ID of the account - * @param archived Decides whether only archived or not-archived boards for the specified account will be returned - * @return all archived or non-archived <code>FullBoard</code>s depending on <code>archived</code> parameter - */ - @AnyThread - public LiveData<List<FullBoard>> getFullBoards(long accountId, boolean archived) { - return dataBaseAdapter.getFullBoards(accountId, archived); - } - - /** - * Get all non-archived <code>FullBoard</code>s with edit permissions for the specified account. - * - * @param accountId ID of the account - * @return all non-archived <code>Board</code>s with edit permission - */ - @AnyThread - public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) { - return dataBaseAdapter.getBoardsWithEditPermission(accountId); - } - - @AnyThread - public LiveData<Boolean> hasArchivedBoards(long accountId) { - return dataBaseAdapter.hasArchivedBoards(accountId); - } - @AnyThread - public void createBoard(long accountId, @NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { + public void createBoard(@NonNull Account account, @NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { executor.submit(() -> { - final Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); - final User owner = dataBaseAdapter.getUserByUidDirectly(accountId, account.getUserName()); + final User owner = dataBaseAdapter.getUserByUidDirectly(account.getId(), account.getUserName()); if (owner == null) { callback.onError(new IllegalStateException("Owner is null. This can be the case if the Deck app has never before been opened in the webinterface")); } else { @@ -554,8 +354,8 @@ public class SyncManager { board.setOwnerId(owner.getLocalId()); fullBoard.setOwner(owner); fullBoard.setBoard(board); - board.setAccountId(accountId); - fullBoard.setAccountId(accountId); + board.setAccountId(account.getId()); + fullBoard.setAccountId(account.getId()); new DataPropagationHelper(serverAdapter, dataBaseAdapter, executor).createEntity(new BoardDataProvider(), fullBoard, ResponseCallback.from(account, callback)); } }); @@ -675,11 +475,18 @@ public class SyncManager { } } } - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId); - ServerAdapter serverAdapterToUse = this.serverAdapter; - if (originAccountId != targetAccountId) { - serverAdapterToUse = new ServerAdapter(appContext, targetAccount.getName()); + final ServerAdapter serverAdapterToUse; + if (originAccountId == targetAccountId) { + serverAdapterToUse = this.serverAdapter; + } else { + try { + serverAdapterToUse = new ServerAdapter(context, AccountImporter.getSingleSignOnAccount(context, targetAccount.getName()), connectivityUtil); + } catch (NextcloudFilesAppAccountNotFoundException e) { + callback.onError(e); + return; + } } syncHelperFactory.create(serverAdapterToUse, dataBaseAdapter, null) .setResponseCallback(new ResponseCallback<>(targetAccount) { @@ -703,7 +510,7 @@ public class SyncManager { @AnyThread public LiveData<List<it.niedermann.nextcloud.deck.model.ocs.Activity>> syncActivitiesForCard(@NonNull Card card) { executor.submit(() -> { - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { if (card.getId() != null) { syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) .setResponseCallback(new ResponseCallback<>(dataBaseAdapter.getAccountByIdDirectly(card.getAccountId())) { @@ -764,10 +571,6 @@ public class SyncManager { }); } - public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) { - return dataBaseAdapter.getFullCommentsForLocalCardId(localCardId); - } - @AnyThread public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback<Void> callback) { executor.submit(() -> { @@ -787,19 +590,6 @@ public class SyncManager { }); } - public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { - return dataBaseAdapter.getStacksForBoard(accountId, localBoardId); - } - - public LiveData<FullStack> getStack(long accountId, long localStackId) { - return dataBaseAdapter.getStack(accountId, localStackId); - } - - @WorkerThread - public Long getBoardLocalIdByLocalCardIdDirectly(long localCardId) { - return dataBaseAdapter.getBoardLocalIdByLocalCardIdDirectly(localCardId); - } - @AnyThread public void createAccessControl(long accountId, @NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) { executor.submit(() -> { @@ -814,15 +604,6 @@ public class SyncManager { }); } - @WorkerThread - public AccessControl getAccessControlByRemoteIdDirectly(long accountId, Long id) { - return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, id); - } - - public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) { - return dataBaseAdapter.getAccessControlByLocalBoardId(accountId, id); - } - @AnyThread public void updateAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) { executor.submit(() -> { @@ -844,6 +625,8 @@ public class SyncManager { public void onResponse(Void response) { // revoked own board-access? if (entity.getAccountId() == entity.getAccountId() && entity.getUser().getUid().equals(account.getUserName())) { + dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId()); + dataBaseAdapter.removeCurrentStackId(board.getAccountId(), board.getLocalId()); dataBaseAdapter.deleteBoardPhysically(board.getBoard()); } callback.onResponse(response); @@ -858,17 +641,10 @@ public class SyncManager { }); } - public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { - return dataBaseAdapter.getFullBoardById(accountId, localId); - } - - public Board getBoardById(Long localId) { - return dataBaseAdapter.getBoardByLocalIdDirectly(localId); - } - @AnyThread - public void createStack(long accountId, @NonNull String title, long boardLocalId, @NonNull IResponseCallback<FullStack> callback) { + public void createStack(long accountId, long boardLocalId, @NonNull String title, @NonNull IResponseCallback<FullStack> callback) { executor.submit(() -> { + DeckLog.info("Create Stack in account", accountId, "on board with local ID ", boardLocalId); Stack stack = new Stack(title, boardLocalId); Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId()); @@ -883,7 +659,7 @@ public class SyncManager { } @AnyThread - public void deleteStack(long accountId, long stackLocalId, long boardLocalId, @NonNull IResponseCallback<Void> callback) { + public void deleteStack(long accountId, long boardLocalId, long stackLocalId, @NonNull IResponseCallback<Void> callback) { executor.submit(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullStack fullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackLocalId); @@ -956,22 +732,6 @@ public class SyncManager { }); } - public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { - return dataBaseAdapter.getCardWithProjectsByLocalId(accountId, cardLocalId); - } - - public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { - return dataBaseAdapter.getFullCardsForStack(accountId, localStackId, filter); - } - - public void countCardsInStackDirectly(long accountId, long localStackId, @NonNull IResponseCallback<Integer> callback) { - executor.submit(() -> dataBaseAdapter.countCardsInStackDirectly(accountId, localStackId, callback)); - } - - public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback<Integer> callback) { - executor.submit(() -> dataBaseAdapter.countCardsWithLabel(localLabelId, callback)); - } - // TODO implement, see https://github.com/stefan-niedermann/nextcloud-deck/issues/395 public LiveData<List<FullCard>> getArchivedFullCardsForBoard(long accountId, long localBoardId) { MutableLiveData<List<FullCard>> dummyData = new MutableLiveData<>(); @@ -1043,7 +803,7 @@ public class SyncManager { } } - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) .setResponseCallback(new ResponseCallback<>(account) { @Override @@ -1153,9 +913,21 @@ public class SyncManager { public void archiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { executor.submit(() -> { try { - FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); - b.getBoard().setArchived(true); - updateBoard(b, callback); + final var fullBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); + fullBoard.getBoard().setArchived(true); + updateBoard(fullBoard, new IResponseCallback<>() { + @Override + public void onResponse(FullBoard response) { + dataBaseAdapter.saveNeighbourOfBoard(fullBoard.getAccountId(), fullBoard.getLocalId()); + callback.onResponse(response); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + callback.onError(throwable); + } + }); } catch (Throwable e) { callback.onError(e); } @@ -1168,7 +940,19 @@ public class SyncManager { try { FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); b.getBoard().setArchived(false); - updateBoard(b, callback); + updateBoard(b, new IResponseCallback<>() { + @Override + public void onResponse(FullBoard response) { + dataBaseAdapter.saveCurrentBoardId(b.getAccountId(), b.getLocalId()); + callback.onResponse(response); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + callback.onError(throwable); + } + }); } catch (Throwable e) { callback.onError(e); } @@ -1207,7 +991,7 @@ public class SyncManager { fullCardFromDB.setCard(card.getCard()); card.getCard().setStatus(DBStatus.LOCAL_EDITED.getId()); dataBaseAdapter.updateCard(card.getCard(), false); - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) .setResponseCallback(new ResponseCallback<>(account) { @@ -1286,7 +1070,12 @@ public class SyncManager { ServerAdapter serverToUse = serverAdapter; if (originAccountId != targetAccountId) { - serverToUse = new ServerAdapter(appContext, targetAccount.getName()); + try { + serverToUse = new ServerAdapter(context, AccountImporter.getSingleSignOnAccount(context, targetAccount.getName()), connectivityUtil); + } catch (NextcloudFilesAppAccountNotFoundException e) { + callback.onError(e); + throw new RuntimeException(e); + } } new DataPropagationHelper(serverToUse, dataBaseAdapter, executor).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new ResponseCallback<>(targetAccount) { @Override @@ -1394,10 +1183,6 @@ public class SyncManager { }); } - public MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId) { - return createAndAssignLabelToCard(accountId, label, localCardId, serverAdapter); - } - @AnyThread private MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId, ServerAdapter serverAdapterToUse) { MutableLiveData<Label> liveData = new MutableLiveData<>(); @@ -1455,7 +1240,7 @@ public class SyncManager { Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { serverAdapter.assignUserToCard(board.getId(), stack.getId(), card.getId(), user.getUid(), new ResponseCallback<>(account) { @Override @@ -1484,7 +1269,7 @@ public class SyncManager { Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); - if (serverAdapterToUse.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { serverAdapterToUse.assignLabelToCard(board.getId(), stack.getId(), card.getId(), label.getId(), new ResponseCallback<>(account) { @Override @@ -1503,7 +1288,7 @@ public class SyncManager { Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { serverAdapter.unassignLabelFromCard(board.getId(), stack.getId(), card.getId(), label.getId(), new ResponseCallback<>(account) { @Override public void onResponse(Void response) { @@ -1518,7 +1303,7 @@ public class SyncManager { public void unassignUserFromCard(@NonNull User user, @NonNull Card card) { executor.submit(() -> { dataBaseAdapter.deleteJoinedUserForCard(card.getLocalId(), user.getLocalId()); - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); @@ -1532,79 +1317,31 @@ public class SyncManager { }); } - public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId, long notAssignedToLocalCardId, final int topX) { - return dataBaseAdapter.findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX); - } - - public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId) { - return dataBaseAdapter.findProposalsForUsersToAssign(accountId, boardId, -1L, -1); - } - - public LiveData<List<User>> findProposalsForUsersToAssignForACL(final long accountId, long boardId, final int topX) { - return dataBaseAdapter.findProposalsForUsersToAssignForACL(accountId, boardId, topX); - } - - public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId, long notAssignedToLocalCardId) { - return dataBaseAdapter.findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId); - } - - public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) { - return findProposalsForLabelsToAssign(accountId, boardId, -1L); - } - - public LiveData<User> getUserByLocalId(long accountId, long localId) { - return dataBaseAdapter.getUserByLocalId(accountId, localId); - } - - public LiveData<User> getUserByUid(long accountId, String uid) { - return dataBaseAdapter.getUserByUid(accountId, uid); - } - - @WorkerThread - public User getUserByUidDirectly(long accountId, String uid) { - return dataBaseAdapter.getUserByUidDirectly(accountId, uid); - } - - public LiveData<List<User>> searchUserByUidOrDisplayName(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, final String searchTerm) { - return dataBaseAdapter.searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, searchTerm); - } - - public UserSearchLiveData searchUserByUidOrDisplayNameForACL() { - return new UserSearchLiveData(dataBaseAdapter, serverAdapter); - } - - public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { - return dataBaseAdapter.getBoardByRemoteId(accountId, remoteId); - } - - @WorkerThread - public Board getBoardByRemoteIdDirectly(long accountId, long remoteId) { - return dataBaseAdapter.getBoardByRemoteIdDirectly(accountId, remoteId); - } - - public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) { - return dataBaseAdapter.getStackByRemoteId(accountId, localBoardId, remoteId); - } - - public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) { - return dataBaseAdapter.getCardByRemoteID(accountId, remoteId); - } - - @WorkerThread - public Optional<Card> getCardByRemoteIDDirectly(long accountId, long remoteId) { - return Optional.ofNullable(dataBaseAdapter.getCardByRemoteIDDirectly(accountId, remoteId)); - } - - public long createUser(long accountId, User user) { - return dataBaseAdapter.createUser(accountId, user); - } - - public void updateUser(long accountId, @NonNull User user) { - dataBaseAdapter.updateUser(accountId, user, true); - } + public void triggerUserSearch(@NonNull Account account, @NonNull String constraint) { + executor.submit(() -> serverAdapter.searchUser(constraint, new ResponseCallback<>(account) { + @Override + public void onResponse(OcsUserList response) { + if (response == null || response.getUsers().isEmpty()) { + return; + } + for (var user : response.getUsers()) { + final var existingUser = dataBaseAdapter.getUserByUidDirectly(account.getId(), user.getId()); + if (existingUser == null) { + User newUser = new User(); + newUser.setStatus(DBStatus.UP_TO_DATE.getId()); + newUser.setPrimaryKey(user.getId()); + newUser.setUid(user.getId()); + newUser.setDisplayname(user.getDisplayName()); + dataBaseAdapter.createUser(account.getId(), newUser); + } + } + } - public LiveData<List<Label>> searchNotYetAssignedLabelsByTitle(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, @NonNull String searchTerm) { - return dataBaseAdapter.searchNotYetAssignedLabelsByTitle(accountId, boardId, notYetAssignedToLocalCardId, searchTerm); + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + } + })); } /** @@ -1649,7 +1386,7 @@ public class SyncManager { } } -// if (serverAdapter.hasInternetConnection()){ +// if (connectivityUtil.hasInternetConnection()){ // // call reorder // Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(movedCard.getCard().getStackId()); // Stack newStack = newStackId == stack.getLocalId() ? stack : dataBaseAdapter.getStackByLocalIdDirectly(newStackId); @@ -1680,7 +1417,7 @@ public class SyncManager { reorderLocally(cardsOfNewStack, movedCard, newStackId, newOrder); } //FIXME: remove the sync-block, when commentblock up there is activated. (waiting for deck server bugfix) - if (hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(movedCard.getCard().getStackId()); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(movedCard.getCard().getAccountId()); @@ -1696,82 +1433,6 @@ public class SyncManager { } - private void reorderLocally(List<FullCard> cardsOfNewStack, @NonNull FullCard movedCard, long newStackId, int newOrder) { - // set new stack and order - Card movedInnerCard = movedCard.getCard(); - int oldOrder = movedInnerCard.getOrder(); - long oldStackId = movedInnerCard.getStackId(); - - - List<Card> changedCards = new ArrayList<>(); - - int startingAtOrder = newOrder; - if (oldStackId == newStackId) { - // card was only reordered in the same stack - movedInnerCard.setStatusEnum(movedInnerCard.getStatus() == DBStatus.LOCAL_MOVED.getId() ? DBStatus.LOCAL_MOVED : DBStatus.LOCAL_EDITED); - // move direction? - if (oldOrder > newOrder) { - // up - changedCards.add(movedCard.getCard()); - for (FullCard cardToUpdate : cardsOfNewStack) { - Card cardEntity = cardToUpdate.getCard(); - if (cardEntity.getOrder() < newOrder) { - continue; - } - if (cardEntity.getOrder() >= oldOrder) { - break; - } - changedCards.add(cardEntity); - } - } else { - // down - startingAtOrder = oldOrder; - for (FullCard cardToUpdate : cardsOfNewStack) { - Card cardEntity = cardToUpdate.getCard(); - if (cardEntity.getOrder() <= oldOrder) { - continue; - } - if (cardEntity.getOrder() > newOrder) { - break; - } - changedCards.add(cardEntity); - } - changedCards.add(movedCard.getCard()); - } - } else { - // card was moved to an other stack - movedInnerCard.setStackId(newStackId); - movedInnerCard.setStatusEnum(DBStatus.LOCAL_MOVED); - changedCards.add(movedCard.getCard()); - for (FullCard fullCard : cardsOfNewStack) { - // skip unchanged cards - if (fullCard.getCard().getOrder() < newOrder) { - continue; - } - changedCards.add(fullCard.getCard()); - } - } - reorderAscending(movedInnerCard, changedCards, startingAtOrder); - } - - private void reorderAscending(@NonNull Card movedCard, @NonNull List<Card> cardsToReorganize, int startingAtOrder) { - final Instant now = Instant.now(); - for (Card card : cardsToReorganize) { - card.setOrder(startingAtOrder); - if (card.getStatus() == DBStatus.UP_TO_DATE.getId()) { - card.setStatusEnum(DBStatus.LOCAL_EDITED_SILENT); - card.setLastModifiedLocal(now); - } - startingAtOrder++; - } - //update the moved one first, because otherwise a bunch of livedata is fired, leading the card to dispose and reappear - cardsToReorganize.remove(movedCard); - dataBaseAdapter.updateCard(movedCard, false); - for (Card card : cardsToReorganize) { - dataBaseAdapter.updateCard(card, false); - } - } - /** * FIXME clean up on error * When uploading the exact same attachment 2 times to the same card, the server starts burning and gets mad and returns status 500 @@ -1797,12 +1458,12 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<Attachment> updateAttachmentForCard(long accountId, @NonNull Attachment existing, @NonNull String mimeType, @NonNull File file) { - WrappedLiveData<Attachment> liveData = new WrappedLiveData<>(); + public LiveData<Attachment> updateAttachmentForCard(long accountId, @NonNull Attachment existing, @NonNull String mimeType, @NonNull File file) { + final var liveData = new MutableLiveData<Attachment>(); executor.submit(() -> { Attachment attachment = populateAttachmentEntityForFile(existing, existing.getCardId(), mimeType, file); attachment.setLastModifiedLocal(Instant.now()); - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, existing.getCardId()); Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getCard().getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId()); @@ -1817,7 +1478,8 @@ public class SyncManager { @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { - liveData.postError(throwable); + DeckLog.error(throwable); +// liveData.postError(throwable); } }); } @@ -1840,7 +1502,7 @@ public class SyncManager { @AnyThread public void deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId, @NonNull IResponseCallback<Void> callback) { executor.submit(() -> { - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, localCardId); Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getCard().getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId()); @@ -1853,135 +1515,10 @@ public class SyncManager { }); } - // ------------------- - // Widgets - // ------------------- - - // # filter widget - - @AnyThread - public void createFilterWidget(@NonNull FilterWidget filterWidget, @NonNull IResponseCallback<Integer> callback) { - executor.submit(() -> { - try { - int filterWidgetId = dataBaseAdapter.createFilterWidgetDirectly(filterWidget); - callback.onResponse(filterWidgetId); - } catch (Throwable t) { - callback.onError(t); - } - }); - } - - @AnyThread - public void updateFilterWidget(@NonNull FilterWidget filterWidget, @NonNull ResponseCallback<Boolean> callback) { - executor.submit(() -> { - try { - dataBaseAdapter.updateFilterWidgetDirectly(filterWidget); - callback.onResponse(Boolean.TRUE); - } catch (Throwable t) { - callback.onError(t); - } - }); - } - - @AnyThread - public void getFilterWidget(@NonNull Integer filterWidgetId, @NonNull IResponseCallback<FilterWidget> callback) { - executor.submit(() -> { - try { - callback.onResponse(dataBaseAdapter.getFilterWidgetByIdDirectly(filterWidgetId)); - } catch (Throwable t) { - callback.onError(t); - } - }); - } - - @AnyThread - public void deleteFilterWidget(int filterWidgetId, @NonNull IResponseCallback<Boolean> callback) { - executor.submit(() -> { - 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); - } - - @WorkerThread - public LiveData<List<UpcomingCardsAdapterItem>> getCardsForUpcomingCards() { - return dataBaseAdapter.getCardsForUpcomingCard(); - } - - @WorkerThread - public List<UpcomingCardsAdapterItem> getCardsForUpcomingCardsForWidget() { - return dataBaseAdapter.getCardsForUpcomingCardForWidget(); - } - - // # 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. + * FIXME <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/640">GitHub Issue #640</a> */ - @AnyThread - public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) { - executor.submit(() -> dataBaseAdapter.createSingleCardWidget(widgetId, accountId, boardId, localCardId)); - } - - @WorkerThread - public FullSingleCardWidgetModel getSingleCardWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { - final FullSingleCardWidgetModel model = dataBaseAdapter.getFullSingleCardWidgetModel(appWidgetId); - if (model == null) { - throw new NoSuchElementException("There is no " + FullSingleCardWidgetModel.class.getSimpleName() + " with the given appWidgetId " + appWidgetId); - } - return model; - } - - @AnyThread - public void deleteSingleCardWidgetModel(int widgetId) { - executor.submit(() -> dataBaseAdapter.deleteSingleCardWidget(widgetId)); - } - - public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { - executor.submit(() -> dataBaseAdapter.createStackWidget(appWidgetId, accountId, stackId, darkTheme)); - } - - @WorkerThread - public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { - final StackWidgetModel model = dataBaseAdapter.getStackWidgetModelDirectly(appWidgetId); - if (model == null) { - throw new NoSuchElementException(); - } - return model; - } - - public void deleteStackWidgetModel(int appWidgetId) { - executor.submit(() -> dataBaseAdapter.deleteStackWidget(appWidgetId)); - } - - /** - * FIXME https://github.com/stefan-niedermann/nextcloud-deck/issues/640 - */ - public static boolean ignoreExceptionOnVoidError(Throwable t) { - return t instanceof NullPointerException && "Attempt to invoke interface method 'void io.reactivex.disposables.Disposable.dispose()' on a null object reference".equals(t.getMessage()); - } - - @WorkerThread - public Stack getStackDirectly(long stackLocalId) { - return dataBaseAdapter.getStackByLocalIdDirectly(stackLocalId); - } - - @ColorInt - @WorkerThread - public Integer getBoardColorDirectly(long accountId, long localBoardId) { - return dataBaseAdapter.getBoardColorDirectly(accountId, localBoardId); + public static boolean isNoOnVoidError(Throwable t) { + return !(t instanceof NullPointerException) || !"Attempt to invoke interface method 'void io.reactivex.disposables.Disposable.dispose()' on a null object reference".equals(t.getMessage()); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java index af6e08826..a454dde1d 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 @@ -4,19 +4,29 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; import androidx.preference.PreferenceManager; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.ListenableWorker; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; 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.persistence.BaseRepository; public class SyncWorker extends Worker { @@ -25,25 +35,62 @@ public class SyncWorker extends Worker { .setRequiredNetworkType(NetworkType.CONNECTED) .build(); + private final BaseRepository baseRepository; + private final SharedPreferences.Editor editor; + public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); + this.baseRepository = new BaseRepository(context); + this.editor = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit(); } @NonNull @Override public Result doWork() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit(); - SyncManager syncManager = new SyncManager(getApplicationContext(), null); - if (syncManager.hasInternetConnection()) { - DeckLog.info("Starting background synchronization"); - sharedPreferencesEditor.putLong(getApplicationContext().getString(R.string.shared_preference_last_background_sync), System.currentTimeMillis()); - sharedPreferencesEditor.apply(); - boolean success = syncManager.synchronizeEverything(); - DeckLog.info("Finishing background synchronization. Success: ", success); - return success ? Result.failure() : Result.success(); + DeckLog.info("Starting background synchronization"); + editor.putLong(getApplicationContext().getString(R.string.shared_preference_last_background_sync), System.currentTimeMillis()); + editor.apply(); + + try { + return synchronizeEverything(getApplicationContext(), baseRepository.readAccountsDirectly()); + } catch (NextcloudFilesAppAccountNotFoundException e) { + return Result.failure(); + } finally { + DeckLog.info("Finishing background synchronization."); + } + } + + @WorkerThread + private ListenableWorker.Result synchronizeEverything(@NonNull Context context, @NonNull List<Account> accounts) throws NextcloudFilesAppAccountNotFoundException { + if (accounts.isEmpty()) { + return Result.success(); + } + final var success = new AtomicBoolean(true); + final var latch = new CountDownLatch(accounts.size()); + + try { + for (Account account : accounts) { + new SyncManager(context, account).synchronize(new ResponseCallback<>(account) { + @Override + public void onResponse(Boolean response) { + success.set(success.get() && Boolean.TRUE.equals(response)); + latch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + success.set(false); + super.onError(throwable); + latch.countDown(); + } + }); + } + latch.await(); + return success.get() ? Result.success() : Result.failure(); + } catch (InterruptedException e) { + DeckLog.logError(e); + return Result.failure(); } - return Result.success(); } public static void update(@NonNull Context context) { 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 4406ce78e..e5cdbf192 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 @@ -4,16 +4,14 @@ import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN; import android.content.Context; import android.content.SharedPreferences; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.net.Uri; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import com.nextcloud.android.sso.api.ParsedResponse; +import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.File; import java.util.List; @@ -44,6 +42,7 @@ import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; import it.niedermann.nextcloud.deck.model.propagation.CardUpdate; import it.niedermann.nextcloud.deck.model.propagation.Reorder; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; @@ -51,43 +50,36 @@ import okhttp3.ResponseBody; public class ServerAdapter { - private final String prefKeyWifiOnly; + private final ConnectivityUtil connectivityUtil; private final String prefKeyEtags; - final SharedPreferences sharedPreferences; - - @NonNull - private final Context applicationContext; + private final SharedPreferences sharedPreferences; private final ApiProvider provider; - 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 ServerAdapter(@NonNull Context context, + @NonNull SingleSignOnAccount ssoAccount, + @NonNull ConnectivityUtil connectivityUtil) { + this(context, new ApiProvider(context, ssoAccount), connectivityUtil); } - public void ensureInternetConnection() { - final boolean isConnected = hasInternetConnection(); - if (!isConnected) { - throw new OfflineException(); - } + public ServerAdapter(@NonNull Context context, + @NonNull ApiProvider apiProvider, + @NonNull ConnectivityUtil connectivityUtil) { + this.prefKeyEtags = context.getResources().getString(R.string.pref_key_etags); + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.connectivityUtil = connectivityUtil; + this.provider = apiProvider; } + @Deprecated() public boolean hasInternetConnection() { - ConnectivityManager cm = (ConnectivityManager) applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE); - if (cm != null) { - if (sharedPreferences.getBoolean(prefKeyWifiOnly, false)) { - NetworkInfo networkInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - if (networkInfo == null) { - return false; - } - return networkInfo.isConnected(); - } else { - return cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnected(); - } + return connectivityUtil.hasInternetConnection(); + } + + public void ensureInternetConnection() { + final boolean isConnected = connectivityUtil.hasInternetConnection(); + if (!isConnected) { + throw new OfflineException(); } - return false; } // TODO what is this? 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 5c438c421..284abf7d1 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,11 +1,16 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db; -import static androidx.lifecycle.Transformations.distinctUntilChanged; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.concurrent.CompletableFuture.supplyAsync; +import static java.util.stream.Collectors.toList; import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.database.sqlite.SQLiteConstraintException; +import android.text.TextUtils; import androidx.annotation.AnyThread; import androidx.annotation.ColorInt; @@ -13,27 +18,34 @@ import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; +import androidx.core.content.ContextCompat; import androidx.lifecycle.LiveData; +import androidx.preference.PreferenceManager; import androidx.sqlite.db.SimpleSQLiteQuery; -import org.jetbrains.annotations.NotNull; +import com.nextcloud.android.sso.helper.SingleAccountHelper; 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.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.stream.Collectors; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.android.sharedpreferences.SharedPreferenceLongLiveData; 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.AccessControl; import it.niedermann.nextcloud.deck.model.Account; @@ -77,27 +89,40 @@ 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.ui.upcomingcards.UpcomingCardsAdapterItem; import it.niedermann.nextcloud.deck.ui.widget.singlecard.SingleCardWidget; public class DataBaseAdapter { - @NonNull private final DeckDatabase db; @NonNull private final Context context; @NonNull private final ExecutorService widgetNotifierExecutor; + @NonNull + private final ExecutorService executor; + private static final Long NOT_AVAILABLE = -1L; + private final SharedPreferences sharedPreferences; + private final SharedPreferences.Editor sharedPreferencesEditor; + @ColorInt + private final int defaultColor; public DataBaseAdapter(@NonNull Context appContext) { - this(appContext, DeckDatabase.getInstance(appContext), Executors.newCachedThreadPool()); + this(appContext, DeckDatabase.getInstance(appContext), Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); } - private DataBaseAdapter(@NonNull Context applicationContext, @NonNull DeckDatabase db, @NonNull ExecutorService widgetNotifierExecutor) { + @VisibleForTesting + protected DataBaseAdapter(@NonNull Context applicationContext, + @NonNull DeckDatabase db, + @NonNull ExecutorService widgetNotifierExecutor, + @NonNull ExecutorService executor) { this.context = applicationContext; this.db = db; this.widgetNotifierExecutor = widgetNotifierExecutor; + this.executor = executor; + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.sharedPreferencesEditor = this.sharedPreferences.edit(); + this.defaultColor = ContextCompat.getColor(context, R.color.defaultBrand); } @NonNull @@ -118,11 +143,14 @@ public class DataBaseAdapter { } public LiveData<Boolean> hasAccounts() { - return LiveDataHelper.postCustomValue(db.getAccountDao().countAccounts(), data -> data != null && data > 0); + return new ReactiveLiveData<>(db.getAccountDao().countAccounts()) + .distinctUntilChanged() + .map(count -> count != null && count > 0); } public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { - return distinctUntilChanged(db.getBoardDao().getBoardByRemoteId(accountId, remoteId)); + return new ReactiveLiveData<>(db.getBoardDao().getBoardByRemoteId(accountId, remoteId)) + .distinctUntilChanged(); } @WorkerThread @@ -139,7 +167,8 @@ public class DataBaseAdapter { } public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) { - return distinctUntilChanged(db.getStackDao().getStackByRemoteId(accountId, localBoardId, remoteId)); + return new ReactiveLiveData<>(db.getStackDao().getStackByRemoteId(accountId, localBoardId, remoteId)) + .distinctUntilChanged(); } public Stack getStackByLocalIdDirectly(final long localStackId) { @@ -156,7 +185,8 @@ public class DataBaseAdapter { } public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) { - return distinctUntilChanged(db.getCardDao().getCardByRemoteId(accountId, remoteId)); + return new ReactiveLiveData<>(db.getCardDao().getCardByRemoteId(accountId, remoteId)) + .distinctUntilChanged(); } @WorkerThread @@ -225,16 +255,18 @@ public class DataBaseAdapter { return db.getCardDao().getCardByRemoteIdDirectly(accountId, remoteId); } - public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, FilterInformation filter) { - if (filter == null) { - return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardsForStack(accountId, localStackId), this::filterRelationsForCard); - } - return LiveDataHelper.interceptLiveData(db.getCardDao().getFilteredFullCardsForStack(getQueryForFilter(filter, accountId, localStackId)), this::filterRelationsForCard); + public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { + return new ReactiveLiveData<>( + filter == null + ? db.getCardDao().getFullCardsForStack(accountId, localStackId) + : db.getCardDao().getFilteredFullCardsForStack(getQueryForFilter(filter, accountId, localStackId))) + .tap(this::filterRelationsForCard, executor) + .distinctUntilChanged(); } private void fillSqlWithEntityListValues(StringBuilder query, Collection<Object> args, @NonNull List<? extends IRemoteEntity> entities) { - List<Long> idList = entities.stream().map(IRemoteEntity::getLocalId).collect(Collectors.toList()); + List<Long> idList = entities.stream().map(IRemoteEntity::getLocalId).collect(toList()); fillSqlWithListValues(query, args, idList); } @@ -257,19 +289,19 @@ public class DataBaseAdapter { @AnyThread private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, long accountId, long localStackId) { - return getQueryForFilter(filter, Collections.singletonList(accountId), Collections.singletonList(localStackId)); + return getQueryForFilter(filter, singletonList(accountId), singletonList(localStackId)); } @AnyThread - private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, List<Long> accountIds, List<Long> localStackIds) { + private SimpleSQLiteQuery getQueryForFilter(@NonNull FilterInformation filter, @NonNull List<Long> accountIds, @NonNull List<Long> localStackIds) { final Collection<Object> args = new ArrayList<>(); StringBuilder query = new StringBuilder("SELECT * FROM card c WHERE 1=1 "); - if (accountIds != null && !accountIds.isEmpty()) { + if (!accountIds.isEmpty()) { query.append("and accountId in ("); fillSqlWithListValues(query, args, accountIds); query.append(") "); } - if (localStackIds != null && !localStackIds.isEmpty()) { + if (!localStackIds.isEmpty()) { query.append("and stackId in ("); fillSqlWithListValues(query, args, localStackIds); query.append(") "); @@ -335,7 +367,7 @@ public class DataBaseAdapter { throw new IllegalArgumentException("You need to add your new EDueType value\"" + filter.getDueType() + "\" here!"); } } - if (filter.getFilterText() != null && !filter.getFilterText().isEmpty()) { + if (!TextUtils.isEmpty(filter.getFilterText())) { query.append(" and (c.description like ? or c.title like ?) "); String filterText = "%" + filter.getFilterText() + "%"; args.add(filterText); @@ -385,7 +417,8 @@ public class DataBaseAdapter { @UiThread public LiveData<Label> getLabelByRemoteId(long accountId, long remoteId) { - return distinctUntilChanged(db.getLabelDao().getLabelByRemoteId(accountId, remoteId)); + return new ReactiveLiveData<>(db.getLabelDao().getLabelByRemoteId(accountId, remoteId)) + .distinctUntilChanged(); } @WorkerThread @@ -529,7 +562,7 @@ public class DataBaseAdapter { final long id = db.getAccountDao().insert(account); widgetNotifierExecutor.submit(() -> { - DeckLog.verbose("Adding new created", Account.class.getSimpleName(), " with ", id, " to all instances of ", EWidgetType.UPCOMING_WIDGET.name()); + 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); @@ -551,29 +584,29 @@ public class DataBaseAdapter { @UiThread public LiveData<Account> readAccount(long id) { - return distinctUntilChanged(fillAccountsUserName(db.getAccountDao().getAccountById(id))); + return new ReactiveLiveData<>(db.getAccountDao().getAccountById(id)) + .tap(account -> account.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(account.getId(), account.getUserName())), executor) + .distinctUntilChanged(); } @UiThread public LiveData<Account> readAccount(String name) { - return distinctUntilChanged(fillAccountsUserName(db.getAccountDao().getAccountByName(name))); + return new ReactiveLiveData<>(db.getAccountDao().getAccountByName(name)) + .tap(account -> account.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(account.getId(), account.getUserName())), executor) + .distinctUntilChanged(); } @UiThread public LiveData<List<Account>> readAccounts() { - return distinctUntilChanged(fillAccountsListUserName(db.getAccountDao().getAllAccounts())); + return new ReactiveLiveData<>(db.getAccountDao().getAllAccounts()) + .tap(accounts -> accounts.forEach(account -> account.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(account.getId(), account.getUserName()))), executor) + .distinctUntilChanged(); } - private LiveData<Account> fillAccountsUserName(LiveData<Account> source) { - return LiveDataHelper.interceptLiveData(distinctUntilChanged(source), data -> data.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(data.getId(), data.getUserName()))); - } - - private LiveData<List<Account>> fillAccountsListUserName(LiveData<List<Account>> source) { - return LiveDataHelper.interceptLiveData(distinctUntilChanged(source), data -> { - for (Account a : data) { - a.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(a.getId(), a.getUserName())); - } - }); + public LiveData<Integer> getAccountColor(long accountId) { + return new ReactiveLiveData<>(db.getAccountDao().getAccountColor(accountId)) + .distinctUntilChanged() + .map(color -> color == null ? defaultColor : color); } @WorkerThread @@ -588,20 +621,14 @@ public class DataBaseAdapter { return account; } - - public LiveData<List<Board>> getBoards(long accountId) { - return distinctUntilChanged(db.getBoardDao().getBoardsForAccount(accountId)); - } - public LiveData<List<Board>> getBoards(long accountId, boolean archived) { - return distinctUntilChanged( - archived - ? db.getBoardDao().getArchivedBoardsForAccount(accountId) - : db.getBoardDao().getNonArchivedBoardsForAccount(accountId)); + return new ReactiveLiveData<>(db.getBoardDao().getNotDeletedBoards(accountId, archived ? 1 : 0)) + .distinctUntilChanged(); } public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) { - return distinctUntilChanged(db.getBoardDao().getBoardsWithEditPermissionsForAccount(accountId)); + return new ReactiveLiveData<>(db.getBoardDao().getBoardsWithEditPermissionsForAccount(accountId)) + .distinctUntilChanged(); } @WorkerThread @@ -612,14 +639,14 @@ public class DataBaseAdapter { return id; } - public void deleteBoard(Board board, boolean setStatus) { + public void deleteBoard(@NonNull Board board, boolean setStatus) { markAsDeletedIfNeeded(board, setStatus); db.getBoardDao().update(board); notifyAllWidgets(); notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.BOARD, board.getLocalId()); } - public void deleteBoardPhysically(Board board) { + public void deleteBoardPhysically(@NonNull Board board) { db.getBoardDao().delete(board); notifyAllWidgets(); } @@ -631,7 +658,8 @@ public class DataBaseAdapter { } public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { - return distinctUntilChanged(db.getStackDao().getStacksForBoard(accountId, localBoardId)); + return new ReactiveLiveData<>(db.getStackDao().getStacksForBoard(accountId, localBoardId)) + .distinctUntilChanged(); } @WorkerThread @@ -641,7 +669,8 @@ public class DataBaseAdapter { @MainThread public LiveData<FullStack> getStack(long accountId, long localStackId) { - return distinctUntilChanged(db.getStackDao().getFullStack(accountId, localStackId)); + return new ReactiveLiveData<>(db.getStackDao().getFullStack(accountId, localStackId)) + .distinctUntilChanged(); } @WorkerThread @@ -685,12 +714,16 @@ public class DataBaseAdapter { @AnyThread public LiveData<FullCard> getCardByLocalId(long accountId, long localCardId) { - return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardByLocalId(accountId, localCardId), this::filterRelationsForCard); + return new ReactiveLiveData<>(db.getCardDao().getFullCardByLocalId(accountId, localCardId)) + .tap(this::filterRelationsForCard, executor) + .distinctUntilChanged(); } @AnyThread public LiveData<FullCardWithProjects> getCardWithProjectsByLocalId(long accountId, long localCardId) { - return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardWithProjectsByLocalId(accountId, localCardId), this::filterRelationsForCard); + return new ReactiveLiveData<>(db.getCardDao().getFullCardWithProjectsByLocalId(accountId, localCardId)) + .tap(this::filterRelationsForCard, executor) + .distinctUntilChanged(); } @WorkerThread @@ -765,7 +798,9 @@ public class DataBaseAdapter { } public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long localBoardId) { - return LiveDataHelper.interceptLiveData(db.getAccessControlDao().getAccessControlByLocalBoardId(accountId, localBoardId), this::readRelationsForACL); + return new ReactiveLiveData<>(db.getAccessControlDao().getAccessControlByLocalBoardId(accountId, localBoardId)) + .tap(this::readRelationsForACL, executor) + .distinctUntilChanged(); } public List<AccessControl> getAccessControlByLocalBoardIdDirectly(long accountId, Long localBoardId) { @@ -789,7 +824,8 @@ public class DataBaseAdapter { } public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { - return distinctUntilChanged(db.getBoardDao().getFullBoardById(accountId, localId)); + return new ReactiveLiveData<>(db.getBoardDao().getFullBoardById(accountId, localId)) + .distinctUntilChanged(); } @WorkerThread @@ -798,42 +834,50 @@ public class DataBaseAdapter { } public LiveData<User> getUserByLocalId(long accountId, long localId) { - return db.getUserDao().getUserByLocalId(accountId, localId); + return new ReactiveLiveData<>(db.getUserDao().getUserByLocalId(accountId, localId)) + .distinctUntilChanged(); } public LiveData<User> getUserByUid(long accountId, String uid) { - return db.getUserDao().getUserByUid(accountId, uid); + return new ReactiveLiveData<>(db.getUserDao().getUserByUid(accountId, uid)) + .distinctUntilChanged(); } public LiveData<List<User>> getUsersForAccount(final long accountId) { - return db.getUserDao().getUsersForAccount(accountId); + return new ReactiveLiveData<>(db.getUserDao().getUsersForAccount(accountId)) + .distinctUntilChanged(); } public LiveData<List<User>> searchUserByUidOrDisplayName(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, final String searchTerm) { validateSearchTerm(searchTerm); - return db.getUserDao().searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%"); + return new ReactiveLiveData<>(db.getUserDao().searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%")) + .distinctUntilChanged(); } - public List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long notYetAssignedToACL, final String searchTerm) { + public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedToACL, final String searchTerm) { validateSearchTerm(searchTerm); - return db.getUserDao().searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%"); + return db.getUserDao().searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%"); } public LiveData<List<Label>> searchNotYetAssignedLabelsByTitle(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, String searchTerm) { validateSearchTerm(searchTerm); - return db.getLabelDao().searchNotYetAssignedLabelsByTitle(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%"); + return new ReactiveLiveData<>(db.getLabelDao().searchNotYetAssignedLabelsByTitle(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%")) + .distinctUntilChanged(); } public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId, long notAssignedToLocalCardId, final int topX) { - return db.getUserDao().findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX); + return new ReactiveLiveData<>(db.getUserDao().findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX)) + .distinctUntilChanged(); } public LiveData<List<User>> findProposalsForUsersToAssignForACL(final long accountId, long boardId, final int topX) { - return db.getUserDao().findProposalsForUsersToAssignForACL(accountId, boardId, topX); + return new ReactiveLiveData<>(db.getUserDao().findProposalsForUsersToAssignForACL(accountId, boardId, topX)) + .distinctUntilChanged(); } public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId, long notAssignedToLocalCardId) { - return db.getLabelDao().findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId); + return new ReactiveLiveData<>(db.getLabelDao().findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId)) + .distinctUntilChanged(); } @WorkerThread @@ -923,7 +967,8 @@ public class DataBaseAdapter { } public LiveData<Label> getLabelByLocalId(long localLabelId) { - return db.getLabelDao().getLabelByLocalId(localLabelId); + return new ReactiveLiveData<>(db.getLabelDao().getLabelByLocalId(localLabelId)) + .distinctUntilChanged(); } public List<FullBoard> getLocallyChangedBoards(long accountId) { @@ -1002,7 +1047,8 @@ public class DataBaseAdapter { } public LiveData<List<Activity>> getActivitiesForCard(Long localCardId) { - return db.getActivityDao().getActivitiesForCard(localCardId); + return new ReactiveLiveData<>(db.getActivityDao().getActivitiesForCard(localCardId)) + .distinctUntilChanged(); } public long createActivity(long accountId, Activity activity) { @@ -1033,22 +1079,22 @@ public class DataBaseAdapter { } public LiveData<List<DeckComment>> getCommentsForLocalCardId(long localCardId) { - return LiveDataHelper.interceptLiveData(db.getCommentDao().getCommentByLocalCardId(localCardId), (list) -> { - for (DeckComment deckComment : list) { - deckComment.setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getLocalId())); - } - }); + return new ReactiveLiveData<>(db.getCommentDao().getCommentByLocalCardId(localCardId)) + .tap(list -> list.forEach(comment -> comment.setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(comment.getLocalId()))), executor) + .distinctUntilChanged(); } public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) { - return LiveDataHelper.interceptLiveData(db.getCommentDao().getFullCommentByLocalCardId(localCardId), (list) -> { - for (FullDeckComment deckComment : list) { - deckComment.getComment().setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getLocalId())); - if (deckComment.getParent() != null) { - deckComment.getParent().setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getComment().getParentId())); - } - } - }); + return new ReactiveLiveData<>(db.getCommentDao().getFullCommentByLocalCardId(localCardId)) + .tap(list -> { + for (FullDeckComment deckComment : list) { + deckComment.getComment().setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getLocalId())); + if (deckComment.getParent() != null) { + deckComment.getParent().setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getComment().getParentId())); + } + } + }, executor) + .distinctUntilChanged(); } @WorkerThread @@ -1114,7 +1160,8 @@ public class DataBaseAdapter { } public LiveData<Long> getLocalBoardIdByCardRemoteIdAndAccountId(long cardRemoteId, long accountId) { - return db.getBoardDao().getLocalBoardIdByCardRemoteIdAndAccountId(cardRemoteId, accountId); + return new ReactiveLiveData<>(db.getBoardDao().getLocalBoardIdByCardRemoteIdAndAccountId(cardRemoteId, accountId)) + .distinctUntilChanged(); } @WorkerThread @@ -1138,11 +1185,14 @@ public class DataBaseAdapter { } public LiveData<List<FullBoard>> getFullBoards(long accountId, boolean archived) { - return db.getBoardDao().getArchivedFullBoards(accountId, (archived ? 1 : 0)); + return new ReactiveLiveData<>(db.getBoardDao().getNotDeletedFullBoards(accountId, archived ? 1 : 0)) + .distinctUntilChanged(); } public LiveData<Boolean> hasArchivedBoards(long accountId) { - return LiveDataHelper.postCustomValue(distinctUntilChanged(db.getBoardDao().countArchivedBoards(accountId)), data -> data != null && data > 0); + return new ReactiveLiveData<>(db.getBoardDao().countArchivedBoards(accountId)) + .distinctUntilChanged() + .map(hasArchivedBoards -> hasArchivedBoards != null && hasArchivedBoards > 0); } @WorkerThread @@ -1269,14 +1319,16 @@ public class DataBaseAdapter { } public LiveData<List<UpcomingCardsAdapterItem>> getCardsForUpcomingCard() { - return LiveDataHelper.postCustomValue(db.getCardDao().getUpcomingCards(), this::cardResultsToUpcomingCardsAdapterItems); + return new ReactiveLiveData<>(db.getCardDao().getUpcomingCards()) + .map(this::cardResultsToUpcomingCardsAdapterItems, executor) + .distinctUntilChanged(); } public List<UpcomingCardsAdapterItem> getCardsForUpcomingCardForWidget() { return cardResultsToUpcomingCardsAdapterItems(db.getCardDao().getUpcomingCardsDirectly()); } - @NotNull + @NonNull private List<UpcomingCardsAdapterItem> cardResultsToUpcomingCardsAdapterItems(List<FullCard> cardsResult) { filterRelationsForCard(cardsResult); final List<UpcomingCardsAdapterItem> result = new ArrayList<>(cardsResult.size()); @@ -1302,7 +1354,7 @@ public class DataBaseAdapter { } else filter.setDueType(EDueType.NO_FILTER); if (filterWidget.getAccounts().isEmpty()) { - cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, null, null))); + cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, emptyList(), emptyList()))); } else { for (FilterWidgetAccount account : filterWidget.getAccounts()) { filter.setNoAssignedUser(account.isIncludeNoUser()); @@ -1328,24 +1380,21 @@ public class DataBaseAdapter { if (!account.getBoards().isEmpty()) { for (FilterWidgetBoard board : account.getBoards()) { filter.setNoAssignedLabel(board.isIncludeNoLabel()); - final List<Long> stacks; + final List<Long> stacks = new ArrayList<>(); 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()); + stacks.addAll(db.getStackDao().getLocalStackIdsByLocalBoardIdDirectly(board.getBoardId())); } else { - stacks = new ArrayList<>(); - for (FilterWidgetStack stack : board.getStacks()) { - stacks.add(stack.getStackId()); - } + stacks.addAll(board.getStacks().stream().map(FilterWidgetStack::getStackId).collect(toList())); } - cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, Collections.singletonList(account.getAccountId()), stacks))); + cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, singletonList(account.getAccountId()), stacks))); } } else { - cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, Collections.singletonList(account.getAccountId()), null))); + cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, singletonList(account.getAccountId()), emptyList()))); } } } @@ -1378,21 +1427,17 @@ public class DataBaseAdapter { 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); - } - } + cardsResult.removeIf(fullCard -> fullCard.getCard().getDueDate() == null && !db.getStackDao().isStackOnSharedBoardDirectly(fullCard.getCard().getStackId())); List<Long> accountIds = null; if (!filterWidget.getAccounts().isEmpty()) { - accountIds = filterWidget.getAccounts().stream().map(FilterWidgetAccount::getAccountId).collect(Collectors.toList()); + accountIds = filterWidget.getAccounts().stream().map(FilterWidgetAccount::getAccountId).collect(toList()); } // https://github.com/stefan-niedermann/nextcloud-deck/issues/822 exclude archived cards and boards final List<Long> archivedStacks = db.getStackDao().getLocalStackIdsInArchivedBoardsByAccountIdsDirectly(accountIds); for (Long archivedStack : archivedStacks) { final List<FullCard> archivedCards = cardsResult.stream() .filter(c -> c.getCard().isArchived() || archivedStack.equals(c.getCard().getStackId())) - .collect(Collectors.toList()); + .collect(toList()); cardsResult.removeAll(archivedCards); } // https://github.com/stefan-niedermann/nextcloud-deck/issues/800 all cards within non-shared boards need to be included @@ -1420,7 +1465,8 @@ public class DataBaseAdapter { } public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) { - return db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%" + host + "%", boardRemoteId); + return new ReactiveLiveData<>(db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%" + host + "%", boardRemoteId)) + .distinctUntilChanged(); } public List<Account> readAccountsForHostWithReadAccessToBoardDirectly(String host, long boardRemoteId) { @@ -1464,14 +1510,16 @@ public class DataBaseAdapter { } public LiveData<Integer> countProjectResourcesInProject(Long projectLocalId) { - return db.getOcsProjectResourceDao().countProjectResourcesInProject(projectLocalId); + return new ReactiveLiveData<>(db.getOcsProjectResourceDao().countProjectResourcesInProject(projectLocalId)) + .distinctUntilChanged(); } public LiveData<List<OcsProjectResource>> getResourcesByLocalProjectId(Long projectLocalId) { - return db.getOcsProjectResourceDao().getResourcesByLocalProjectId(projectLocalId); + return new ReactiveLiveData<>(db.getOcsProjectResourceDao().getResourcesByLocalProjectId(projectLocalId)) + .distinctUntilChanged(); } - public void assignCardToProjectIfMissng(Long accountId, Long localProjectId, Long remoteCardId) { + public void assignCardToProjectIfMissing(Long accountId, Long localProjectId, Long remoteCardId) { final Card card = db.getCardDao().getCardByRemoteIdDirectly(accountId, remoteCardId); if (card != null) { final JoinCardWithProject existing = db.getJoinCardWithOcsProjectDao().getAssignmentByCardIdAndProjectIdDirectly(card.getLocalId(), localProjectId); @@ -1502,6 +1550,12 @@ public class DataBaseAdapter { // UpcomingWidget.notifyDatasetChanged(context); } + public LiveData<Integer> getBoardColor$(long accountId, long localBoardId) { + return new ReactiveLiveData<>(db.getBoardDao().getBoardColor(accountId, localBoardId)) + .map(color -> color == null ? defaultColor : color) + .distinctUntilChanged(); + } + @ColorInt public Integer getBoardColorDirectly(long accountId, long localBoardId) { return db.getBoardDao().getBoardColorByLocalIdDirectly(accountId, localBoardId); @@ -1514,4 +1568,182 @@ public class DataBaseAdapter { public void deleteProjectResourcesByCardIdDirectly(Long localCardId) { db.getJoinCardWithOcsProjectDao().deleteProjectResourcesByCardIdDirectly(localCardId); } + + // ============================================================================================= + // APP STATE + // TODO last boards and stacks per account should be moved to a table to benefit from cascading + // ============================================================================================= + + // --------------- + // Current account + // --------------- + + public void saveCurrentAccount(@NonNull Account account) { + executor.submit(() -> { + // Glide Module depends on correct account being set. + // TODO Use SingleSignOnURL where possible, allow passing ssoAccountName to MarkdownEditor + SingleAccountHelper.setCurrentAccount(context, account.getName()); + + DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_account), "→", account.getId()); + sharedPreferencesEditor.putLong(context.getString(R.string.shared_preference_last_account), account.getId()); + sharedPreferencesEditor.apply(); + }); + } + + public void removeCurrentAccount() { + executor.submit(() -> { + // Glide Module depends on correct account being set. + // TODO Use SingleSignOnURL where possible, allow passing ssoAccountName to MarkdownEditor + SingleAccountHelper.setCurrentAccount(context, null); + + DeckLog.log("--- Remove:", context.getString(R.string.shared_preference_last_account)); + sharedPreferencesEditor.remove(context.getString(R.string.shared_preference_last_account)); + sharedPreferencesEditor.apply(); + }); + } + + public LiveData<Long> getCurrentAccountId$() { + return new ReactiveLiveData<>(new SharedPreferenceLongLiveData(sharedPreferences, this.context.getString(R.string.shared_preference_last_account), NOT_AVAILABLE)) + .distinctUntilChanged() + .tap(accountId -> { + DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account), "→", accountId); + if (NOT_AVAILABLE.equals(accountId)) { + executor.submit(this::removeCurrentAccount); + } + }); + } + + public CompletableFuture<Long> getCurrentAccountId() { + return supplyAsync(() -> { + final long accountId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_account), NOT_AVAILABLE); + DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account), "→", accountId); + + if (NOT_AVAILABLE.equals(accountId)) { + saveNeighbourOfAccount(NOT_AVAILABLE); + throw new CompletionException(new IllegalStateException("No current account ID set")); + } + + return accountId; + }, executor); + } + + @WorkerThread + public void saveNeighbourOfAccount(long currentAccountId) { + getAllAccountsDirectly() + .stream() + .filter(account -> currentAccountId != account.getId()) + .findFirst() + .ifPresentOrElse(this::saveCurrentAccount, this::removeCurrentAccount); + } + + @ColorInt + public CompletableFuture<Integer> getCurrentAccountColor(long accountId) { + return supplyAsync(() -> db.getAccountDao().getAccountColorDirectly(accountId), executor) + .thenApplyAsync(color -> color == null ? defaultColor : color, executor); + } + + // ------------- + // Current board + // ------------- + + public void saveCurrentBoardId(long accountId, long boardId) { + DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId); + sharedPreferencesEditor.putLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, boardId); + sharedPreferencesEditor.apply(); + } + + public void removeCurrentBoardId(long accountId) { + DeckLog.log("--- Remove:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId); + sharedPreferencesEditor.remove(context.getString(R.string.shared_preference_last_board_for_account_) + accountId); + sharedPreferencesEditor.apply(); + } + + public LiveData<Long> getCurrentBoardId$(long accountId) { + return new ReactiveLiveData<>(new SharedPreferenceLongLiveData(sharedPreferences, + this.context.getString(R.string.shared_preference_last_board_for_account_) + accountId, NOT_AVAILABLE)) + .distinctUntilChanged() + .tap(boardId -> { + DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId); + if (NOT_AVAILABLE.equals(boardId)) { + executor.submit(() -> saveNeighbourOfBoard(accountId, NOT_AVAILABLE)); + } + }); + } + + @WorkerThread + public void saveNeighbourOfBoard(long accountId, long currentBoardId) { + getNeighbour(db.getBoardDao().getNotDeletedBoardsDirectly(accountId, 0), currentBoardId) + .ifPresentOrElse(neighbourBoardId -> saveCurrentBoardId(accountId, neighbourBoardId), () -> removeCurrentBoardId(accountId)); + } + + public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) { + return supplyAsync(() -> getBoardColorDirectly(accountId, boardId), executor) + .thenApplyAsync(color -> color == null ? defaultColor : color, executor); + } + + // ------------- + // Current stack + // ------------- + + public void saveCurrentStackId(long accountId, long boardId, long stackId) { + DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", stackId); + sharedPreferencesEditor.putLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, stackId); + sharedPreferencesEditor.apply(); + } + + public void removeCurrentStackId(long accountId, long boardId) { + DeckLog.log("--- Remove:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId); + sharedPreferencesEditor.remove(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId); + sharedPreferencesEditor.apply(); + } + + public LiveData<Long> getCurrentStackId$(long accountId, long boardId) { + return new ReactiveLiveData<>(new SharedPreferenceLongLiveData(sharedPreferences, context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, NOT_AVAILABLE)) + .distinctUntilChanged() + .tap(stackId -> { + DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", stackId); + if (NOT_AVAILABLE.equals(stackId)) { + executor.submit(() -> saveNeighbourOfStack(accountId, boardId, NOT_AVAILABLE)); + } + }); + } + + @WorkerThread + public void saveNeighbourOfStack(long accountId, long boardId, long currentStackId) { + getNeighbour(getFullStacksForBoardDirectly(accountId, boardId), currentStackId) + .ifPresentOrElse( + neighbourStackId -> saveCurrentStackId(accountId, boardId, neighbourStackId), + () -> removeCurrentStackId(accountId, boardId)); + } + + /** + * @return the local ID of the direct neighbour of the given {@param currentId} if available. Prefers neighbours to the start of the wanted, but might also return a neighbour to the end. + */ + private Optional<Long> getNeighbour(List<? extends IRemoteEntity> entities, long currentId) { + if (entities.size() < 1) { + return Optional.empty(); + } + + @Nullable Integer position = null; + + for (int i = 0; i < entities.size(); i++) { + if (entities.get(i).getLocalId() == currentId) { + position = i; + } + } + + // Not found, but there is an entry + if (position == null) { + return Optional.of(entities.get(0).getLocalId()); + } + + // Current entity is last entity + if (position == 0 && entities.size() == 1) { + return Optional.empty(); + } + + return Optional.of(position > 0 + ? entities.get(position - 1).getLocalId() + : entities.get(position + 1).getLocalId()); + } } 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 34a0515e3..739e4b9a4 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 @@ -96,6 +96,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migra 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_30_31; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_31_32; 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; @@ -134,7 +135,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migra FilterWidgetSort.class, }, exportSchema = false, - version = 31 + version = 32 ) @TypeConverters({DateTypeConverter.class, EnumConverter.class}) public abstract class DeckDatabase extends RoomDatabase { @@ -186,6 +187,7 @@ public abstract class DeckDatabase extends RoomDatabase { .addMigrations(new Migration_28_29()) .addMigrations(new Migration_29_30(context)) .addMigrations(new Migration_30_31()) + .addMigrations(new Migration_31_32(context)) .fallbackToDestructiveMigration() .addCallback(ON_CREATE_CALLBACK) .build(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java index 6ebe4a8f3..a4d1af44d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java @@ -42,4 +42,10 @@ public interface AccountDao extends GenericDao<Account> { @Query("SELECT * from account a where a.url like :hostLike and exists (select 1 from board b where b.id = :boardRemoteId and a.id = b.accountId)") List<Account> readAccountsForHostWithReadAccessToBoardDirectly(String hostLike, long boardRemoteId); + + @Query("SELECT a.color FROM account a where a.id = :accountId") + LiveData<Integer> getAccountColor(long accountId); + + @Query("SELECT a.color FROM account a where a.id = :accountId") + Integer getAccountColorDirectly(long accountId); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java index 066b512e9..15daf8923 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java @@ -13,18 +13,17 @@ import it.niedermann.nextcloud.deck.model.full.FullBoard; @Dao public interface BoardDao extends GenericDao<Board> { - @Query("SELECT * FROM board WHERE accountId = :accountId and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") - LiveData<List<Board>> getBoardsForAccount(final long accountId); - - @Query("SELECT * FROM board WHERE accountId = :accountId and archived = 1 and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") - LiveData<List<Board>> getArchivedBoardsForAccount(final long accountId); + @Transaction + @Query("SELECT * FROM board WHERE accountId = :accountId and archived = :archived and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") + LiveData<List<Board>> getNotDeletedBoards(long accountId, int archived); - @Query("SELECT * FROM board WHERE accountId = :accountId and archived = 0 and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") - LiveData<List<Board>> getNonArchivedBoardsForAccount(final long accountId); + @Transaction + @Query("SELECT * FROM board WHERE accountId = :accountId and archived = :archived and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") + List<Board> getNotDeletedBoardsDirectly(long accountId, int archived); @Transaction @Query("SELECT * FROM board WHERE accountId = :accountId and archived = :archived and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") - LiveData<List<FullBoard>> getArchivedFullBoards(long accountId, int archived); + LiveData<List<FullBoard>> getNotDeletedFullBoards(long accountId, int archived); @Query("SELECT * FROM board WHERE accountId = :accountId and id = :remoteId") LiveData<Board> getBoardByRemoteId(final long accountId, final long remoteId); @@ -88,4 +87,7 @@ public interface BoardDao extends GenericDao<Board> { @Query("SELECT b.color FROM board b where b.localId = :localBoardId and b.accountId = :accountId") Integer getBoardColorByLocalIdDirectly(long accountId, long localBoardId); + + @Query("SELECT b.color FROM board b where b.localId = :localBoardId and b.accountId = :accountId") + LiveData<Integer> getBoardColor(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/UserDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java index 671f044b0..7fad7499e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java @@ -55,7 +55,7 @@ public interface UserDao extends GenericDao<User> { "and ( uid LIKE :searchTerm or displayname LIKE :searchTerm or primaryKey LIKE :searchTerm ) " + "and u.localId <> (select b.ownerId from board b where localId = :boardId)" + "ORDER BY u.displayname") - List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long boardId, final String searchTerm); + LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long boardId, final String searchTerm); @Query("SELECT * FROM user WHERE accountId = :accountId and uid = :uid") User getUserByUidDirectly(final long accountId, final String uid); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_31_32.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_31_32.java new file mode 100644 index 000000000..c26208252 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_31_32.java @@ -0,0 +1,29 @@ +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; + +/** + */ +public class Migration_31_32 extends Migration { + + @NonNull + private final Context context; + public Migration_31_32(@NonNull Context context) { + super(31, 32); + this.context = context; + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .remove("it.niedermann.nextcloud.deck.theme_main") + .remove("it.niedermann.nextcloud.deck.last_account_color") + .apply(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/LiveDataHelper.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/LiveDataHelper.java deleted file mode 100644 index af503b029..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/LiveDataHelper.java +++ /dev/null @@ -1,75 +0,0 @@ -package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util; - -import static androidx.lifecycle.Transformations.distinctUntilChanged; - -import androidx.annotation.NonNull; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MediatorLiveData; -import androidx.lifecycle.Observer; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class LiveDataHelper { - - private LiveDataHelper() { - throw new UnsupportedOperationException("This class must not be instantiated."); - } - - private static final ExecutorService executor = Executors.newCachedThreadPool(); - - public static <T> LiveData<T> interceptLiveData(LiveData<T> data, DataChangeProcessor<T> onDataChange) { - MediatorLiveData<T> ret = new MediatorLiveData<>(); - - ret.addSource(data, changedData -> - executor.submit(() -> { - onDataChange.onDataChanged(changedData); - ret.postValue(changedData); - }) - ); - return distinctUntilChanged(ret); - } - - - public static <I, O> LiveData<O> postCustomValue(LiveData<I> data, DataTransformator<I, O> transformator) { - final MediatorLiveData<O> ret = new MediatorLiveData<>(); - ret.addSource(data, changedData -> executor.submit(() -> ret.postValue(transformator.transform(changedData)))); - return distinctUntilChanged(ret); - } - - public static <I> MediatorLiveData<I> of(I oneShot) { - return new MediatorLiveData<>() { - @Override - public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) { - super.observe(owner, observer); - executor.submit(() -> postValue(oneShot)); - } - }; - } - - public static <I, O> LiveData<O> postSingleValue(LiveData<I> data, DataTransformator<I, O> transformator) { - final MediatorLiveData<O> ret = new MediatorLiveData<>(); - ret.addSource(data, changedData -> executor.submit(() -> ret.postValue(transformator.transform(changedData)))); - return distinctUntilChanged(ret); - } - - public static <T> void observeOnce(LiveData<T> liveData, LifecycleOwner owner, Observer<T> observer) { - final Observer<T> tempObserver = new Observer<>() { - @Override - public void onChanged(T result) { - liveData.removeObserver(this); - observer.onChanged(result); - } - }; - liveData.observe(owner, tempObserver); - } - - public interface DataChangeProcessor<T> { - void onDataChanged(T data); - } - - public interface DataTransformator<I, O> { - O transform(I data); - } -}
\ No newline at end of file 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 deleted file mode 100644 index df2eac222..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java +++ /dev/null @@ -1,32 +0,0 @@ -package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util; - -import androidx.annotation.Nullable; -import androidx.lifecycle.MutableLiveData; - -/** - * Extends a {@link MutableLiveData} with an error state - * - * @param <T> - */ -public class WrappedLiveData<T> extends MutableLiveData<T> { - @Nullable - private Throwable error = null; - - public boolean hasError() { - return error != null; - } - - public void setError(@Nullable Throwable error) { - this.error = error; - } - - @Nullable - public Throwable getError() { - return error; - } - - public void postError(@Nullable Throwable error) { - setError(error); - postValue(null); - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java deleted file mode 100644 index b9fa58b7d..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java +++ /dev/null @@ -1,75 +0,0 @@ -package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public class Debouncer <T> { - private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1); - private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<>(); - private final Callback<T> callback; - private final int interval; - - public Debouncer(Callback<T> c, int interval) { - this.callback = c; - this.interval = interval; - } - - public void call(T key) { - TimerTask task = new TimerTask(key); - - TimerTask prev; - do { - prev = delayedMap.putIfAbsent(key, task); - if (prev == null) - sched.schedule(task, interval, TimeUnit.MILLISECONDS); - // Exit only if new task was added to map, or existing task was extended successfully - } while (prev != null && !prev.extend()); - } - - public void terminate() { - sched.shutdownNow(); - } - - public interface Callback<T> { - void call(T key); - } - - // The task that wakes up when the wait time elapses - private class TimerTask implements Runnable { - private final T key; - private long dueTime; - private final Object lock = new Object(); - - public TimerTask(T key) { - this.key = key; - extend(); - } - - public boolean extend() { - synchronized (lock) { - if (dueTime < 0) // Task has been shutdown - return false; - dueTime = System.currentTimeMillis() + interval; - return true; - } - } - - public void run() { - synchronized (lock) { - long remaining = dueTime - System.currentTimeMillis(); - if (remaining > 0) { // Re-schedule task - sched.schedule(this, remaining, TimeUnit.MILLISECONDS); - } else { // Mark as terminated and invoke callback - dueTime = -1; - try { - callback.call(key); - } finally { - delayedMap.remove(key); - } - } - } - } - } -}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java deleted file mode 100644 index e0f546399..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java +++ /dev/null @@ -1,93 +0,0 @@ -package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst; - -import androidx.lifecycle.MediatorLiveData; - -import java.util.List; - -import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.api.ResponseCallback; -import it.niedermann.nextcloud.deck.exceptions.OfflineException; -import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.model.enums.DBStatus; -import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; -import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; - -public class UserSearchLiveData extends MediatorLiveData<List<User>> implements Debouncer.Callback<Long> { - - private static final int DEBOUNCE_TIME = 300; // ms - private final DataBaseAdapter db; - private final ServerAdapter server; - long accountId; - String searchTerm; - long notYetAssignedInACL; - private final Debouncer<Long> debouncer = new Debouncer<>(this, DEBOUNCE_TIME); - - public UserSearchLiveData(DataBaseAdapter db, ServerAdapter server) { - this.db = db; - this.server = server; - } - - public UserSearchLiveData search(long accountId, long notYetAssignedInACL, String searchTerm) { - this.accountId = accountId; - this.searchTerm = searchTerm; - this.notYetAssignedInACL = notYetAssignedInACL; - new Thread(() -> debouncer.call(notYetAssignedInACL)).start(); - return this; - } - - - @Override - public void call(Long key) { - if (key!=notYetAssignedInACL){ - return; - } - - final String term = String.copyValueOf(searchTerm.toCharArray()); - - postCurrentFromDB(term); - - if (server.hasInternetConnection()) { - try { - Account account = db.getAccountByIdDirectly(accountId); - server.searchUser(term, new ResponseCallback<>(account) { - @Override - public void onResponse(OcsUserList response) { - if (response == null || response.getUsers().isEmpty()){ - return; - } - for (OcsUser user : response.getUsers()) { - User existingUser = db.getUserByUidDirectly(accountId, user.getId()); - if (existingUser == null) { - User newUser = new User(); - newUser.setStatus(DBStatus.UP_TO_DATE.getId()); - newUser.setPrimaryKey(user.getId()); - newUser.setUid(user.getId()); - newUser.setDisplayname(user.getDisplayName()); - db.createUser(accountId, newUser); - } - } - if (!term.equals(searchTerm)) { - return; - } - postCurrentFromDB(term); - } - - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - } - }); - } catch (OfflineException e) { - DeckLog.logError(e); - } - } - } - - private void postCurrentFromDB(String term) { - List<User> foundInDB = db.searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedInACL, term); - postValue(foundInDB); - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java index f6dca3f07..613623da5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java @@ -76,7 +76,7 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { protected boolean removeChild(AbstractSyncDataProvider<?> child) { boolean isRemoved = super.removeChild(child); if (isRemoved && child.getClass() == StackDataProvider.class) { - progressDone ++; + progressDone++; updateProgress(); } return isRemoved; @@ -191,11 +191,11 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, ResponseCallback<Boolean> callback) { Long accountId = callback.getAccount().getId(); List<Label> locallyChangedLabels = dataBaseAdapter.getLocallyChangedLabels(accountId); - AsyncUtil.awaitAsyncWork(locallyChangedLabels.size(), (countDownLatch) -> { + AsyncUtil.awaitAsyncWork(locallyChangedLabels.size(), latch -> { for (Label label : locallyChangedLabels) { Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId()); label.setBoardId(board.getId()); - syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), countDownLatch); + syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), latch); } }); @@ -228,13 +228,17 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { } @Override - public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard fullBoard) { - dataBaseAdapter.deleteBoard(fullBoard.getBoard(), true); + public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard board) { + dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId()); + dataBaseAdapter.removeCurrentStackId(board.getAccountId(), board.getLocalId()); + dataBaseAdapter.deleteBoard(board.getBoard(), true); } @Override - public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard fullBoard) { - dataBaseAdapter.deleteBoardPhysically(fullBoard.getBoard()); + public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard board) { + dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId()); + dataBaseAdapter.removeCurrentStackId(board.getAccountId(), board.getLocalId()); + dataBaseAdapter.deleteBoardPhysically(board.getBoard()); } @Override @@ -251,6 +255,8 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { // not pushed up yet so: continue; } + + dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId()); dataBaseAdapter.deleteBoardPhysically(board.getBoard()); } } 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 ccbb01530..39d049b10 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 @@ -88,7 +88,7 @@ public class OcsProjectDataProvider extends AbstractSyncDataProvider<OcsProject> resource.setProjectId(entity.getLocalId()); resource.setLocalId(dataBaseAdapter.createProjectResourceDirectly(accountId, resource)); if ("deck-card".equals(resource.getType())) { - dataBaseAdapter.assignCardToProjectIfMissng(accountId, entity.getLocalId(), resource.getId()); + dataBaseAdapter.assignCardToProjectIfMissing(accountId, entity.getLocalId(), resource.getId()); } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java index cc2ded815..56fbd2506 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java @@ -85,6 +85,8 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { @Override public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullStack entity) { entity.getStack().setBoardId(board.getId()); + + dataBaseAdapter.saveNeighbourOfStack(entity.getAccountId(), entity.getStack().getBoardId(), entity.getLocalId()); dataBaseAdapter.deleteStackPhysically(entity.getStack()); } @@ -145,6 +147,8 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { // not pushed up yet so: continue; } + + dataBaseAdapter.saveNeighbourOfStack(stackToDelete.getAccountId(), stackToDelete.getStack().getBoardId(), stackToDelete.getLocalId()); dataBaseAdapter.deleteStackPhysically(stackToDelete.getStack()); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java index faabda163..d996c6015 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java @@ -1,19 +1,19 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.util; +import androidx.annotation.NonNull; + import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; import it.niedermann.nextcloud.deck.DeckLog; public class AsyncUtil { - public interface LatchCallback { - void doWork(CountDownLatch latch); - } - public static void awaitAsyncWork(int count, LatchCallback worker){ - CountDownLatch countDownLatch = new CountDownLatch(count); - worker.doWork(countDownLatch); + public static void awaitAsyncWork(int count, @NonNull Consumer<CountDownLatch> worker) { + final var latch = new CountDownLatch(count); + worker.accept(latch); try { - countDownLatch.await(); + latch.await(); } catch (InterruptedException e) { DeckLog.logError(e); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/ConnectivityUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/ConnectivityUtil.java new file mode 100644 index 000000000..fdc099753 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/ConnectivityUtil.java @@ -0,0 +1,38 @@ +package it.niedermann.nextcloud.deck.persistence.sync.helpers.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import it.niedermann.nextcloud.deck.R; + +public class ConnectivityUtil { + + private final ConnectivityManager connectivityManager; + private final String prefKeyWifiOnly; + private final SharedPreferences sharedPreferences; + + public ConnectivityUtil(@NonNull Context context) { + this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + this.prefKeyWifiOnly = context.getString(R.string.pref_key_wifi_only); + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + } + + public boolean hasInternetConnection() { + if (connectivityManager != null) { + if (sharedPreferences.getBoolean(prefKeyWifiOnly, false)) { + final var networkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + if (networkInfo == null) { + return false; + } + return networkInfo.isConnected(); + } else { + return connectivityManager.getActiveNetworkInfo() != null && connectivityManager.getActiveNetworkInfo().isConnected(); + } + } + return false; + } +} 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 4fe2db958..7a09e79a4 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 @@ -14,16 +14,22 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import com.nextcloud.android.sso.AccountImporter; +import com.nextcloud.android.sso.api.ParsedResponse; import com.nextcloud.android.sso.exceptions.AccountImportCancelledException; import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; -import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; import com.nextcloud.android.sso.ui.UiExceptionManager; +import java.util.List; +import java.util.concurrent.CompletableFuture; + import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; @@ -31,6 +37,7 @@ import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.databinding.ActivityImportAccountBinding; import it.niedermann.nextcloud.deck.exceptions.OfflineException; import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker; @@ -43,9 +50,9 @@ public class ImportAccountActivity extends AppCompatActivity { private String prefKeyWifiOnly; private boolean originalWifiOnlyValue = false; - private String sharedPreferenceLastAccount; private String urlFragmentUpdateDeck; + private ImportAccountViewModel importAccountViewModel; private ActivityImportAccountBinding binding; @Override @@ -54,23 +61,25 @@ public class ImportAccountActivity extends AppCompatActivity { Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + importAccountViewModel = new ViewModelProvider(this).get(ImportAccountViewModel.class); binding = ActivityImportAccountBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); prefKeyWifiOnly = getString(R.string.pref_key_wifi_only); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - sharedPreferenceLastAccount = getString(R.string.shared_preference_last_account); urlFragmentUpdateDeck = getString(R.string.url_fragment_update_deck); originalWifiOnlyValue = sharedPreferences.getBoolean(prefKeyWifiOnly, false); - binding.welcomeText.setText(getString(R.string.welcome_text, getString(R.string.app_name))); + importAccountViewModel.hasAccounts().observe(this, hasAccounts -> binding.welcomeText.setText(hasAccounts + ? getString(R.string.welcome_text_further_accounts) + : getString(R.string.welcome_text, getString(R.string.app_name)))); + binding.addButton.setOnClickListener((v) -> { binding.status.setText(""); binding.addButton.setEnabled(false); binding.updateDeckButton.setVisibility(View.GONE); - disableWifiPref(); try { AccountImporter.pickNewAccount(this); } catch (NextcloudFilesAppNotInstalledException e) { @@ -99,113 +108,143 @@ public class ImportAccountActivity extends AppCompatActivity { if (requestCode == REQUEST_AUTH_TOKEN_SSO && resultCode == RESULT_CANCELED) { binding.addButton.setEnabled(true); } else { - try { - AccountImporter.onActivityResult(requestCode, resultCode, data, ImportAccountActivity.this, new AccountImporter.IAccountAccessGranted() { - @SuppressLint("ApplySharedPref") - @Override - public void accountAccessGranted(SingleSignOnAccount account) { - runOnUiThread(() -> { - binding.status.setText(null); - binding.status.setVisibility(View.GONE); - binding.progressCircular.setVisibility(View.VISIBLE); - binding.progressText.setVisibility(View.VISIBLE); - binding.progressCircular.setIndeterminate(true); - binding.progressText.setText(R.string.progress_import_indeterminate); - }); - - SingleAccountHelper.setCurrentAccount(getApplicationContext(), account.name); - final var syncManager = new SyncManager(ImportAccountActivity.this); - final var accountToCreate = new Account(account.name, account.userId, account.url); - syncManager.createAccount(accountToCreate, new IResponseCallback<>() { - @Override - public void onResponse(Account createdAccount) { - // Remember last account - THIS HAS TO BE DONE SYNCHRONOUSLY - DeckLog.log("--- Write: shared_preference_last_account | ", createdAccount.getId()); - sharedPreferences - .edit() - .putLong(sharedPreferenceLastAccount, createdAccount.getId()) - .commit(); - - syncManager.refreshCapabilities(new ResponseCallback<>(createdAccount) { - @Override - public void onResponse(Capabilities response) { - if (!response.isMaintenanceEnabled()) { - if (response.getDeckVersion().isSupported()) { - var progress$ = syncManager.synchronize(new ResponseCallback<>(account) { - @Override - public void onResponse(Boolean response) { - restoreWifiPref(); - SyncWorker.update(getApplicationContext()); - setResult(RESULT_OK); - finish(); - } + disableWifiPref().thenAcceptAsync(v -> { + try { + AccountImporter.onActivityResult(requestCode, resultCode, data, ImportAccountActivity.this, new AccountImporter.IAccountAccessGranted() { + @Override + public void accountAccessGranted(SingleSignOnAccount account) { + runOnUiThread(() -> { + binding.status.setText(null); + binding.status.setVisibility(View.GONE); + binding.progressCircular.setVisibility(View.VISIBLE); + binding.progressText.setVisibility(View.VISIBLE); + binding.progressCircular.setIndeterminate(true); + binding.progressText.setText(R.string.progress_import_indeterminate); + }); - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - setStatusText(throwable.getMessage()); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - rollbackAccountCreation(syncManager, createdAccount.getId()); - } - }); - runOnUiThread(() -> progress$.observe(ImportAccountActivity.this, (progress) -> { - DeckLog.log("New progress value", progress.first, progress.second); - if (progress.first > 0) { - binding.progressCircular.setIndeterminate(false); - } - if (progress.first < progress.second) { - binding.progressText.setText(getString(R.string.progress_import, progress.first + 1, progress.second)); + final var accountToCreate = new Account(account.name, account.userId, account.url); + importAccountViewModel.createAccount(accountToCreate, new IResponseCallback<>() { + @Override + public void onResponse(Account createdAccount) { + try { + final var syncManager = new SyncManager(ImportAccountActivity.this, createdAccount); + + syncManager.refreshCapabilities(new ResponseCallback<>(createdAccount) { + @Override + public void onResponse(Capabilities response) { + if (!response.isMaintenanceEnabled()) { + if (response.getDeckVersion().isSupported()) { + final var callback = new IResponseCallback<>() { + @Override + public void onResponse(Object response) { + var progress$ = syncManager.synchronize(new ResponseCallback<>(account) { + @Override + public void onResponse(Boolean response) { + restoreWifiPref(); + SyncWorker.update(getApplicationContext()); + importAccountViewModel.saveCurrentAccount(account); + setResult(RESULT_OK); + finish(); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + setStatusText(throwable.getMessage()); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + rollbackAccountCreation(createdAccount.getId()); + } + }); + + runOnUiThread(() -> progress$.observe(ImportAccountActivity.this, (progress) -> { + DeckLog.log("New progress value", progress.first, progress.second); + if (progress.first > 0) { + binding.progressCircular.setIndeterminate(false); + } + if (progress.first < progress.second) { + binding.progressText.setText(getString(R.string.progress_import, progress.first + 1, progress.second)); + } + binding.progressCircular.setProgress(progress.first); + binding.progressCircular.setMax(progress.second); + })); + } + }; + + if (response.getDeckVersion().firstCallHasDifferentResponseStructure()) { + syncManager.fetchBoardsFromServer(new ResponseCallback<>(account) { + @Override + public void onResponse(ParsedResponse<List<FullBoard>> response) { + callback.onResponse(createdAccount); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + // We proceed with the import anyway. It's just important that one request has been done. + callback.onResponse(createdAccount); + } + }); + } else { + callback.onResponse(createdAccount); + } + } else { + setStatusText(getString(R.string.deck_outdated_please_update, response.getDeckVersion().getOriginalVersion())); + runOnUiThread(() -> { + binding.updateDeckButton.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(createdAccount.getUrl() + urlFragmentUpdateDeck)))); + binding.updateDeckButton.setVisibility(View.VISIBLE); + }); + rollbackAccountCreation(createdAccount.getId()); } - binding.progressCircular.setProgress(progress.first); - binding.progressCircular.setMax(progress.second); - })); - } else { - setStatusText(getString(R.string.deck_outdated_please_update, response.getDeckVersion().getOriginalVersion())); - runOnUiThread(() -> { - binding.updateDeckButton.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW) - .setData(Uri.parse(createdAccount.getUrl() + urlFragmentUpdateDeck)))); - binding.updateDeckButton.setVisibility(View.VISIBLE); - }); - rollbackAccountCreation(syncManager, createdAccount.getId()); + } else { + setStatusText(R.string.maintenance_mode); + rollbackAccountCreation(createdAccount.getId()); + } + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + if (throwable instanceof OfflineException) { + setStatusText(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account); + } else { + setStatusText(throwable.getMessage()); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } + rollbackAccountCreation(createdAccount.getId()); } - } else { - setStatusText(R.string.maintenance_mode); - rollbackAccountCreation(syncManager, createdAccount.getId()); - } + }); + } catch (NextcloudFilesAppAccountNotFoundException e) { + setStatusText(e.getMessage()); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(e, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + rollbackAccountCreation(createdAccount.getId()); } + } - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - if (throwable instanceof OfflineException) { - setStatusText(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account); - } else { - setStatusText(throwable.getMessage()); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - } - rollbackAccountCreation(syncManager, createdAccount.getId()); + @Override + public void onError(Throwable error) { + IResponseCallback.super.onError(error); + if (error instanceof SQLiteConstraintException) { + DeckLog.warn("Account already added"); + runOnUiThread(() -> setStatusText(getString(R.string.account_already_added, accountToCreate.getName()))); + } else { + runOnUiThread(() -> { + setStatusText(error.getMessage()); + ExceptionDialogFragment.newInstance(error, accountToCreate).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + }); } - }); - } - - @Override - public void onError(Throwable error) { - IResponseCallback.super.onError(error); - if (error instanceof SQLiteConstraintException) { - DeckLog.error("Account has already been added, this should not be the case"); + runOnUiThread(() -> binding.addButton.setEnabled(true)); + restoreWifiPref(); } - setStatusText(error.getMessage()); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(error, accountToCreate).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - restoreWifiPref(); - } - }); - } - }); - } catch (AccountImportCancelledException e) { - runOnUiThread(() -> binding.addButton.setEnabled(true)); - restoreWifiPref(); - DeckLog.info("Account import has been canceled."); - } + }); + } + }); + } catch (AccountImportCancelledException e) { + runOnUiThread(() -> binding.addButton.setEnabled(true)); + restoreWifiPref(); + DeckLog.info("Account import has been canceled."); + } + }, ContextCompat.getMainExecutor(this)); } } @@ -221,14 +260,9 @@ public class ImportAccountActivity extends AppCompatActivity { AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this); } - @SuppressLint("ApplySharedPref") - private void rollbackAccountCreation(@NonNull SyncManager syncManager, final long accountId) { + private void rollbackAccountCreation(final long accountId) { DeckLog.log("Rolling back account creation for " + accountId); - syncManager.deleteAccount(accountId); - SharedPreferences.Editor editor = sharedPreferences.edit(); - DeckLog.log("--- Remove: shared_preference_last_account |", accountId); - editor.remove(sharedPreferenceLastAccount); - editor.commit(); // Has to be done synchronously + importAccountViewModel.deleteAccount(accountId); runOnUiThread(() -> binding.addButton.setEnabled(true)); restoreWifiPref(); } @@ -248,12 +282,11 @@ public class ImportAccountActivity extends AppCompatActivity { } @SuppressLint("ApplySharedPref") - private void disableWifiPref() { - DeckLog.info("--- Temporarily disable sync on wifi only setting"); - sharedPreferences - .edit() - .putBoolean(prefKeyWifiOnly, false) - .commit(); + private CompletableFuture<Void> disableWifiPref() { + return CompletableFuture.runAsync(() -> { + DeckLog.info("--- Temporarily disable sync on wifi only setting"); + sharedPreferences.edit().putBoolean(prefKeyWifiOnly, false).commit(); + }); } private void restoreWifiPref() { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountViewModel.java new file mode 100644 index 000000000..6734a18f9 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountViewModel.java @@ -0,0 +1,33 @@ +package it.niedermann.nextcloud.deck.ui; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; + +public class ImportAccountViewModel extends BaseViewModel { + + public ImportAccountViewModel(@NonNull Application application) { + super(application); + } + + public LiveData<Boolean> hasAccounts() { + return baseRepository.hasAccounts(); + } + + public void saveCurrentAccount(@NonNull Account account) { + this.baseRepository.saveCurrentAccount(account); + } + + public void createAccount(@NonNull Account account, @NonNull IResponseCallback<Account> callback) { + this.baseRepository.createAccount(account, callback); + } + + public void deleteAccount(long accountId) { + this.baseRepository.deleteAccount(accountId); + } +} 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 deleted file mode 100644 index 6c861102b..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java +++ /dev/null @@ -1,1194 +0,0 @@ -package it.niedermann.nextcloud.deck.ui; - -import static androidx.lifecycle.Transformations.switchMap; -import static it.niedermann.nextcloud.deck.DeckApplication.NO_ACCOUNT_ID; -import static it.niedermann.nextcloud.deck.DeckApplication.NO_BOARD_ID; -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.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; -import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.clearBrandColors; -import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.saveBrandColors; -import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ABOUT; -import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ADD_BOARD; -import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ARCHIVED_BOARDS; -import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_SETTINGS; -import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_UPCOMING_CARDS; - -import android.animation.AnimatorInflater; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.sqlite.SQLiteConstraintException; -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkRequest; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.PopupMenu; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.AnyThread; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.splashscreen.SplashScreen; -import androidx.core.view.GravityCompat; -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; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener; -import com.google.android.material.snackbar.Snackbar; -import com.google.android.material.tabs.TabLayoutMediator; -import com.nextcloud.android.sso.AccountImporter; -import com.nextcloud.android.sso.exceptions.AccountImportCancelledException; -import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; -import com.nextcloud.android.sso.exceptions.UnknownErrorException; - -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; - -import it.niedermann.android.crosstabdnd.CrossTabDragAndDrop; -import it.niedermann.android.tablayouthelper.TabLayoutHelper; -import it.niedermann.android.tablayouthelper.TabTitleGenerator; -import it.niedermann.android.util.ColorUtil; -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; -import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.model.Board; -import it.niedermann.nextcloud.deck.model.Stack; -import it.niedermann.nextcloud.deck.model.full.FullBoard; -import it.niedermann.nextcloud.deck.model.full.FullCard; -import it.niedermann.nextcloud.deck.model.full.FullStack; -import it.niedermann.nextcloud.deck.model.internal.FilterInformation; -import it.niedermann.nextcloud.deck.model.ocs.Capabilities; -import it.niedermann.nextcloud.deck.model.ocs.Version; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; -import it.niedermann.nextcloud.deck.ui.about.AboutActivity; -import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog; -import it.niedermann.nextcloud.deck.ui.archivedboards.ArchivedBoardsActvitiy; -import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener; -import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener; -import it.niedermann.nextcloud.deck.ui.board.EditBoardDialogFragment; -import it.niedermann.nextcloud.deck.ui.board.EditBoardListener; -import it.niedermann.nextcloud.deck.ui.card.CardAdapter; -import it.niedermann.nextcloud.deck.ui.card.CreateCardListener; -import it.niedermann.nextcloud.deck.ui.card.NewCardDialog; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; -import it.niedermann.nextcloud.deck.ui.filter.FilterDialogFragment; -import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel; -import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel; -import it.niedermann.nextcloud.deck.ui.settings.SettingsActivity; -import it.niedermann.nextcloud.deck.ui.stack.DeleteStackDialogFragment; -import it.niedermann.nextcloud.deck.ui.stack.DeleteStackListener; -import it.niedermann.nextcloud.deck.ui.stack.EditStackDialogFragment; -import it.niedermann.nextcloud.deck.ui.stack.EditStackListener; -import it.niedermann.nextcloud.deck.ui.stack.OnScrollListener; -import it.niedermann.nextcloud.deck.ui.stack.StackAdapter; -import it.niedermann.nextcloud.deck.ui.stack.StackFragment; -import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; -import it.niedermann.nextcloud.deck.ui.theme.ThemedSnackbar; -import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsActivity; -import it.niedermann.nextcloud.deck.util.CustomAppGlideModule; -import it.niedermann.nextcloud.deck.util.DrawerMenuUtil; - -public class MainActivity extends AppCompatActivity implements DeleteStackListener, EditStackListener, DeleteBoardListener, EditBoardListener, ArchiveBoardListener, OnScrollListener, CreateCardListener, OnNavigationItemSelectedListener { - - protected ActivityMainBinding binding; - protected NavHeaderMainBinding headerBinding; - - protected MainViewModel mainViewModel; - private FilterViewModel filterViewModel; - private PickStackViewModel pickStackViewModel; - - @ColorInt - private int colorAccent; - protected SharedPreferences sharedPreferences; - private StackAdapter stackAdapter; - long lastBoardId; - @NonNull - private List<Board> boardsList = new ArrayList<>(); - private LiveData<List<Board>> boardsLiveData; - private Observer<List<Board>> boardsLiveDataObserver; - private Menu listMenu; - - private LiveData<List<Stack>> stacksLiveData; - - private LiveData<Boolean> hasArchivedBoardsLiveData; - private Observer<Boolean> hasArchivedBoardsLiveDataObserver; - - private boolean currentBoardHasStacks = false; - private int currentBoardStacksCount = 0; - - private boolean firstAccountAdded = false; - private ConnectivityManager.NetworkCallback networkCallback; - - private String addList; - private String addBoard; - @Nullable - private TabLayoutMediator mediator; - @Nullable - private TabLayoutHelper tabLayoutHelper; - private boolean stackMoved; - - private final ActivityResultLauncher<Intent> settingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { - if (result.getResultCode() == Activity.RESULT_OK) { - ActivityCompat.recreate(this); - } - }); - - private final ActivityResultLauncher<Intent> importAccountLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { - if (result.getResultCode() == RESULT_OK) { - firstAccountAdded = true; - } else { - finish(); - } - }); - - @Override - protected void onCreate(Bundle savedInstanceState) { - SplashScreen.installSplashScreen(this); - - super.onCreate(savedInstanceState); - - Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); - - setTheme(R.style.AppTheme); - colorAccent = ContextCompat.getColor(this, R.color.accent); - - binding = ActivityMainBinding.inflate(getLayoutInflater()); - headerBinding = NavHeaderMainBinding.bind(binding.navigationView.getHeaderView(0)); - setContentView(binding.getRoot()); - - mainViewModel = new ViewModelProvider(this).get(MainViewModel.class); - filterViewModel = new ViewModelProvider(this).get(FilterViewModel.class); - pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class); - - addList = getString(R.string.add_list); - addBoard = getString(R.string.add_board); - - setSupportActionBar(binding.toolbar); - - final var toggle = new ActionBarDrawerToggle(this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); - binding.drawerLayout.addDrawerListener(toggle); - toggle.syncState(); - - binding.navigationView.setNavigationItemSelectedListener(this); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - - DeckApplication.readCurrentAccountColor().observe(this, this::applyAccountTheme); - DeckApplication.readCurrentBoardColor().observe(this, this::applyBoardTheme); - - binding.filterText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - filterViewModel.setFilterText(s.toString()); - } - - @Override - public void afterTextChanged(Editable s) { - - } - }); - - 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 { - importAccountLauncher.launch(ImportAccountActivity.createIntent(this)); - return null; - } - }).observe(this, accounts -> { - if (accounts == null || accounts.size() == 0) { - // Last account has been deleted. hasAccounts LiveData will handle this, but we make sure, that branding is reset. - saveBrandColors(this, ContextCompat.getColor(this, R.color.defaultBrand)); - return; - } - - final var lastAccountId = readCurrentAccountId(this); - - for (var account : accounts) { - if (lastAccountId == account.getId() || lastAccountId == NO_ACCOUNT_ID) { - mainViewModel.setCurrentAccount(account); - if (!firstAccountAdded) { - DeckLog.info("Syncing the current account on app start"); - registerAutoSyncOnNetworkAvailable(); - firstAccountAdded = false; - } - break; - } - } - - mainViewModel.getCurrentAccountLiveData().removeObservers(this); - mainViewModel.getCurrentAccountLiveData().observe(this, (currentAccount) -> { - saveCurrentAccount(this, mainViewModel.getCurrentAccount()); - mainViewModel.recreateSyncManager(); - - if (mainViewModel.getCurrentAccount().isMaintenanceEnabled()) { - refreshCapabilities(mainViewModel.getCurrentAccount(), null); - } - - lastBoardId = readCurrentBoardId(this, mainViewModel.getCurrentAccount().getId()); - - if (boardsLiveData != null && boardsLiveDataObserver != null) { - boardsLiveData.removeObserver(boardsLiveDataObserver); - } - - boardsLiveData = mainViewModel.getBoards(currentAccount.getId(), false); - boardsLiveDataObserver = (boards) -> { - if (boards == null) { - throw new IllegalStateException("List<Board> boards must not be null."); - } - - 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) { - currentBoard = boardsList.get(i); - setCurrentBoard(currentBoard); - currentBoardIdWasInList = true; - break; - } - } - if (!currentBoardIdWasInList) { - currentBoard = boardsList.get(0); - setCurrentBoard(currentBoard); - } - - binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); - } else { - clearBrandColors(this); - clearCurrentBoard(); - - binding.filter.setOnClickListener(null); - } - - final var finalCurrentBoard = currentBoard; - if (hasArchivedBoardsLiveData != null && hasArchivedBoardsLiveDataObserver != null) { - hasArchivedBoardsLiveData.removeObserver(hasArchivedBoardsLiveDataObserver); - } - hasArchivedBoardsLiveData = mainViewModel.hasArchivedBoards(currentAccount.getId()); - hasArchivedBoardsLiveDataObserver = (hasArchivedBoards) -> { - mainViewModel.setCurrentAccountHasArchivedBoards(Boolean.TRUE.equals(hasArchivedBoards)); - inflateBoardMenu(finalCurrentBoard); - }; - hasArchivedBoardsLiveData.observe(this, hasArchivedBoardsLiveDataObserver); - }; - boardsLiveData.observe(this, boardsLiveDataObserver); - - Glide - .with(binding.accountSwitcher.getContext()) - .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()); - binding.infoBox.setVisibility(mainViewModel.getCurrentAccount().isMaintenanceEnabled() ? View.VISIBLE : View.GONE); - if (mainViewModel.isCurrentAccountIsSupportedVersion()) { - binding.infoBoxVersionNotSupported.setVisibility(View.GONE); - } else { - binding.infoBoxVersionNotSupported.setText(getString(R.string.info_box_version_not_supported, mainViewModel.getCurrentAccount().getServerDeckVersion(), Version.minimumSupported().getOriginalVersion())); - binding.infoBoxVersionNotSupported.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(mainViewModel.getCurrentAccount().getUrl() + getString(R.string.url_fragment_update_deck))))); - binding.infoBoxVersionNotSupported.setVisibility(View.VISIBLE); - } - }); - - stackAdapter = new StackAdapter(this); - binding.viewPager.setAdapter(stackAdapter); - binding.viewPager.setOffscreenPageLimit(2); - - final var dragAndDrop = new CrossTabDragAndDrop<StackFragment, CardAdapter, FullCard>(getResources(), ViewCompat.getLayoutDirection(binding.getRoot()) == ViewCompat.LAYOUT_DIRECTION_LTR); - 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); - }); - - final var listMenuPopup = new PopupMenu(this, binding.listMenuButton); - listMenu = listMenuPopup.getMenu(); - getMenuInflater().inflate(R.menu.list_menu, listMenu); - listMenuPopup.setOnMenuItemClickListener(this::onOptionsItemSelected); - binding.listMenuButton.setOnClickListener((v) -> listMenuPopup.show()); - - binding.fab.setOnClickListener((v) -> { - // TODO We should hide the FAB while the dialog is open - but how to detect the dialog has been closed? - binding.fab.hide(); - if (this.boardsList.size() > 0) { - try { - NewCardDialog.newInstance( - mainViewModel.getCurrentAccount(), - mainViewModel.getCurrentBoardLocalId(), - stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(), - mainViewModel.getCurrentBoardColor() - ).show(getSupportFragmentManager(), NewCardDialog.class.getSimpleName()); - } catch (IndexOutOfBoundsException e) { - EditStackDialogFragment.newInstance().show(getSupportFragmentManager(), addList); - } - } else { - EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard); - } - }); - - binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { /* Silence is gold */ } - - @Override - public void onPageSelected(int position) { - final int currentViewPagerItem = binding.viewPager.getCurrentItem(); - listMenu.findItem(R.id.move_list_left).setVisible(currentBoardHasStacks && currentViewPagerItem > 0); - listMenu.findItem(R.id.move_list_right).setVisible(currentBoardHasStacks && currentViewPagerItem < currentBoardStacksCount - 1); - binding.viewPager.post(() -> { - // stackAdapter size might differ from position when an account has been deleted - if (stackAdapter.getItemCount() > position) { - saveCurrentStackId(getApplicationContext(), mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), stackAdapter.getItem(position).getLocalId()); - } else { - DeckLog.logError(new IllegalStateException("Tried to save current Stack which cannot be available (stackAdapter doesn't have this position)")); - } - }); - - binding.fab.extend(); - } - - @Override - public void onPageScrollStateChanged(int state) { - if (!binding.swipeRefreshLayout.isRefreshing()) { - binding.swipeRefreshLayout.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE); - } - } - }); - filterViewModel.hasActiveFilter().observe(this, (hasActiveFilter) -> binding.filterIndicator.setVisibility(hasActiveFilter ? View.VISIBLE : View.GONE)); -// binding.archivedCards.setOnClickListener((v) -> startActivity(ArchivedCardsActivity.createIntent(this, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), mainViewModel.currentBoardHasEditPermission()))); - binding.enableSearch.setOnClickListener((v) -> showFilterTextToolbar()); - binding.toolbar.setOnClickListener((v) -> showFilterTextToolbar()); - - - binding.swipeRefreshLayout.setOnRefreshListener(() -> { - DeckLog.info("Triggered manual refresh"); - - CustomAppGlideModule.clearCache(this); - - DeckLog.verbose("Trigger refresh capabilities for", mainViewModel.getCurrentAccount().getName()); - refreshCapabilities(mainViewModel.getCurrentAccount(), () -> { - DeckLog.verbose("Trigger synchronization for", mainViewModel.getCurrentAccount().getName()); - mainViewModel.synchronize(new ResponseCallback<>(mainViewModel.getCurrentAccount()) { - @Override - public void onResponse(Boolean response) { - DeckLog.info("End of synchronization for " + mainViewModel.getCurrentAccount().getName() + " → Stop spinner."); - runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false)); - } - - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - DeckLog.info("End of synchronization for " + mainViewModel.getCurrentAccount().getName() + " → Stop spinner."); - showSyncFailedSnackbar(throwable); - runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false)); - } - }); - }); - }); - }); - binding.accountSwitcher.setOnClickListener((v) -> AccountSwitcherDialog.newInstance().show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName())); - } - - private void applyBoardTheme(@ColorInt int color) { - final var utils = ThemeUtils.of(color, this); - final var scheme = ThemeUtils.createScheme(color, this); - - utils.deck.themeTabLayout(binding.stackTitles); - utils.material.themeExtendedFAB(binding.fab); - utils.androidx.themeSwipeRefreshLayout(binding.swipeRefreshLayout); - utils.platform.colorEditText(binding.filterText); - - DrawableCompat.setTint(binding.filterIndicator.getDrawable(), scheme.getOnPrimaryContainer()); - } - - private void applyAccountTheme(@ColorInt int accountColor) { - final var utils = ThemeUtils.of(accountColor, this); - - utils.platform.colorNavigationView(binding.navigationView, false); - - headerBinding.headerView.setBackgroundColor(accountColor); - @ColorInt final int headerTextColor = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(accountColor); - headerBinding.appName.setTextColor(headerTextColor); - DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor); - DrawableCompat.setTint(headerBinding.copyDebugLogs.getDrawable(), headerTextColor); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - this.binding = null; - this.headerBinding = null; - if (tabLayoutHelper != null) { - tabLayoutHelper.release(); - } - } - - @Override - public void onCreateStack(String stackName) { - DeckLog.info("Create Stack in account", mainViewModel.getCurrentAccount().getName(), "on board", mainViewModel.getCurrentBoardLocalId()); - mainViewModel.createStack(mainViewModel.getCurrentAccount().getId(), stackName, mainViewModel.getCurrentBoardLocalId(), new IResponseCallback<>() { - @Override - public void onResponse(FullStack response) { - DeckApplication.saveCurrentStackId(MainActivity.this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), response.getLocalId()); - binding.viewPager.post(() -> { - try { - binding.viewPager.setCurrentItem(stackAdapter.getPosition(response.getLocalId())); - } catch (NoSuchElementException e) { - DeckLog.logError(e); - } - }); - } - - @Override - public void onError(Throwable error) { - IResponseCallback.super.onError(error); - runOnUiThread(() -> ThemedSnackbar.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())) - .setAnchorView(binding.fab) - .show()); - } - }); - } - - @Override - public void onUpdateStack(long localStackId, String stackName) { - mainViewModel.updateStackTitle(localStackId, stackName, new IResponseCallback<>() { - @Override - public void onResponse(FullStack response) { - DeckLog.info("Successfully updated", Stack.class.getSimpleName(), "to", stackName); - } - - @Override - public void onError(Throwable throwable) { - IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - } - }); - } - - @Override - public void onCreateBoard(String title, @ColorInt int color) { - if (boardsLiveData == null || boardsLiveDataObserver == null) { - throw new IllegalStateException("Cannot create board when no one observe boards yet. boardsLiveData or observer is null."); - } - boardsLiveData.removeObserver(boardsLiveDataObserver); - final var boardToCreate = new Board(title, color); - boardToCreate.setPermissionEdit(true); - boardToCreate.setPermissionManage(true); - - mainViewModel.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate, new IResponseCallback<>() { - @Override - public void onResponse(FullBoard response) { - runOnUiThread(() -> { - if (response != null) { - boardsList.add(response.getBoard()); - setCurrentBoard(response.getBoard()); - inflateBoardMenu(response.getBoard()); - EditStackDialogFragment.newInstance().show(getSupportFragmentManager(), addList); - } - boardsLiveData.observe(MainActivity.this, boardsLiveDataObserver); - }); - } - - @Override - public void onError(Throwable throwable) { - IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ThemedSnackbar.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())) - .setAnchorView(binding.fab) - .show()); - } - }); - } - - @Override - public void onUpdateBoard(FullBoard fullBoard) { - mainViewModel.updateBoard(fullBoard, new IResponseCallback<>() { - @Override - public void onResponse(FullBoard response) { - DeckLog.info("Successfully updated board", fullBoard.getBoard().getTitle()); - } - - @Override - public void onError(Throwable throwable) { - IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - } - }); - } - - private void refreshCapabilities(final Account account, @Nullable Runnable runAfter) { - DeckLog.verbose("Refreshing capabilities for", account.getName()); - mainViewModel.refreshCapabilities(new ResponseCallback<>(account) { - @Override - public void onResponse(Capabilities response) { - DeckLog.verbose("Finished refreshing capabilities for", account.getName(), "successfully."); - if (response.isMaintenanceEnabled()) { - DeckLog.verbose("Maintenance mode is enabled."); - } else { - DeckLog.verbose("Maintenance mode is disabled."); - // 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() && !response.getDeckVersion().isSupported()) { - ActivityCompat.recreate(MainActivity.this); - } - } - - if (runAfter != null) { - runAfter.run(); - } - } - - @Override - public void onError(Throwable throwable) { - DeckLog.warn("Error on refreshing capabilities for", account.getName(), "(" + throwable.getMessage() + ")."); - if (throwable.getClass() == OfflineException.class || 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()); - } - - if (runAfter != null) { - runAfter.run(); - } - } - }); - } - - protected void clearCurrentBoard() { - binding.toolbar.setTitle(R.string.app_name_short); - binding.filterText.setHint(R.string.app_name_short); - binding.swipeRefreshLayout.setVisibility(View.GONE); - binding.listMenuButton.setVisibility(View.GONE); - binding.emptyContentViewStacks.setVisibility(View.GONE); - binding.emptyContentViewBoards.setVisibility(View.VISIBLE); - } - - protected void setCurrentBoard(@NonNull Board board) { - if (stacksLiveData != null) { - stacksLiveData.removeObservers(this); - } - saveBrandColors(this, board.getColor()); - mainViewModel.setCurrentBoard(board); - filterViewModel.clearFilterInformation(true); - - lastBoardId = board.getLocalId(); - saveCurrentBoardId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()); - binding.navigationView.setCheckedItem(boardsList.indexOf(board)); - - binding.toolbar.setTitle(board.getTitle()); - binding.filterText.setHint(getString(R.string.search_in, board.getTitle())); - - showEditButtonsIfPermissionsGranted(); - - binding.emptyContentViewBoards.setVisibility(View.GONE); - binding.swipeRefreshLayout.setVisibility(View.VISIBLE); - - stacksLiveData = mainViewModel.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), board.getLocalId()); - stacksLiveData.observe(this, (List<Stack> stacks) -> { - if (stacks == null) { - throw new IllegalStateException("Given List<FullStack> must not be null"); - } - currentBoardStacksCount = stacks.size(); - - if (currentBoardStacksCount == 0) { - binding.emptyContentViewStacks.setVisibility(View.VISIBLE); - currentBoardHasStacks = false; - } else { - binding.emptyContentViewStacks.setVisibility(View.GONE); - currentBoardHasStacks = true; - } - listMenu.findItem(R.id.archive_cards).setVisible(currentBoardHasStacks); - - int stackPositionInAdapter = 0; - stackAdapter.setStacks(stacks); - - final var currentStackId = readCurrentStackId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()); - for (int i = 0; i < currentBoardStacksCount; i++) { - if (stacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) { - stackPositionInAdapter = i; - break; - } - } - final int stackPositionInAdapterClone = stackPositionInAdapter; - final TabTitleGenerator tabTitleGenerator = position -> { - if (stacks.size() > position) { - return stacks.get(position).getTitle(); - } else { - DeckLog.warn("Could not generate tab title for position " + position + " because list size is only " + currentBoardStacksCount); - return "ERROR"; - } - }; - final var newMediator = new TabLayoutMediator(binding.stackTitles, binding.viewPager, (tab, position) -> tab.setText(tabTitleGenerator.getTitle(position))); - runOnUiThread(() -> { - setStackMediator(newMediator); - binding.viewPager.setCurrentItem(stackPositionInAdapterClone, false); - if (stackMoved) { // Required to make sure that the correct tab will be selected after moving stacks - binding.viewPager.post(() -> binding.viewPager.setCurrentItem(stackPositionInAdapterClone, false)); - stackMoved = false; - } - updateTabLayoutHelper(tabTitleGenerator); - }); - - listMenu.findItem(R.id.rename_list).setVisible(currentBoardHasStacks); - listMenu.findItem(R.id.delete_list).setVisible(currentBoardHasStacks); - }); - } - - @UiThread - private void updateTabLayoutHelper(@NonNull TabTitleGenerator tabTitleGenerator) { - if (this.tabLayoutHelper == null) { - this.tabLayoutHelper = new TabLayoutHelper(binding.stackTitles, binding.viewPager, tabTitleGenerator); - } else { - tabLayoutHelper.setTabTitleGenerator(tabTitleGenerator); - } - } - - @UiThread - private void setStackMediator(@NonNull final TabLayoutMediator newMediator) { - if (mediator != null) { - mediator.detach(); - } - newMediator.attach(); - this.mediator = newMediator; - } - - @UiThread - protected void inflateBoardMenu(@Nullable Board currentBoard) { - binding.navigationView.setItemIconTintList(null); - final var menu = binding.navigationView.getMenu(); - menu.clear(); - 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: - startActivity(AboutActivity.createIntent(this, mainViewModel.getCurrentAccount())); - break; - case MENU_ID_SETTINGS: - settingsLauncher.launch(SettingsActivity.createIntent(this)); - break; - case MENU_ID_ADD_BOARD: - EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard); - break; - case MENU_ID_ARCHIVED_BOARDS: - startActivity(ArchivedBoardsActvitiy.createIntent(MainActivity.this, mainViewModel.getCurrentAccount())); - break; - case MENU_ID_UPCOMING_CARDS: - startActivity(UpcomingCardsActivity.createIntent(MainActivity.this)); - break; - default: - setCurrentBoard(boardsList.get(item.getItemId())); - break; - } - binding.drawerLayout.closeDrawer(GravityCompat.START); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == R.id.archive_cards) { - final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); - final var stackLocalId = stack.getLocalId(); - mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, (numberOfCards) -> runOnUiThread(() -> - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.archive_cards) - .setMessage(getString(FilterInformation.hasActiveFilter(filterViewModel.getFilterInformation().getValue()) - ? R.string.do_you_want_to_archive_all_cards_of_the_filtered_list - : R.string.do_you_want_to_archive_all_cards_of_the_list, stack.getTitle())) - .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> { - final var filterInformation = filterViewModel.getFilterInformation().getValue(); - mainViewModel.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation, new IResponseCallback<>() { - @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)) { - IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - } - } - }); - }) - .setNeutralButton(android.R.string.cancel, null) - .create() - .show() - )); - return true; - } else if (itemId == R.id.add_list) { - EditStackDialogFragment.newInstance().show(getSupportFragmentManager(), addList); - return true; - } else if (itemId == R.id.rename_list) { - final var stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); - observeOnce(mainViewModel.getStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, fullStack -> - EditStackDialogFragment.newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle()) - .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); - return true; - } else if (itemId == R.id.move_list_left || itemId == R.id.move_list_right) { - final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); - // TODO error handling - mainViewModel.reorderStack(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), stackId, itemId == R.id.move_list_right); - stackMoved = true; - return true; - } else if (itemId == R.id.delete_list) { - final long stackLocalId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); - mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, (numberOfCards) -> runOnUiThread(() -> { - if (numberOfCards != null && numberOfCards > 0) { - DeleteStackDialogFragment.newInstance(stackLocalId, numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName()); - } else { - onStackDeleted(stackLocalId); - } - })); - return true; - } - return super.onOptionsItemSelected(item); - } - - protected void showEditButtonsIfPermissionsGranted() { - if (mainViewModel.currentBoardHasEditPermission()) { - binding.fab.show(); - binding.listMenuButton.setVisibility(View.VISIBLE); - binding.emptyContentViewStacks.showDescription(); - } else { - binding.fab.hide(); - binding.listMenuButton.setVisibility(View.GONE); - binding.emptyContentViewStacks.hideDescription(); - } - } - - @Override - public void onScrollUp() { - binding.fab.extend(); - } - - @Override - public void onScrollDown() { - binding.fab.shrink(); - } - - @Override - public void onBottomReached() { - binding.fab.extend(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - try { - AccountImporter.onActivityResult(requestCode, resultCode, data, this, (account) -> { - final var accountToCreate = new Account(account.name, account.userId, account.url); - mainViewModel.createAccount(accountToCreate, new IResponseCallback<>() { - @Override - public void onResponse(Account createdAccount) { - final var importSyncManager = new SyncManager(MainActivity.this, account.name); - importSyncManager.refreshCapabilities(new ResponseCallback<>(createdAccount) { - @SuppressLint("StringFormatInvalid") - @Override - public void onResponse(Capabilities response) { - if (!response.isMaintenanceEnabled()) { - if (response.getDeckVersion().isSupported()) { - runOnUiThread(() -> { - final var importSnackbar = ThemedSnackbar.make(binding.coordinatorLayout, R.string.account_is_getting_imported, Snackbar.LENGTH_INDEFINITE) - .setAnchorView(binding.fab); - importSnackbar.show(); - importSyncManager.synchronize(new ResponseCallback<>(createdAccount) { - @Override - public void onResponse(Boolean syncSuccess) { - importSnackbar.dismiss(); - runOnUiThread(() -> ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.account_imported), Snackbar.LENGTH_LONG) - .setAnchorView(binding.fab) - .setAction(R.string.simple_switch, (a) -> { - createdAccount.setColor(response.getColor()); - mainViewModel.setSyncManager(importSyncManager); - mainViewModel.setCurrentAccount(createdAccount); - }) - .show()); - } - - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - runOnUiThread(() -> { - importSnackbar.dismiss(); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - }); - } - }); - }); - } else { - DeckLog.warn("Cannot import account because server version is too low (" + response.getDeckVersion() + "). Minimum server version is currently", Version.minimumSupported()); - runOnUiThread(() -> new MaterialAlertDialogBuilder(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 var openURL = new Intent(Intent.ACTION_VIEW); - openURL.setData(Uri.parse(createdAccount.getUrl() + getString(R.string.url_fragment_update_deck))); - startActivity(openURL); - finish(); - }).show()); - mainViewModel.deleteAccount(createdAccount.getId()); - } - } else { - DeckLog.warn("Cannot import account because server version is currently in maintenance mode."); - runOnUiThread(() -> new MaterialAlertDialogBuilder(MainActivity.this) - .setTitle(R.string.maintenance_mode) - .setMessage(getString(R.string.maintenance_mode_explanation, createdAccount.getUrl())) - .setPositiveButton(R.string.simple_close, null) - .show()); - mainViewModel.deleteAccount(createdAccount.getId()); - } - } - - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - mainViewModel.deleteAccount(createdAccount.getId()); - if (throwable instanceof OfflineException) { - DeckLog.warn("Cannot import account because device is currently offline."); - runOnUiThread(() -> new MaterialAlertDialogBuilder(MainActivity.this) - .setTitle(R.string.you_are_currently_offline) - .setMessage(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account) - .setPositiveButton(R.string.simple_close, null) - .show()); - } else { - ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } - } - }); - } - - @Override - public void onError(Throwable error) { - IResponseCallback.super.onError(error); - if (error instanceof SQLiteConstraintException) { - DeckLog.warn("Account already added"); - ThemedSnackbar.make(binding.coordinatorLayout, R.string.account_already_added, Snackbar.LENGTH_LONG) - .setAnchorView(binding.fab) - .show(); - } else { - ExceptionDialogFragment.newInstance(error, accountToCreate).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } - } - }); - }); - } catch (AccountImportCancelledException e) { - DeckLog.info("Account import has been canceled."); - } - } - - @Override - public void onBackPressed() { - if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { - binding.drawerLayout.closeDrawer(GravityCompat.START); - } else if (binding.searchToolbar.getVisibility() == View.VISIBLE) { - hideFilterTextToolbar(); - } else { - super.onBackPressed(); - } - } - - private void showFilterTextToolbar() { - binding.toolbar.setVisibility(View.GONE); - binding.searchToolbar.setVisibility(View.VISIBLE); - binding.searchToolbar.setNavigationOnClickListener(v1 -> onBackPressed()); - binding.enableSearch.setVisibility(View.GONE); - binding.filterText.requestFocus(); - final var imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(binding.filterText, InputMethodManager.SHOW_IMPLICIT); - binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_on)); - } - - private void hideFilterTextToolbar() { - binding.filterText.setText(null); - final var imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); - binding.searchToolbar.setVisibility(View.GONE); - binding.enableSearch.setVisibility(View.VISIBLE); - binding.toolbar.setVisibility(View.VISIBLE); - binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_off)); - } - - private void registerAutoSyncOnNetworkAvailable() { - final var connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - final var builder = new NetworkRequest.Builder(); - - if (connectivityManager != null) { - if (networkCallback == null) { - networkCallback = new ConnectivityManager.NetworkCallback() { - @Override - public void onAvailable(@NonNull Network network) { - DeckLog.log("Got Network connection"); - mainViewModel.synchronize(new ResponseCallback<>(mainViewModel.getCurrentAccount()) { - @Override - public void onResponse(Boolean response) { - DeckLog.log("Auto-Sync after connection available successful"); - } - - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - if (throwable.getClass() == OfflineException.class || throwable instanceof OfflineException) { - DeckLog.error("Do not show sync failed snackbar because it is an ", OfflineException.class.getSimpleName(), "- assuming the user has wi-fi disabled but \"Sync only on wi-fi\" enabled"); - } else if (throwable.getClass() == UnknownErrorException.class || throwable instanceof UnknownErrorException) { - DeckLog.error("Do not show sync failed snackbar because it is an ", UnknownErrorException.class.getSimpleName(), "- assuming a not reachable server or infrastructure issues"); - } else { - showSyncFailedSnackbar(throwable); - } - } - }); - } - - @Override - public void onLost(@NonNull Network network) { - DeckLog.log("Network lost"); - } - }; - } - try { - connectivityManager.unregisterNetworkCallback(networkCallback); - } catch (IllegalArgumentException ignored) { - } - connectivityManager.registerNetworkCallback(builder.build(), networkCallback); - } - } - - /** - * Find a StackFragment by it's ID, may return null. - * - * @param stackId ID of the stack to find - * @return Instance of StackFragment - */ - @Nullable - public StackFragment findStackFragmentById(long stackId) { - return (StackFragment) getSupportFragmentManager().findFragmentByTag("f" + stackId); - } - - /** - * This method is called when a new Card is created - * - * @param createdCard The new Card's data - */ - @Override - public void onCardCreated(FullCard createdCard) { - final var card = createdCard.getCard(); - DeckLog.log("Card Created! Title:" + card.getTitle() + " in stack ID: " + card.getStackId()); - - // Scroll the given StackFragment to the bottom, so the new Card is in view. - final var fragment = findStackFragmentById(card.getStackId()); - if (fragment != null) { - fragment.scrollToBottom(); - } - } - - @Override - public void onDismiss(DialogInterface dialog) { - this.binding.fab.show(); - } - - @Override - public void onStackDeleted(long stackLocalId) { - int nextStackPosition; - try { - nextStackPosition = stackAdapter.getNeighbourPosition(binding.viewPager.getCurrentItem()); - } catch (NoSuchElementException | IndexOutOfBoundsException e) { - nextStackPosition = 0; - DeckLog.logError(e); - } - binding.viewPager.setCurrentItem(nextStackPosition); - mainViewModel.deleteStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, mainViewModel.getCurrentBoardLocalId(), new IResponseCallback<>() { - @Override - public void onResponse(Void response) { - DeckLog.info("Successfully deleted stack with local id", stackLocalId, "and remote id", stackLocalId); - } - - @Override - public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { - IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - } - } - }); - } - - @Override - public void onBoardDeleted(Board board) { - final int index = this.boardsList.indexOf(board); - if (board.getLocalId().equals(mainViewModel.getCurrentBoardLocalId())) { - if (index > 0) { // Select first board after deletion - setCurrentBoard(this.boardsList.get(0)); - } else if (this.boardsList.size() > 1) { // Select second board after deletion - setCurrentBoard(this.boardsList.get(1)); - } else { // No other board is available, open create dialog - clearBrandColors(this); - clearCurrentBoard(); - EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard); - } - } - - mainViewModel.deleteBoard(board, new IResponseCallback<>() { - @Override - public void onResponse(Void response) { - DeckLog.info("Successfully deleted board", board.getTitle()); - } - - @Override - public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { - IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - } - } - }); - - binding.drawerLayout.closeDrawer(GravityCompat.START); - } - - - /** - * Displays a {@link ThemedSnackbar} for an exception of a failed sync, but only if the cause wasn't maintenance mode (this should be handled by a TextView instead of a snackbar). - * - * @param throwable the cause of the failed sync - */ - @AnyThread - private void showSyncFailedSnackbar(@NonNull Throwable throwable) { - if (!(throwable instanceof NextcloudHttpRequestFailedException) || ((NextcloudHttpRequestFailedException) throwable).getStatusCode() != HttpURLConnection.HTTP_UNAVAILABLE) { - runOnUiThread(() -> { - if (binding != null) { // Can be null in case the activity has been destroyed before the synchronization process has been finished - ThemedSnackbar.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())) - .setAnchorView(binding.fab) - .show(); - } - }); - } - } - - @Override - public void onArchive(@NonNull Board board) { - mainViewModel.archiveBoard(board, new IResponseCallback<>() { - @Override - public void onResponse(FullBoard response) { - DeckLog.info("Successfully archived board", board.getTitle()); - } - - @Override - public void onError(Throwable throwable) { - IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); - } - }); - } - - @Override - public void onClone(Board board) { - final String[] animals = {getString(R.string.clone_cards)}; - final boolean[] checkedItems = {false}; - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.clone_board) - .setMultiChoiceItems(animals, checkedItems, (dialog, which, isChecked) -> checkedItems[0] = isChecked) - .setPositiveButton(R.string.simple_clone, (dialog, which) -> { - binding.drawerLayout.closeDrawer(GravityCompat.START); - final var snackbar = ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.cloning_board, board.getTitle()), Snackbar.LENGTH_INDEFINITE) - .setAnchorView(binding.fab); - snackbar.show(); - mainViewModel.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0], new IResponseCallback<>() { - @Override - public void onResponse(FullBoard response) { - runOnUiThread(() -> { - snackbar.dismiss(); - setCurrentBoard(response.getBoard()); - ThemedSnackbar.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())) - .setAnchorView(binding.fab) - .show(); - }); - } - - @Override - public void onError(Throwable throwable) { - IResponseCallback.super.onError(throwable); - runOnUiThread(() -> { - snackbar.dismiss(); - ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - }); - } - }); - }) - .setNeutralButton(android.R.string.cancel, null) - .show(); - } -}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java deleted file mode 100644 index ffa220d94..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java +++ /dev/null @@ -1,303 +0,0 @@ -package it.niedermann.nextcloud.deck.ui; - -import android.app.Application; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -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; -import it.niedermann.nextcloud.deck.model.Board; -import it.niedermann.nextcloud.deck.model.Card; -import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.model.Stack; -import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.model.full.FullBoard; -import it.niedermann.nextcloud.deck.model.full.FullCard; -import it.niedermann.nextcloud.deck.model.full.FullStack; -import it.niedermann.nextcloud.deck.model.internal.FilterInformation; -import it.niedermann.nextcloud.deck.model.ocs.Capabilities; -import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; - -@SuppressWarnings("WeakerAccess") -public class MainViewModel extends AndroidViewModel { - - private SyncManager syncManager; - - private final MutableLiveData<Account> currentAccount = new MutableLiveData<>(); - @Nullable - private Board currentBoard; - private boolean currentAccountHasArchivedBoards = false; - - private boolean currentAccountIsSupportedVersion = false; - - public MainViewModel(@NonNull Application application) { - super(application); - 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(); - } - - public LiveData<Account> getCurrentAccountLiveData() { - return this.currentAccount; - } - - public void setCurrentAccount(Account currentAccount) { - this.currentAccount.setValue(currentAccount); - this.currentAccountIsSupportedVersion = currentAccount.getServerDeckVersionAsObject().isSupported(); - } - - public void setCurrentBoard(@NonNull Board currentBoard) { - this.currentBoard = currentBoard; - } - - public Long getCurrentBoardLocalId() { - if (currentBoard == null) { - throw new IllegalStateException("getCurrentBoardLocalId() called before setCurrentBoard()"); - } - return this.currentBoard.getLocalId(); - } - - @ColorInt - public Integer getCurrentBoardColor() { - if (currentBoard == null) { - throw new IllegalStateException("getCurrentBoardColor() called before setCurrentBoard()"); - } - return this.currentBoard.getColor(); - } - - public Long getCurrentBoardRemoteId() { - if (currentBoard == null) { - throw new IllegalStateException("getCurrentBoardRemoteId() called before setCurrentBoard()"); - } - return this.currentBoard.getId(); - } - - public boolean currentBoardHasEditPermission() { - return this.currentBoard != null && this.currentBoard.isPermissionEdit() && currentAccountIsSupportedVersion; - } - - public boolean currentAccountHasArchivedBoards() { - return currentAccountHasArchivedBoards; - } - - public void setCurrentAccountHasArchivedBoards(boolean currentAccountHasArchivedBoards) { - this.currentAccountHasArchivedBoards = currentAccountHasArchivedBoards; - } - - public boolean isCurrentAccountIsSupportedVersion() { - return currentAccountIsSupportedVersion; - } - - public void recreateSyncManager() { - this.syncManager = new SyncManager(getApplication()); - } - - public void setSyncManager(@NonNull SyncManager syncManager) { - this.syncManager = syncManager; - } - - public void synchronize(@NonNull ResponseCallback<Boolean> responseCallback) { - syncManager.synchronize(responseCallback); - } - - public void refreshCapabilities(@NonNull ResponseCallback<Capabilities> callback) { - syncManager.refreshCapabilities(callback); - } - - public LiveData<Boolean> hasAccounts() { - return syncManager.hasAccounts(); - } - - public void createAccount(@NonNull Account account, @NonNull IResponseCallback<Account> callback) { - syncManager.createAccount(account, callback); - } - - public void deleteAccount(long id) { - syncManager.deleteAccount(id); - } - - public LiveData<List<Account>> readAccounts() { - return syncManager.readAccounts(); - } - - public void createBoard(long accountId, @NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { - syncManager.createBoard(accountId, board, callback); - } - - public void updateBoard(@NonNull FullBoard board, @NonNull IResponseCallback<FullBoard> callback) { - syncManager.updateBoard(board, callback); - } - - public LiveData<List<Board>> getBoards(long accountId, boolean archived) { - return syncManager.getBoards(accountId, archived); - } - - public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { - return syncManager.getFullBoardById(accountId, localId); - } - - public void archiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { - syncManager.archiveBoard(board, callback); - } - - public void dearchiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { - syncManager.dearchiveBoard(board, callback); - } - - public void cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards, @NonNull IResponseCallback<FullBoard> callback) { - syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards, callback); - } - - public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback<Void> callback) { - syncManager.deleteBoard(board, callback); - } - - public LiveData<Boolean> hasArchivedBoards(long accountId) { - return syncManager.hasArchivedBoards(accountId); - } - - public void createAccessControl(long accountId, @NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) { - syncManager.createAccessControl(accountId, entity, callback); - } - - public void updateAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) { - syncManager.updateAccessControl(entity, callback); - } - - public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) { - return syncManager.getAccessControlByLocalBoardId(accountId, id); - } - - public void deleteAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<Void> callback) { - syncManager.deleteAccessControl(entity, callback); - } - - public void createLabel(long accountId, Label label, long localBoardId, @NonNull IResponseCallback<Label> callback) { - syncManager.createLabel(accountId, label, localBoardId, callback); - } - - public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback<Integer> callback) { - syncManager.countCardsWithLabel(localLabelId, callback); - } - - public void updateLabel(@NonNull Label label, @NonNull IResponseCallback<Label> callback) { - syncManager.updateLabel(label, callback); - } - - public void deleteLabel(@NonNull Label label, @NonNull IResponseCallback<Void> callback) { - syncManager.deleteLabel(label, callback); - } - - public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { - return syncManager.getStacksForBoard(accountId, localBoardId); - } - - public void createStack(long accountId, @NonNull String title, long boardLocalId, @NonNull IResponseCallback<FullStack> callback) { - syncManager.createStack(accountId, title, boardLocalId, callback); - } - - public LiveData<FullStack> getStack(long accountId, long localStackId) { - return syncManager.getStack(accountId, localStackId); - } - - public void reorderStack(long accountId, long boardLocalId, long stackLocalId, boolean moveToRight) { - syncManager.reorderStack(accountId, boardLocalId, stackLocalId, moveToRight); - } - - public void updateStackTitle(long localStackId, @NonNull String newTitle, @NonNull IResponseCallback<FullStack> callback) { - syncManager.updateStackTitle(localStackId, newTitle, callback); - } - - public void deleteStack(long accountId, long stackLocalId, long boardLocalId, @NonNull IResponseCallback<Void> callback) { - syncManager.deleteStack(accountId, stackLocalId, boardLocalId, callback); - } - - public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) { - syncManager.reorder(accountId, movedCard, newStackId, newIndex); - } - - public void countCardsInStack(long accountId, long localStackId, @NonNull IResponseCallback<Integer> callback) { - syncManager.countCardsInStackDirectly(accountId, localStackId, callback); - } - - public void archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation, @NonNull IResponseCallback<Void> callback) { - syncManager.archiveCardsInStack(accountId, stackLocalId, filterInformation, callback); - } - - public void updateCard(@NonNull FullCard fullCard, @NonNull IResponseCallback<FullCard> callback) { - syncManager.updateCard(fullCard, callback); - } - - public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) { - syncManager.addCommentToCard(accountId, cardId, comment); - } - - public void addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file, @NonNull IResponseCallback<Attachment> callback) { - syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file, callback); - } - - public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) { - syncManager.addOrUpdateSingleCardWidget(widgetId, accountId, boardId, localCardId); - } - - public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { - return syncManager.getFullCardsForStack(accountId, localStackId, filter); - } - - public void moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId, @NonNull IResponseCallback<Void> callback) { - syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback); - } - - public LiveData<List<FullCard>> getArchivedFullCardsForBoard(long accountId, long localBoardId) { - return syncManager.getArchivedFullCardsForBoard(accountId, localBoardId); - } - - public void assignUserToCard(@NonNull User user, @NonNull Card card) { - syncManager.assignUserToCard(user, card); - } - - public void unassignUserFromCard(@NonNull User user, @NonNull Card card) { - syncManager.unassignUserFromCard(user, card); - } - - public User getUserByUidDirectly(long accountId, String uid) { - return syncManager.getUserByUidDirectly(accountId, uid); - } - - public void archiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) { - syncManager.archiveCard(card, callback); - } - - public void dearchiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) { - syncManager.dearchiveCard(card, callback); - } - - public void deleteCard(@NonNull Card card, @NonNull IResponseCallback<Void> callback) { - syncManager.deleteCard(card, callback); - } - - public void saveCard(long accountId, long boardLocalId, long stackLocalId, @NonNull FullCard fullCard, @NonNull IResponseCallback<FullCard> callback) { - syncManager.createFullCard(accountId, boardLocalId, stackLocalId, fullCard, 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 358f17bbf..5b4f33462 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,10 +1,7 @@ package it.niedermann.nextcloud.deck.ui; -import static androidx.lifecycle.Transformations.switchMap; - import android.os.Bundle; import android.text.Editable; -import android.text.TextWatcher; import android.view.View; import androidx.annotation.NonNull; @@ -13,8 +10,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; -import java.util.List; - +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.ActivityPickStackBinding; @@ -28,6 +24,7 @@ import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener; import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.ui.theme.Themed; +import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher; public abstract class PickStackActivity extends AppCompatActivity implements Themed, PickStackListener { @@ -46,23 +43,22 @@ public abstract class PickStackActivity extends AppCompatActivity implements The setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - switchMap(viewModel.hasAccounts(), hasAccounts -> { - if (hasAccounts) { - return viewModel.readAccounts(); - } else { - // TODO After successfully importing the account, the creation will throw a TokenMissMatchException - Recreate SyncManager? - startActivity(ImportAccountActivity.createIntent(this)); - return null; - } - }).observe(this, (List<Account> accounts) -> { - if (accounts == null || accounts.size() == 0) { - throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry"); - } - getSupportFragmentManager() - .beginTransaction() - .add(R.id.fragment_container, PickStackFragment.newInstance(showBoardsWithoutEditPermission())) - .commit(); - }); + final var hasAccounts$ = new ReactiveLiveData<>(viewModel.hasAccounts()); + + hasAccounts$ + .filter(hasAccounts -> !hasAccounts) + .observe(this, () -> { + startActivity(ImportAccountActivity.createIntent(this)); + finish(); + }); + + hasAccounts$ + .filter(hasAccounts -> hasAccounts) + .observe(this, () -> getSupportFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, PickStackFragment.newInstance(showBoardsWithoutEditPermission())) + .commit()); + binding.cancel.setOnClickListener((v) -> finish()); binding.submit.setOnClickListener((v) -> { viewModel.setSubmitInProgress(true); @@ -88,22 +84,7 @@ public abstract class PickStackActivity extends AppCompatActivity implements The if (requireContent()) { viewModel.setContentIsSatisfied(false); binding.inputWrapper.setVisibility(View.VISIBLE); - binding.input.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Nothing to do here... - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - viewModel.setContentIsSatisfied(s != null && !s.toString().trim().isEmpty()); - } - - @Override - public void afterTextChanged(Editable s) { - // Nothing to do here... - } - }); + binding.input.addTextChangedListener(new OnTextChangedWatcher(s -> viewModel.setContentIsSatisfied(s != null && !s.trim().isEmpty()))); } else { viewModel.setContentIsSatisfied(true); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java index 7ab189aca..32dc2b9d4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java @@ -1,8 +1,5 @@ package it.niedermann.nextcloud.deck.ui; -import static androidx.lifecycle.Transformations.distinctUntilChanged; -import static androidx.lifecycle.Transformations.map; - import android.annotation.SuppressLint; import android.app.Application; import android.net.Uri; @@ -14,25 +11,24 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import com.nextcloud.android.sso.helper.SingleAccountHelper; - import java.net.MalformedURLException; import java.net.URL; import java.util.Optional; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; 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.model.Account; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; import it.niedermann.nextcloud.deck.util.ProjectUtil; -public class PushNotificationViewModel extends AndroidViewModel { +public class PushNotificationViewModel extends BaseViewModel { // Provided by Files app NotificationJob private static final String KEY_SUBJECT = "subject"; @@ -41,12 +37,10 @@ public class PushNotificationViewModel extends AndroidViewModel { private static final String KEY_ACCOUNT = "account"; private static final String KEY_CARD_REMOTE_ID = "objectId"; - private final SyncManager readAccountSyncManager; private final MutableLiveData<Account> account = new MutableLiveData<>(); public PushNotificationViewModel(@NonNull Application application) { super(application); - this.readAccountSyncManager = new SyncManager(application); } @WorkerThread @@ -63,8 +57,7 @@ public class PushNotificationViewModel extends AndroidViewModel { .orElseThrow(() -> new IllegalArgumentException("Account not found")); this.account.postValue(account); - SingleAccountHelper.setCurrentAccount(getApplication(), account.getName()); - final var syncManager = new SyncManager(getApplication()); + final var syncManager = new SyncManager(getApplication(), account); final var card = syncManager.getCardByRemoteIDDirectly(account.getId(), cardRemoteId); @@ -200,7 +193,7 @@ public class PushNotificationViewModel extends AndroidViewModel { } private Optional<Account> extractAccount(@NonNull Bundle bundle) { - return Optional.ofNullable(readAccountSyncManager.readAccountDirectly(bundle.getString(KEY_ACCOUNT))); + return Optional.ofNullable(baseRepository.readAccountDirectly(bundle.getString(KEY_ACCOUNT))); } private Optional<Long> extractBoardLocalId(@NonNull SyncManager syncManager, long accountId, long cardRemoteId) { @@ -230,7 +223,9 @@ public class PushNotificationViewModel extends AndroidViewModel { } public LiveData<Integer> getAccount() { - return distinctUntilChanged(map(this.account, Account::getColor)); + return new ReactiveLiveData<>(this.account) + .map(Account::getColor) + .distinctUntilChanged(); } public interface PushNotificationCallback extends IResponseCallback<CardInformation> { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/StackChangeCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/StackChangeCallback.java new file mode 100644 index 000000000..90fd41990 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/StackChangeCallback.java @@ -0,0 +1,71 @@ +package it.niedermann.nextcloud.deck.ui; + +import android.view.Menu; + +import androidx.annotation.NonNull; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; + +import java.util.function.Consumer; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.ui.stack.StackAdapter; + +public class StackChangeCallback extends ViewPager2.OnPageChangeCallback { + + private final StackAdapter adapter; + private final ViewPager2 viewPager; + private final ExtendedFloatingActionButton fab; + private final SwipeRefreshLayout swipeRefreshLayout; + private final Menu menu; + private final Consumer<Stack> onStackSelected; + + public StackChangeCallback( + @NonNull StackAdapter adapter, + @NonNull ViewPager2 viewPager, + @NonNull ExtendedFloatingActionButton fab, + @NonNull SwipeRefreshLayout swipeRefreshLayout, + @NonNull Menu menu, + @NonNull Consumer<Stack> onStackSelected + ) { + this.adapter = adapter; + this.viewPager = viewPager; + this.fab = fab; + this.swipeRefreshLayout = swipeRefreshLayout; + this.menu = menu; + this.onStackSelected = onStackSelected; + } + + @Override + public void onPageSelected(int position) { + this.updateMoveItemVisibility(); + this.viewPager.post(() -> { + // stackAdapter size might differ from position when an account has been deleted + if (this.adapter.getItemCount() > position) { + this.onStackSelected.accept(this.adapter.getItem(position)); + } else { + DeckLog.logError(new IllegalStateException("Tried to save current Stack which cannot be available (stackAdapter doesn't have this position)")); + } + }); + this.fab.extend(); + } + + @Override + public void onPageScrollStateChanged(int state) { + if (!swipeRefreshLayout.isRefreshing()) { + swipeRefreshLayout.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE); + } + } + + public void updateMoveItemVisibility() { + final var currentBoardHasStacks = adapter.getItemCount() > 0; + final int currentViewPagerItem = viewPager.getCurrentItem(); + + menu.findItem(R.id.move_list_left).setVisible(currentBoardHasStacks && currentViewPagerItem > 0); + menu.findItem(R.id.move_list_right).setVisible(currentBoardHasStacks && currentViewPagerItem < adapter.getItemCount() - 1); + } +} 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 cfb43c382..dc5de960f 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 @@ -13,16 +13,16 @@ 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.exception.ExceptionHandler; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; -public class AboutActivity extends AppCompatActivity { - private static final String BUNDLE_KEY_ACCOUNT = "account"; +public class AboutActivity extends AppCompatActivity implements Themed { + private static final String KEY_ACCOUNT = "account"; private ActivityAboutBinding binding; private final static int[] tabTitles = new int[]{ R.string.about_credits_tab_title, @@ -35,13 +35,22 @@ public class AboutActivity extends AppCompatActivity { super.onCreate(savedInstanceState); Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + final var args = getIntent().getExtras(); + + if (args == null || !args.containsKey(KEY_ACCOUNT)) { + throw new IllegalArgumentException("Provide at least " + KEY_ACCOUNT); + } + + final var account = (Account) args.getSerializable(KEY_ACCOUNT); + binding = ActivityAboutBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); - DeckApplication.readCurrentAccountColor().observe(this, color -> ThemeUtils.of(color, this).deck.themeTabLayout(binding.tabLayout)); + applyTheme(account.getColor()); - setSupportActionBar(binding.toolbar); - binding.viewPager.setAdapter(new TabsPagerAdapter(this, (Account) getIntent().getSerializableExtra(BUNDLE_KEY_ACCOUNT))); + binding.viewPager.setAdapter(new TabsPagerAdapter(this, account)); new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> tab.setText(tabTitles[position])).attach(); } @@ -51,6 +60,13 @@ public class AboutActivity extends AppCompatActivity { this.binding = null; } + @Override + public void applyTheme(int color) { + final var utils = ThemeUtils.of(color, this); + + utils.deck.themeTabLayout(binding.tabLayout); + } + private static class TabsPagerAdapter extends FragmentStateAdapter { @Nullable @@ -70,7 +86,7 @@ public class AboutActivity extends AppCompatActivity { case 1: return new AboutFragmentContributingTab(); case 2: - return new AboutFragmentLicenseTab(); + return AboutFragmentLicenseTab.newInstance(account); default: throw new IllegalArgumentException("position must be between 0 and 2"); } @@ -91,6 +107,6 @@ public class AboutActivity extends AppCompatActivity { @NonNull public static Intent createIntent(@NonNull Context context, @NonNull Account account) { return new Intent(context, AboutActivity.class) - .putExtra(BUNDLE_KEY_ACCOUNT, account); + .putExtra(KEY_ACCOUNT, account); } }
\ No newline at end of file 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 dafc54bee..5ec2c4c2c 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 @@ -10,32 +10,37 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import it.niedermann.nextcloud.deck.DeckApplication; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.FragmentAboutLicenseTabBinding; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; -public class AboutFragmentLicenseTab extends Fragment { +public class AboutFragmentLicenseTab extends Fragment implements Themed { + private static final String KEY_ACCOUNT = "account"; private FragmentAboutLicenseTabBinding binding; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final var args = getArguments(); + + if (args == null || !args.containsKey(KEY_ACCOUNT)) { + throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided"); + } + + final Account account = (Account) requireArguments().getSerializable(KEY_ACCOUNT); + binding = FragmentAboutLicenseTabBinding.inflate(inflater, container, false); setTextWithURL(binding.aboutIconsDisclaimerAppIcon, getResources(), R.string.about_icons_disclaimer_app_icon, R.string.about_app_icon_author_link_label, R.string.url_about_icon_author); setTextWithURL(binding.aboutIconsDisclaimerMdiIcons, getResources(), R.string.about_icons_disclaimer_mdi_icons, R.string.about_icons_disclaimer_mdi, R.string.url_about_icons_disclaimer_mdi); binding.aboutAppLicenseButton.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_license))))); - return binding.getRoot(); - } - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - DeckApplication.readCurrentAccountColor().observe(getViewLifecycleOwner(), color -> - ThemeUtils.of(color, requireContext()).material.colorMaterialButtonPrimaryFilled(binding.aboutAppLicenseButton)); + applyTheme(account.getColor()); + + return binding.getRoot(); } @Override @@ -43,4 +48,21 @@ public class AboutFragmentLicenseTab extends Fragment { super.onDestroy(); this.binding = null; } + + public static Fragment newInstance(@NonNull Account account) { + final var fragment = new AboutFragmentLicenseTab(); + + final var args = new Bundle(); + args.putSerializable(KEY_ACCOUNT, account); + fragment.setArguments(args); + + return fragment; + } + + @Override + public void applyTheme(int color) { + final var utils = ThemeUtils.of(color, requireContext()); + + utils.material.colorMaterialButtonPrimaryFilled(binding.aboutAppLicenseButton); + } }
\ 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 1f0098c4f..4a2ef80e6 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,104 +1,110 @@ package it.niedermann.nextcloud.deck.ui.accountswitcher; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; +import static android.app.Activity.RESULT_OK; import android.app.Dialog; +import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.text.TextUtils; +import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.nextcloud.android.sso.AccountImporter; -import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; -import com.nextcloud.android.sso.ui.UiExceptionManager; import java.util.Objects; import java.util.stream.Collectors; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.android.util.DimensionUtil; -import it.niedermann.nextcloud.deck.DeckApplication; -import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogAccountSwitcherBinding; -import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.ui.MainViewModel; +import it.niedermann.nextcloud.deck.ui.ImportAccountActivity; import it.niedermann.nextcloud.deck.ui.manageaccounts.ManageAccountsActivity; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; public class AccountSwitcherDialog extends DialogFragment { private AccountSwitcherAdapter adapter; private DialogAccountSwitcherBinding binding; - private MainViewModel viewModel; + private AccountViewModel accountViewModel; + + private final ActivityResultLauncher<Intent> importAccountLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() != RESULT_OK) { + requireActivity().finish(); + } + }); @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater()); - viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); - - final Account currentAccount = viewModel.getCurrentAccount(); - binding.accountName.setText( - TextUtils.isEmpty(currentAccount.getUserDisplayName()) - ? currentAccount.getUserName() - : currentAccount.getUserDisplayName() - ); - binding.accountHost.setText(Uri.parse(currentAccount.getUrl()).getHost()); - binding.check.setSelected(true); - - Glide.with(requireContext()) - .load(currentAccount.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size))) - .placeholder(R.drawable.ic_baseline_account_circle_24) - .error(R.drawable.ic_baseline_account_circle_24) - .apply(RequestOptions.circleCropTransform()) - .into(binding.currentAccountItemAvatar); - - binding.accountLayout.setOnClickListener((v) -> dismiss()); + accountViewModel = new ViewModelProvider(requireActivity()).get(AccountViewModel.class); adapter = new AccountSwitcherAdapter((localAccount -> { - viewModel.setCurrentAccount(localAccount); + accountViewModel.saveCurrentAccount(localAccount); dismiss(); })); - observeOnce(viewModel.readAccounts(), requireActivity(), (accounts) -> - adapter.setAccounts(accounts.stream().filter(account -> - !Objects.equals(account.getId(), viewModel.getCurrentAccount().getId())).collect(Collectors.toList()))); - - observeOnce(DeckApplication.readCurrentBoardColor(), requireActivity(), this::applyTheme); - + binding.accountLayout.setOnClickListener((v) -> dismiss()); + binding.check.setSelected(true); binding.accountsList.setAdapter(adapter); - binding.addAccount.setOnClickListener((v) -> { - try { - AccountImporter.pickNewAccount(requireActivity()); - } catch (NextcloudFilesAppNotInstalledException e) { - UiExceptionManager.showDialogForException(requireContext(), e); - DeckLog.warn("============================================================="); - DeckLog.warn("Nextcloud app is not installed. Cannot choose account"); - DeckLog.logError(e); - } catch (AndroidGetAccountsPermissionNotGranted e) { - AccountImporter.requestAndroidAccountPermissionsAndPickAccount(requireActivity()); - } + importAccountLauncher.launch(ImportAccountActivity.createIntent(requireContext())); dismiss(); }); - binding.manageAccounts.setOnClickListener((v) -> { requireActivity().startActivity(ManageAccountsActivity.createIntent(requireContext())); dismiss(); }); + new ReactiveLiveData<>(accountViewModel.getCurrentAccount()) + .flatMap(currentAccount -> { + binding.accountName.setText( + TextUtils.isEmpty(currentAccount.getUserDisplayName()) + ? currentAccount.getUserName() + : currentAccount.getUserDisplayName() + ); + binding.accountHost.setText(Uri.parse(currentAccount.getUrl()).getHost()); + + Glide.with(requireContext()) + .load(currentAccount.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size))) + .placeholder(R.drawable.ic_baseline_account_circle_24) + .error(R.drawable.ic_baseline_account_circle_24) + .apply(RequestOptions.circleCropTransform()) + .into(binding.currentAccountItemAvatar); + + applyTheme(currentAccount.getColor()); + + return new ReactiveLiveData<>(accountViewModel.readAccounts()) + .map(accounts -> accounts + .stream() + .filter(account -> !Objects.equals(account.getId(), currentAccount.getId())) + .collect(Collectors.toList()) + ); + }) + .observe(this, adapter::setAccounts); + return new MaterialAlertDialogBuilder(requireContext()) .setView(binding.getRoot()) .create(); } @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + @Override public void onDestroy() { super.onDestroy(); this.binding = null; @@ -109,6 +115,9 @@ public class AccountSwitcherDialog extends DialogFragment { } private void applyTheme(int color) { -// applyThemeToLayerDrawable((LayerDrawable) binding.check.getDrawable(), R.id.area, mainColor); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final var utils = ThemeUtils.of(color, requireContext()); + utils.deck.colorSelectedCheck(binding.check.getContext(), binding.check.getDrawable()); + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java index 345a2fe23..0efa7f769 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java @@ -1,6 +1,7 @@ package it.niedermann.nextcloud.deck.ui.accountswitcher; import android.net.Uri; +import android.os.Build; import android.text.TextUtils; import android.view.View; @@ -15,7 +16,7 @@ import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { @@ -34,12 +35,17 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { ); binding.accountHost.setText(Uri.parse(account.getUrl()).getHost()); Glide.with(itemView.getContext()) - .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) + .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) .into(binding.accountItemAvatar); itemView.setOnClickListener((v) -> onAccountClick.accept(account)); binding.delete.setVisibility(View.GONE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final var utils = ThemeUtils.of(account.getColor(), itemView.getContext()); + utils.deck.colorSelectedCheck(binding.currentAccountIndicator.getContext(), binding.currentAccountIndicator.getDrawable()); + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountViewModel.java new file mode 100644 index 000000000..4b2cf711a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountViewModel.java @@ -0,0 +1,32 @@ +package it.niedermann.nextcloud.deck.ui.accountswitcher; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import java.util.List; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; + +public class AccountViewModel extends BaseViewModel { + + public AccountViewModel(@NonNull Application application) { + super(application); + } + + public void saveCurrentAccount(@NonNull Account account) { + baseRepository.saveCurrentAccount(account); + } + + public LiveData<Account> getCurrentAccount() { + return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$()) + .flatMap(baseRepository::readAccount); + } + + public LiveData<List<Account>> readAccounts() { + return baseRepository.readAccounts(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java index 30f1e5d49..ba60d9666 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java @@ -5,19 +5,22 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; +import androidx.annotation.NonNull; import androidx.appcompat.widget.PopupMenu; -import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; + import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemArchivedBoardBinding; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.ui.board.DeleteBoardDialogFragment; -import it.niedermann.nextcloud.deck.ui.board.EditBoardDialogFragment; import it.niedermann.nextcloud.deck.ui.board.accesscontrol.AccessControlDialogFragment; -import it.niedermann.nextcloud.deck.util.ViewUtil; +import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; @SuppressWarnings("WeakerAccess") public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder { @@ -29,15 +32,17 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder { this.binding = binding; } - void bind(boolean isSupportedVersion, Board board, FragmentManager fragmentManager, Consumer<Board> dearchiveBoardListener) { + void bind(@NonNull Account account, @NonNull Board board, FragmentManager fragmentManager, @NonNull Consumer<Board> dearchiveBoardListener) { final Context context = itemView.getContext(); - binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, board.getColor())); + final var util = ThemeUtils.of(account.getColor(), context); + + binding.boardIcon.setImageDrawable(util.deck.getColoredBoardDrawable(context, board.getColor())); binding.boardMenu.setVisibility(View.GONE); binding.boardTitle.setText(board.getTitle()); - if (isSupportedVersion) { + if (account.getServerDeckVersionAsObject().isSupported()) { if (board.isPermissionManage()) { binding.boardMenu.setVisibility(View.VISIBLE); - binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600))); + binding.boardMenu.setImageDrawable(util.platform.tintDrawable(context, R.drawable.ic_menu, ColorRole.ON_SURFACE)); binding.boardMenu.setOnClickListener((v) -> { PopupMenu popup = new PopupMenu(context, binding.boardMenu); popup.getMenuInflater().inflate(R.menu.archived_board_menu, popup.getMenu()); @@ -49,10 +54,10 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder { final String editBoard = context.getString(R.string.edit_board); int itemId = item.getItemId(); if (itemId == SHARE_BOARD_ID) { - AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName()); + AccessControlDialogFragment.newInstance(account, board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName()); return true; } else if (itemId == R.id.edit_board) { - EditBoardDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, editBoard); + EditBoardDialogFragment.newInstance(account, board.getLocalId()).show(fragmentManager, editBoard); return true; } else if (itemId == R.id.dearchive_board) { dearchiveBoardListener.accept(board); @@ -67,8 +72,8 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder { }); } else if (board.isPermissionShare()) { binding.boardMenu.setVisibility(View.VISIBLE); - binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, ContextCompat.getColor(context, R.color.grey600))); - binding.boardMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName())); + binding.boardMenu.setImageDrawable(util.platform.tintDrawable(context, R.drawable.ic_share_grey600_18dp, ColorRole.ON_SURFACE)); + binding.boardMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(account, board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName())); } binding.boardMenu.setVisibility(View.VISIBLE); } else { 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/ArchivedBoardsActivity.java index 4a19b78e3..ac978adf8 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/ArchivedBoardsActivity.java @@ -1,16 +1,17 @@ package it.niedermann.nextcloud.deck.ui.archivedboards; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProvider; -import java.util.Collections; - +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.ActivityArchivedBinding; @@ -18,53 +19,56 @@ 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.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener; import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener; -import it.niedermann.nextcloud.deck.ui.board.EditBoardListener; +import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardListener; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.ui.theme.Themed; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; -public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteBoardListener, EditBoardListener, ArchiveBoardListener { - - private static final String BUNDLE_KEY_ACCOUNT = "accountId"; +public class ArchivedBoardsActivity extends AppCompatActivity implements Themed, DeleteBoardListener, EditBoardListener, ArchiveBoardListener { - private MainViewModel viewModel; + private static final String KEY_ACCOUNT = "account"; + private ArchivedBoardsViewModel archivedBoardsViewModel; private ActivityArchivedBinding binding; private ArchivedBoardsAdapter adapter; + private Account account; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); - final Bundle args = getIntent().getExtras(); - if (args == null || !args.containsKey(BUNDLE_KEY_ACCOUNT)) { - throw new IllegalArgumentException("Please provide at least " + BUNDLE_KEY_ACCOUNT); - } + final var args = getIntent().getExtras(); - final Account account = (Account) args.getSerializable(BUNDLE_KEY_ACCOUNT); - - if (account == null) { - throw new IllegalArgumentException(BUNDLE_KEY_ACCOUNT + " must not be null."); + if (args == null || !args.containsKey(KEY_ACCOUNT)) { + throw new IllegalArgumentException("Provide at least " + KEY_ACCOUNT); } + account = (Account) args.getSerializable(KEY_ACCOUNT); + binding = ActivityArchivedBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - viewModel = new ViewModelProvider(this).get(MainViewModel.class); - viewModel.setCurrentAccount(account); + archivedBoardsViewModel = new ViewModelProvider(this, new SyncViewModel.Factory(getApplication(), account)).get(ArchivedBoardsViewModel.class); - adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), this::onArchive); + adapter = new ArchivedBoardsAdapter(account, getSupportFragmentManager(), this::onArchive); binding.recyclerView.setAdapter(adapter); - viewModel.getBoards(account.getId(), true).observe(this, (boards) -> { - viewModel.setCurrentAccountHasArchivedBoards(boards != null && boards.size() > 0); - adapter.setBoards(boards == null ? Collections.emptyList() : boards); - }); + final var archivedBoards$ = new ReactiveLiveData<>(archivedBoardsViewModel.getArchivedBoards(account.getId())); + + archivedBoards$ + .filter(boards -> boards.size() == 0) + .distinctUntilChanged() + .observe(this, this::finish); + archivedBoards$ + .filter(boards -> boards.size() > 0) + .distinctUntilChanged() + .observe(this, adapter::setBoards); } @Override @@ -73,16 +77,9 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB this.binding = null; } - @NonNull - public static Intent createIntent(@NonNull Context context, @NonNull Account account) { - return new Intent(context, ArchivedBoardsActvitiy.class) - .putExtra(BUNDLE_KEY_ACCOUNT, account) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - @Override public void onBoardDeleted(Board board) { - viewModel.deleteBoard(board, new IResponseCallback<>() { + archivedBoardsViewModel.deleteBoard(board, new IResponseCallback<>() { @Override public void onResponse(Void response) { DeckLog.info("Successfully deleted board", board.getTitle()); @@ -90,9 +87,9 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB @Override public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + if (SyncManager.isNoOnVoidError(throwable)) { IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + showExceptionDialog(throwable, account); } } }); @@ -100,7 +97,7 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB @Override public void onUpdateBoard(FullBoard fullBoard) { - viewModel.updateBoard(fullBoard, new IResponseCallback<>() { + archivedBoardsViewModel.updateBoard(fullBoard, new IResponseCallback<>() { @Override public void onResponse(FullBoard response) { DeckLog.info("Successfully updated board", fullBoard.getBoard().getTitle()); @@ -109,14 +106,14 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB @Override public void onError(Throwable throwable) { IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + showExceptionDialog(throwable, account); } }); } @Override public void onArchive(Board board) { - viewModel.dearchiveBoard(board, new IResponseCallback<>() { + archivedBoardsViewModel.dearchiveBoard(board, new IResponseCallback<>() { @Override public void onResponse(FullBoard response) { DeckLog.info("Successfully dearchived board", response.getBoard().getTitle()); @@ -125,14 +122,14 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB @Override public void onError(Throwable throwable) { IResponseCallback.super.onError(throwable); - runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + showExceptionDialog(throwable, account); } }); } @Override - public void onClone(Board board) { - throw new IllegalStateException("Cloning boards is not available at " + ArchivedBoardsActvitiy.class.getSimpleName()); + public void onClone(@NonNull Account account, @NonNull Board board) { + throw new IllegalStateException("Cloning boards is not available at " + ArchivedBoardsActivity.class.getSimpleName()); } @Override @@ -140,4 +137,28 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB finish(); // close this activity as oppose to navigating up return true; } + + @AnyThread + private void showExceptionDialog(@NonNull Throwable throwable, @Nullable Account account) { + ExceptionDialogFragment + .newInstance(throwable, account) + .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + + @NonNull + public static Intent createIntent(@NonNull Context context, @NonNull Account account) { + return new Intent(context, ArchivedBoardsActivity.class) + .putExtra(KEY_ACCOUNT, account) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Override + public void onDismiss(DialogInterface dialog) { + + } + + @Override + public void applyTheme(int color) { + binding.emptyContentView.applyTheme(color); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java index 7bf82314c..0df6db362 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java @@ -12,21 +12,23 @@ import java.util.ArrayList; import java.util.List; import it.niedermann.nextcloud.deck.databinding.ItemArchivedBoardBinding; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Board; public class ArchivedBoardsAdapter extends RecyclerView.Adapter<ArchivedBoardViewHolder> { - private final boolean isSupportedVersion; + @NonNull + private final Account account; @NonNull private final Consumer<Board> onDearchiveListener; @NonNull private final FragmentManager fragmentManager; @NonNull - private List<Board> boards = new ArrayList<>(); + private final List<Board> boards = new ArrayList<>(); @SuppressWarnings("WeakerAccess") - public ArchivedBoardsAdapter(boolean isSupportedVersion, @NonNull FragmentManager fragmentManager, @NonNull Consumer<Board> onDearchiveListener) { - this.isSupportedVersion = isSupportedVersion; + public ArchivedBoardsAdapter(@NonNull Account account, @NonNull FragmentManager fragmentManager, @NonNull Consumer<Board> onDearchiveListener) { + this.account = account; this.fragmentManager = fragmentManager; this.onDearchiveListener = onDearchiveListener; setHasStableIds(true); @@ -45,7 +47,7 @@ public class ArchivedBoardsAdapter extends RecyclerView.Adapter<ArchivedBoardVie @Override public void onBindViewHolder(@NonNull ArchivedBoardViewHolder holder, int position) { - holder.bind(isSupportedVersion, boards.get(position), fragmentManager, onDearchiveListener); + holder.bind(account, boards.get(position), fragmentManager, onDearchiveListener); } @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsViewModel.java new file mode 100644 index 000000000..c2cfa527a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsViewModel.java @@ -0,0 +1,42 @@ +package it.niedermann.nextcloud.deck.ui.archivedboards; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import java.util.Collections; +import java.util.List; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; + +public class ArchivedBoardsViewModel extends SyncViewModel { + + public ArchivedBoardsViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + super(application, account); + } + + public LiveData<List<Board>> getArchivedBoards(long accountId) { + return new ReactiveLiveData<>(baseRepository.getBoards(accountId, true)) + .map(boards -> boards == null ? Collections.<Board>emptyList() : boards); + } + + public void updateBoard(@NonNull FullBoard board, @NonNull IResponseCallback<FullBoard> callback) { + syncManager.updateBoard(board, callback); + } + + public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback<Void> callback) { + syncManager.deleteBoard(board, callback); + } + + public void dearchiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { + syncManager.dearchiveBoard(board, callback); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java deleted file mode 100644 index ae023a7d8..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java +++ /dev/null @@ -1,88 +0,0 @@ -package it.niedermann.nextcloud.deck.ui.archivedcards; - -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 androidx.lifecycle.ViewModelProvider; - -import it.niedermann.nextcloud.deck.databinding.ActivityArchivedBinding; -import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper; -import it.niedermann.nextcloud.deck.ui.MainViewModel; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; -import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel; - -public class ArchivedCardsActvitiy extends AppCompatActivity { - - private static final String BUNDLE_KEY_ACCOUNT = "accountId"; - private static final String BUNDLE_KEY_BOARD_ID = "boardId"; - private static final String BUNDLE_KEY_CAN_EDIT = "canEdit"; - - private ActivityArchivedBinding binding; - private ArchivedCardsAdapter adapter; - private MainViewModel viewModel; - private PickStackViewModel pickStackViewModel; - - private Account account; - private long boardId; - private boolean canEdit = false; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); - - final Bundle args = getIntent().getExtras(); - if (args == null || !args.containsKey(BUNDLE_KEY_ACCOUNT) || !args.containsKey(BUNDLE_KEY_BOARD_ID)) { - throw new IllegalArgumentException("Please provide at least " + BUNDLE_KEY_ACCOUNT + " and " + BUNDLE_KEY_BOARD_ID); - } - - this.account = (Account) args.getSerializable(BUNDLE_KEY_ACCOUNT); - this.boardId = args.getLong(BUNDLE_KEY_BOARD_ID); - canEdit = args.getBoolean(BUNDLE_KEY_CAN_EDIT); - - if (this.account == null) { - throw new IllegalArgumentException(BUNDLE_KEY_ACCOUNT + " must not be null."); - } - if (this.boardId <= 0) { - throw new IllegalArgumentException(BUNDLE_KEY_BOARD_ID + " must a positive long value."); - } - - binding = ActivityArchivedBinding.inflate(getLayoutInflater()); - viewModel = new ViewModelProvider(this).get(MainViewModel.class); - pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class); - - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - - viewModel.setCurrentAccount(account); - LiveDataHelper.observeOnce(viewModel.getFullBoardById(account.getId(), boardId), this, (fullBoard) -> { - viewModel.setCurrentBoard(fullBoard.getBoard()); - - adapter = new ArchivedCardsAdapter(this, getSupportFragmentManager(), viewModel); - binding.recyclerView.setAdapter(adapter); - - viewModel.getArchivedFullCardsForBoard(account.getId(), boardId).observe(this, (fullCards) -> adapter.setCardList(fullCards)); - }); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - this.binding = null; - } - - @NonNull - public static Intent createIntent(@NonNull Context context, @NonNull Account account, long boardId, boolean currentBoardHasEditPermission) { - return new Intent(context, ArchivedCardsActvitiy.class) - .putExtra(BUNDLE_KEY_ACCOUNT, account) - .putExtra(BUNDLE_KEY_BOARD_ID, boardId) - .putExtra(BUNDLE_KEY_CAN_EDIT, currentBoardHasEditPermission) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } -} 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 deleted file mode 100644 index ea1ff4d89..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java +++ /dev/null @@ -1,68 +0,0 @@ -package it.niedermann.nextcloud.deck.ui.archivedcards; - -import android.app.Activity; -import android.view.MenuItem; - -import androidx.annotation.NonNull; -import androidx.fragment.app.FragmentManager; - -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.Card; -import it.niedermann.nextcloud.deck.model.full.FullCard; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; -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; - -public class ArchivedCardsAdapter extends CardAdapter { - - @SuppressWarnings("WeakerAccess") - public ArchivedCardsAdapter(@NonNull Activity activity, @NonNull FragmentManager fragmentManager, @NonNull MainViewModel viewModel) { - super(activity, fragmentManager, 0L, viewModel, null); - } - - @Override - public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) { - viewHolder.bind(cardList.get(position), mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), false, R.menu.archived_card_menu, this, counterMaxValue, scheme); - } - - @Override - public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) { - int itemId = menuItem.getItemId(); - if (itemId == R.id.action_card_dearchive) { - mainViewModel.dearchiveCard(fullCard, new IResponseCallback<>() { - @Override - public void onResponse(FullCard response) { - DeckLog.info("Successfully dearchived", Card.class.getSimpleName(), fullCard.getCard().getTitle()); - } - - @Override - public void onError(Throwable throwable) { - IResponseCallback.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) { - mainViewModel.deleteCard(fullCard.getCard(), new IResponseCallback<>() { - @Override - public void onResponse(Void response) { - DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle()); - } - - @Override - public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { - IResponseCallback.super.onError(throwable); - activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName())); - } - } - }); - return true; - } - return super.onCardOptionsItemSelected(menuItem, fullCard); - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java index 87a17470c..684f7b0a2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java @@ -3,23 +3,19 @@ package it.niedermann.nextcloud.deck.ui.attachments; import android.app.Application; import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; @SuppressWarnings("WeakerAccess") -public class AttachmentsViewModel extends AndroidViewModel { - - private final SyncManager syncManager; +public class AttachmentsViewModel extends BaseViewModel { public AttachmentsViewModel(@NonNull Application application) { super(application); - this.syncManager = new SyncManager(application); } public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { - return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId); + return baseRepository.getFullCardWithProjectsByLocalId(accountId, cardLocalId); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java index ff0d3e941..2e04135e4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java @@ -1,8 +1,11 @@ package it.niedermann.nextcloud.deck.ui.board; +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Board; public interface ArchiveBoardListener { void onArchive(Board board); - void onClone(Board board); + void onClone(@NonNull Account account, @NonNull Board board); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java deleted file mode 100644 index c9501ffa9..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java +++ /dev/null @@ -1,52 +0,0 @@ -package it.niedermann.nextcloud.deck.ui.board; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.model.Board; -import it.niedermann.nextcloud.deck.util.ViewUtil; - -public class BoardAdapter extends ArrayAdapter<Board> { - - @NonNull - private Context context; - - public BoardAdapter(@NonNull Context context, @NonNull Board[] boardsList) { - super(context, R.layout.item_board, boardsList); - this.context = context; - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - Board board = getItem(position); - // Check if an existing view is being reused, otherwise inflate the view - - if (convertView == null) { - convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_board, parent, false); - } - TextView boardName = convertView.findViewById(R.id.boardName); - if (board != null) { - boardName.setText(board.getTitle()); - boardName.setCompoundDrawables(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, board.getColor()), null, null, null); - } else { - DeckLog.logError(new IllegalArgumentException("board at position " + position + "is null")); - } - return convertView; - } - - @Override - public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - return getView(position, convertView, parent); - } - -}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java deleted file mode 100644 index 9d8fcdbde..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java +++ /dev/null @@ -1,13 +0,0 @@ -package it.niedermann.nextcloud.deck.ui.board; - -import androidx.annotation.ColorInt; - -import it.niedermann.nextcloud.deck.model.full.FullBoard; - -public interface EditBoardListener { - void onUpdateBoard(FullBoard fullBoard); - - default void onCreateBoard(String title, @ColorInt int color) { - // Creating board is not necessary - } -}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java index c97a1e4ba..8cd5c6548 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 @@ -9,10 +9,14 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAccessControlBinding; import it.niedermann.nextcloud.deck.databinding.ItemAccessControlOwnerBinding; @@ -21,7 +25,6 @@ import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.ui.theme.Themed; -import it.niedermann.nextcloud.deck.util.ViewUtil; public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements Themed { @@ -76,15 +79,25 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View final AccessControl ac = accessControls.get(position); switch (getItemViewType(position)) { case TYPE_HEADER: { - final OwnerViewHolder ownerHolder = (OwnerViewHolder) holder; + final var ownerHolder = (OwnerViewHolder) holder; ownerHolder.binding.owner.setText(ac.getUser().getDisplayname()); - ViewUtil.addAvatar(ownerHolder.binding.avatar, account.getUrl(), ac.getUser().getUid(), R.drawable.ic_person_grey600_24dp); + Glide.with(ownerHolder.binding.avatar.getContext()) + .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(ownerHolder.binding.avatar.getContext(), R.dimen.avatar_size), ac.getUser().getUid())) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(ownerHolder.binding.avatar); break; } case TYPE_ITEM: default: { - final AccessControlViewHolder acHolder = (AccessControlViewHolder) holder; - ViewUtil.addAvatar(acHolder.binding.avatar, account.getUrl(), ac.getUser().getUid(), R.drawable.ic_person_grey600_24dp); + final var acHolder = (AccessControlViewHolder) holder; + Glide.with(acHolder.binding.avatar.getContext()) + .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(acHolder.binding.avatar.getContext(), R.dimen.avatar_size), ac.getUser().getUid())) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(acHolder.binding.avatar); acHolder.binding.username.setText(ac.getUser().getDisplayname()); acHolder.binding.username.setCompoundDrawables(null, null, ac.getStatus() == DBStatus.LOCAL_EDITED.getId() 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 7aa0dc199..72e444fb0 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 @@ -11,11 +11,13 @@ import android.widget.AdapterView.OnItemClickListener; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import java.util.List; @@ -24,22 +26,25 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.DialogBoardShareBinding; import it.niedermann.nextcloud.deck.model.AccessControl; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; -import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.card.UserAutoCompleteAdapter; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.ui.theme.ThemedSnackbar; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; public class AccessControlDialogFragment extends DialogFragment implements AccessControlChangedListener, OnItemClickListener { - private MainViewModel viewModel; + private AccessControlViewModel accessControlViewModel; private DialogBoardShareBinding binding; + private static final String KEY_ACCOUNT = "account"; private static final String KEY_BOARD_ID = "board_id"; + private Account account; private long boardId; private UserAutoCompleteAdapter userAutoCompleteAdapter; private AccessControlAdapter adapter; @@ -49,8 +54,14 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces super.onAttach(context); final Bundle args = getArguments(); - if (args == null || !args.containsKey(KEY_BOARD_ID)) { - throw new IllegalArgumentException(KEY_BOARD_ID + " must be provided as arguments"); + if (args == null || !args.containsKey(KEY_ACCOUNT) || !args.containsKey(KEY_BOARD_ID)) { + throw new IllegalArgumentException(KEY_ACCOUNT + " and " + KEY_BOARD_ID + " must be provided as arguments"); + } + + this.account = (Account) args.getSerializable(KEY_ACCOUNT); + + if (this.account == null) { + throw new IllegalArgumentException(KEY_ACCOUNT + " must not be null"); } this.boardId = args.getLong(KEY_BOARD_ID); @@ -65,22 +76,28 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces public Dialog onCreateDialog(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); + accessControlViewModel = new ViewModelProvider(requireActivity(), new SyncViewModel.Factory(requireActivity().getApplication(), account)).get(AccessControlViewModel.class); final var dialogBuilder = new MaterialAlertDialogBuilder(requireContext()); binding = DialogBoardShareBinding.inflate(requireActivity().getLayoutInflater()); - adapter = new AccessControlAdapter(viewModel.getCurrentAccount(), this, requireContext()); + + adapter = new AccessControlAdapter(account, this, requireContext()); binding.peopleList.setAdapter(adapter); - viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (FullBoard fullBoard) -> { + accessControlViewModel.getFullBoardById(account.getId(), boardId).observe(this, (FullBoard fullBoard) -> { if (fullBoard != null) { - viewModel.getAccessControlByLocalBoardId(viewModel.getCurrentAccount().getId(), boardId).observe(this, (List<AccessControl> accessControlList) -> { + accessControlViewModel.getAccessControlByLocalBoardId(fullBoard.getAccountId(), fullBoard.getLocalId()).observe(this, (List<AccessControl> accessControlList) -> { final AccessControl ownerControl = new AccessControl(); ownerControl.setLocalId(HEADER_ITEM_LOCAL_ID); ownerControl.setUser(fullBoard.getOwner()); accessControlList.add(0, ownerControl); adapter.update(accessControlList, fullBoard.getBoard().isPermissionManage()); - userAutoCompleteAdapter = new UserAutoCompleteAdapter(requireActivity(), viewModel.getCurrentAccount(), boardId); + try { + userAutoCompleteAdapter = new UserAutoCompleteAdapter(requireActivity(), account, boardId); + } catch (NextcloudFilesAppAccountNotFoundException e) { + ExceptionDialogFragment.newInstance(e, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + // TODO Handle error + } binding.people.setAdapter(userAutoCompleteAdapter); binding.people.setOnItemClickListener(this); }); @@ -91,6 +108,7 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces dismiss(); } }); + return dialogBuilder .setTitle(R.string.share_board) .setView(binding.getRoot()) @@ -106,7 +124,7 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces @Override public void updateAccessControl(AccessControl accessControl) { - viewModel.updateAccessControl(accessControl, new IResponseCallback<>() { + accessControlViewModel.updateAccessControl(accessControl, new IResponseCallback<>() { @Override public void onResponse(AccessControl response) { DeckLog.info("Successfully updated", AccessControl.class.getSimpleName(), "for user", accessControl.getUser().getDisplayname()); @@ -115,14 +133,14 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces @Override public void onError(Throwable throwable) { IResponseCallback.super.onError(throwable); - requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + ExceptionDialogFragment.newInstance(throwable, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } }); } @Override public void deleteAccessControl(AccessControl ac) { - viewModel.deleteAccessControl(ac, new IResponseCallback<>() { + accessControlViewModel.deleteAccessControl(ac, new IResponseCallback<>() { @Override public void onResponse(Void response) { DeckLog.info("Successfully deleted access control for user", ac.getUser().getDisplayname()); @@ -130,11 +148,13 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces @Override public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + if (SyncManager.isNoOnVoidError(throwable)) { IResponseCallback.super.onError(throwable); - requireActivity().runOnUiThread(() -> ThemedSnackbar.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()); + + accessControlViewModel.getCurrentBoardColor(ac.getAccountId(), ac.getBoardId()) + .thenAcceptAsync(color -> ThemedSnackbar.make(requireView(), getString(R.string.error_revoking_ac, ac.getUser().getDisplayname()), Snackbar.LENGTH_LONG, color) + .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) + .show(), ContextCompat.getMainExecutor(requireContext())); } } }); @@ -150,7 +170,7 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces 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); - viewModel.createAccessControl(viewModel.getCurrentAccount().getId(), ac, new IResponseCallback<>() { + accessControlViewModel.createAccessControl(account, ac, new IResponseCallback<>() { @Override public void onResponse(AccessControl response) { DeckLog.info("Successfully created", AccessControl.class.getSimpleName(), "for user", user.getDisplayname()); @@ -159,7 +179,7 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces @Override public void onError(Throwable throwable) { IResponseCallback.super.onError(throwable); - requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + ExceptionDialogFragment.newInstance(throwable, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } }); binding.people.setText(""); @@ -174,10 +194,11 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces this.adapter.applyTheme(color); } - public static DialogFragment newInstance(long boardLocalId) { + public static DialogFragment newInstance(@NonNull Account account, long boardLocalId) { final DialogFragment dialog = new AccessControlDialogFragment(); final Bundle args = new Bundle(); + args.putSerializable(KEY_ACCOUNT, account); args.putLong(KEY_BOARD_ID, boardLocalId); dialog.setArguments(args); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlViewModel.java new file mode 100644 index 000000000..d4ed8ba32 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlViewModel.java @@ -0,0 +1,48 @@ +package it.niedermann.nextcloud.deck.ui.board.accesscontrol; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.AccessControl; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; + +public class AccessControlViewModel extends SyncViewModel { + + public AccessControlViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + super(application, account); + } + + public LiveData<FullBoard> getFullBoardById(long accountId, long localId) { + return baseRepository.getFullBoardById(accountId, localId); + } + + public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, long id) { + return baseRepository.getAccessControlByLocalBoardId(accountId, id); + } + + public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) { + return baseRepository.getCurrentBoardColor(accountId, boardId); + } + + public void createAccessControl(@NonNull Account account, @NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) { + syncManager.createAccessControl(account.getId(), entity, callback); + } + + public void updateAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) { + syncManager.updateAccessControl(entity, callback); + } + + public void deleteAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<Void> callback) { + syncManager.deleteAccessControl(entity, callback); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardDialogFragment.java index 9e05fbd80..78abe2b2c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardDialogFragment.java @@ -1,7 +1,8 @@ -package it.niedermann.nextcloud.deck.ui.board; +package it.niedermann.nextcloud.deck.ui.board.edit; import android.app.Dialog; import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -20,18 +21,22 @@ import java.util.Objects; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogTextColorInputBinding; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.full.FullBoard; -import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; -public class EditBoardDialogFragment extends DialogFragment { +public class EditBoardDialogFragment extends DialogFragment implements Themed { private DialogTextColorInputBinding binding; + private static final String KEY_ACCOUNT = "account"; private static final String KEY_BOARD_ID = "board_id"; private EditBoardListener editBoardListener; + private Account account; + private FullBoard fullBoard = null; @Override @@ -42,6 +47,14 @@ public class EditBoardDialogFragment extends DialogFragment { } else { throw new ClassCastException("Caller must implement " + EditBoardListener.class.getCanonicalName()); } + + final var args = getArguments(); + + if (args == null || !args.containsKey(KEY_ACCOUNT)) { + throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided"); + } + + this.account = (Account) args.getSerializable(KEY_ACCOUNT); } @NonNull @@ -53,33 +66,40 @@ public class EditBoardDialogFragment extends DialogFragment { final var builder = new MaterialAlertDialogBuilder(requireContext()) .setView(binding.getRoot()) .setNeutralButton(android.R.string.cancel, null); + final var viewModel = new ViewModelProvider(requireActivity()).get(EditBoardViewModel.class); final var args = getArguments(); - if (args != null && args.containsKey(KEY_BOARD_ID)) { + + if (args == null || (!args.containsKey(KEY_BOARD_ID) && !args.containsKey(KEY_ACCOUNT))) { + throw new IllegalArgumentException("Bundle must at least contain " + KEY_ACCOUNT + " or " + KEY_BOARD_ID); + } + + if (args.containsKey(KEY_BOARD_ID)) { + final long boardId = args.getLong(KEY_BOARD_ID); builder.setTitle(R.string.edit_board); builder.setPositiveButton(R.string.simple_save, (dialog, which) -> { this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor()); this.fullBoard.board.setTitle(binding.input.getText().toString()); this.editBoardListener.onUpdateBoard(fullBoard); }); - final var viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); - viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), args.getLong(KEY_BOARD_ID)).observe(EditBoardDialogFragment.this, fullBoard -> { + viewModel.getFullBoardById(account.getId(), boardId).observe(this, fullBoard -> { if (fullBoard.board != null) { this.fullBoard = fullBoard; - final var utils = ThemeUtils.of(fullBoard.getBoard().getColor(), requireContext()); + applyTheme(fullBoard.getBoard().getColor()); final String title = this.fullBoard.getBoard().getTitle(); binding.input.setText(title); binding.input.setSelection(title.length()); binding.colorChooser.selectColor(this.fullBoard.getBoard().getColor()); - utils.material.colorTextInputLayout(binding.inputWrapper); } }); } else { builder.setTitle(R.string.add_board); - builder.setPositiveButton(R.string.simple_add, (dialog, which) -> editBoardListener.onCreateBoard(binding.input.getText().toString(), binding.colorChooser.getSelectedColor())); + builder.setPositiveButton(R.string.simple_add, (dialog, which) -> editBoardListener.onCreateBoard(account, binding.input.getText().toString(), binding.colorChooser.getSelectedColor())); binding.colorChooser.selectColor(ContextCompat.getColor(requireContext(), R.color.board_default_color)); + + viewModel.getAccountColor(account.getId()).observe(this, this::applyTheme); } return builder.create(); @@ -94,22 +114,42 @@ public class EditBoardDialogFragment extends DialogFragment { } @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + editBoardListener.onDismiss(dialog); + } + + @Override public void onDestroy() { super.onDestroy(); this.binding = null; } - public static DialogFragment newInstance(long boardId) { + @Override + public void applyTheme(int color) { + final var utils = ThemeUtils.of(color, requireContext()); + + utils.material.colorTextInputLayout(binding.inputWrapper); + } + + public static DialogFragment newInstance(@NonNull Account account, long boardId) { final DialogFragment dialog = new EditBoardDialogFragment(); - final Bundle args = new Bundle(); + final var args = new Bundle(); + args.putSerializable(KEY_ACCOUNT, account); args.putLong(KEY_BOARD_ID, boardId); dialog.setArguments(args); return dialog; } - public static DialogFragment newInstance() { - return new EditBoardDialogFragment(); + public static DialogFragment newInstance(@NonNull Account account) { + final DialogFragment dialog = new EditBoardDialogFragment(); + + final var args = new Bundle(); + args.putSerializable(KEY_ACCOUNT, account); + dialog.setArguments(args); + + return dialog; } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardListener.java new file mode 100644 index 000000000..d261f668e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardListener.java @@ -0,0 +1,17 @@ +package it.niedermann.nextcloud.deck.ui.board.edit; + +import android.content.DialogInterface; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.full.FullBoard; + +public interface EditBoardListener extends DialogInterface.OnDismissListener { + void onUpdateBoard(FullBoard fullBoard); + + default void onCreateBoard(@NonNull Account account, String title, @ColorInt int color) { + // Creating board is not necessary + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardViewModel.java new file mode 100644 index 000000000..3b094ecf7 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardViewModel.java @@ -0,0 +1,24 @@ +package it.niedermann.nextcloud.deck.ui.board.edit; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; + +public class EditBoardViewModel extends BaseViewModel { + + public EditBoardViewModel(@NonNull Application application) { + super(application); + } + + public LiveData<FullBoard> getFullBoardById(long accountId, long localId) { + return baseRepository.getFullBoardById(accountId, localId); + } + + public LiveData<Integer> getAccountColor(long accountId) { + return baseRepository.getAccountColor(accountId); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/LabelsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/LabelsViewModel.java new file mode 100644 index 000000000..28fcee9e6 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/LabelsViewModel.java @@ -0,0 +1,42 @@ +package it.niedermann.nextcloud.deck.ui.board.managelabels; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; + +public class LabelsViewModel extends SyncViewModel { + + public LabelsViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + super(application, account); + } + + public LiveData<FullBoard> getFullBoardById(Long boardLocalId) { + return new ReactiveLiveData<>(baseRepository.getFullBoardById(account.getId(), boardLocalId)); + } + + public void updateLabel(@NonNull Label label, @NonNull IResponseCallback<Label> callback) { + syncManager.updateLabel(label, callback); + } + + public void createLabel(@NonNull Label label, long localBoardId, @NonNull IResponseCallback<Label> callback) { + syncManager.createLabel(account.getId(), label, localBoardId, callback); + } + + public void deleteLabel(@NonNull Label label, @NonNull IResponseCallback<Void> callback) { + syncManager.deleteLabel(label, callback); + } + + public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback<Integer> callback) { + baseRepository.countCardsWithLabel(localLabelId, callback); + } +} 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 ca3547ae7..677776fce 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 @@ -21,22 +21,25 @@ import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.DialogBoardManageLabelsBinding; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; -import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.theme.DeleteAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; public class ManageLabelsDialogFragment extends ThemedDialogFragment implements ManageLabelListener, EditLabelListener { - private MainViewModel viewModel; + private LabelsViewModel labelsViewModel; private DialogBoardManageLabelsBinding binding; private ManageLabelsAdapter adapter; private String[] colors; + private static final String KEY_ACCOUNT = "account"; private static final String KEY_BOARD_ID = "board_id"; + private Account account; private long boardId; @Override @@ -44,8 +47,14 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements super.onAttach(context); final Bundle args = getArguments(); - if (args == null || !args.containsKey(KEY_BOARD_ID)) { - throw new IllegalArgumentException(KEY_BOARD_ID + " must be provided as arguments"); + if (args == null || !args.containsKey(KEY_ACCOUNT) || !args.containsKey(KEY_BOARD_ID)) { + throw new IllegalArgumentException(KEY_ACCOUNT + " and " + KEY_BOARD_ID + " must be provided as arguments"); + } + + this.account = (Account) args.getSerializable(KEY_ACCOUNT); + + if (this.account == null) { + throw new IllegalStateException(KEY_ACCOUNT + " must not be null"); } this.boardId = args.getLong(KEY_BOARD_ID); @@ -60,13 +69,13 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements public Dialog onCreateDialog(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); + labelsViewModel = new ViewModelProvider(requireActivity(), new SyncViewModel.Factory(this.requireActivity().getApplication(), account)).get(LabelsViewModel.class); final var dialogBuilder = new MaterialAlertDialogBuilder(requireContext()); binding = DialogBoardManageLabelsBinding.inflate(requireActivity().getLayoutInflater()); colors = getResources().getStringArray(R.array.board_default_colors); adapter = new ManageLabelsAdapter(this, requireContext()); binding.labels.setAdapter(adapter); - viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (fullBoard) -> { + labelsViewModel.getFullBoardById(boardId).observe(this, fullBoard -> { if (fullBoard == null) { throw new IllegalStateException("FullBoard should not be null"); } @@ -80,7 +89,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements label.setTitle(binding.addLabelTitle.getText().toString()); label.setColor(colors[new Random().nextInt(colors.length)]); - viewModel.createLabel(viewModel.getCurrentAccount().getId(), label, boardId, new IResponseCallback<>() { + labelsViewModel.createLabel(label, boardId, new IResponseCallback<>() { @Override public void onResponse(Label response) { requireActivity().runOnUiThread(() -> { @@ -124,10 +133,11 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements utils.material.colorTextInputLayout(binding.addLabelTitleWrapper); } - public static DialogFragment newInstance(long boardLocalId) { + public static DialogFragment newInstance(@NonNull Account account, long boardLocalId) { final DialogFragment dialog = new ManageLabelsDialogFragment(); final Bundle args = new Bundle(); + args.putSerializable(KEY_ACCOUNT, account); args.putLong(KEY_BOARD_ID, boardLocalId); dialog.setArguments(args); @@ -136,7 +146,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements @Override public void requestDelete(@NonNull Label label) { - viewModel.countCardsWithLabel(label.getLocalId(), (count) -> requireActivity().runOnUiThread(() -> { + labelsViewModel.countCardsWithLabel(label.getLocalId(), count -> requireActivity().runOnUiThread(() -> { if (count > 0) { new DeleteAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.delete_something, label.getTitle())) @@ -151,7 +161,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements } private void deleteLabel(@NonNull Label label) { - viewModel.deleteLabel(label, new IResponseCallback<>() { + labelsViewModel.deleteLabel(label, new IResponseCallback<>() { @Override public void onResponse(Void response) { DeckLog.info("Successfully deleted label", label.getTitle()); @@ -159,7 +169,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements @Override public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + if (SyncManager.isNoOnVoidError(throwable)) { IResponseCallback.super.onError(throwable); toastFromThread(throwable.getLocalizedMessage()); } @@ -174,7 +184,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements @Override public void onLabelUpdated(@NonNull Label label) { - viewModel.updateLabel(label, new IResponseCallback<>() { + labelsViewModel.updateLabel(label, new IResponseCallback<>() { @Override public void onResponse(Label label) { DeckLog.info("Successfully update label", label.getTitle()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java index 34a9157ff..fb2ee19b2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java @@ -13,11 +13,11 @@ import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; -import androidx.core.graphics.drawable.DrawableCompat; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.google.android.material.card.MaterialCardView; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; import org.jetbrains.annotations.Contract; @@ -31,12 +31,12 @@ import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.ui.theme.DeckViewThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.util.AttachmentUtil; import it.niedermann.nextcloud.deck.util.DateUtil; import it.niedermann.nextcloud.deck.util.MimeTypeUtil; -import it.niedermann.nextcloud.deck.util.ViewUtil; import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; -import scheme.Scheme; public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder { @@ -48,7 +48,7 @@ public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder { * Removes all {@link OnClickListener} and {@link OnLongClickListener} */ @CallSuper - public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @NonNull Scheme scheme) { + public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @Nullable ThemeUtils utils) { final var context = itemView.getContext(); bindCardClickListener(null); @@ -57,7 +57,9 @@ public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder { getCardMenu().setVisibility(hasEditPermission ? View.VISIBLE : View.GONE); getCardTitle().setText(fullCard.getCard().getTitle().trim()); - DrawableCompat.setTint(getNotSyncedYet().getDrawable(), scheme.getOnPrimaryContainer()); + if (utils != null) { + utils.platform.colorImageView(getNotSyncedYet(), ColorRole.PRIMARY); + } // TODO should be discussed with UX // utils.material.themeCardView(getCard()); @@ -111,9 +113,8 @@ public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder { } private static void setupDueDate(@NonNull TextView cardDueDate, @NonNull Card card) { - final var context = cardDueDate.getContext(); - cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().toEpochMilli())); - ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate().atZone(ZoneId.systemDefault()).toLocalDate()); + cardDueDate.setText(DateUtil.getRelativeDateTimeString(cardDueDate.getContext(), card.getDueDate().toEpochMilli())); + DeckViewThemeUtils.themeDueDate(cardDueDate, card.getDueDate().atZone(ZoneId.systemDefault()).toLocalDate()); } protected static void setupCoverImages(@NonNull Account account, @NonNull ViewGroup coverImagesHolder, @NonNull FullCard fullCard, int maxCoverImagesCount) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardActionListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardActionListener.java new file mode 100644 index 000000000..fe5050eba --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardActionListener.java @@ -0,0 +1,23 @@ +package it.niedermann.nextcloud.deck.ui.card; + +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; + +public interface CardActionListener { + + void onArchive(@NonNull FullCard fullCard); + + void onDelete(@NonNull FullCard fullCard); + + void onAssignCurrentUser(@NonNull FullCard fullCard); + + void onUnassignCurrentUser(@NonNull FullCard fullCard); + + void onMove(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard); + + void onShareLink(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard); + + void onShareContent(@NonNull FullCard fullCard); +} 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 1d003ec78..38388615b 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,91 +1,68 @@ package it.niedermann.nextcloud.deck.ui.card; import static androidx.preference.PreferenceManager.getDefaultSharedPreferences; -import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN; import android.app.Activity; import android.content.ClipData; -import android.content.Intent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; 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.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding; import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding; import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultOnlyTitleBinding; -import it.niedermann.nextcloud.deck.model.Card; -import it.niedermann.nextcloud.deck.model.Stack; +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.persistence.sync.SyncManager; -import it.niedermann.nextcloud.deck.ui.MainViewModel; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; -import it.niedermann.nextcloud.deck.ui.theme.Themed; -import it.niedermann.nextcloud.deck.util.CardUtil; -import scheme.Scheme; -public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener, Themed { +public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener { - private final ExecutorService executor; private final boolean compactMode; + @Nullable + private Account account; + @Nullable + private FullBoard fullBoard; @NonNull - protected final MainViewModel mainViewModel; - @NonNull - protected final FragmentManager fragmentManager; - private final long stackId; - @NonNull - protected final Activity activity; + private final Activity activity; @Nullable private final SelectCardListener selectCardListener; @NonNull - protected List<FullCard> cardList = new ArrayList<>(); + private final CardActionListener cardActionListener; @NonNull - protected String counterMaxValue; + private final List<FullCard> cardList = new ArrayList<>(); @NonNull - protected Scheme scheme; - @StringRes - private final int shareLinkRes; - protected final int maxCoverImages; - - public CardAdapter(@NonNull Activity activity, @NonNull FragmentManager fragmentManager, long stackId, @NonNull MainViewModel mainViewModel, @Nullable SelectCardListener selectCardListener) { - this(activity, fragmentManager, stackId, mainViewModel, selectCardListener, Executors.newSingleThreadExecutor()); - } - - private CardAdapter(@NonNull Activity activity, @NonNull FragmentManager fragmentManager, long stackId, @NonNull MainViewModel mainViewModel, @Nullable SelectCardListener selectCardListener, @NonNull ExecutorService executor) { + private final String counterMaxValue; + @Nullable + private ThemeUtils utils; + private final int maxCoverImages; + + public CardAdapter( + @NonNull Activity activity, + @NonNull CardActionListener cardActionListener, + @Nullable SelectCardListener selectCardListener + ) { this.activity = activity; this.counterMaxValue = this.activity.getString(R.string.counter_max_value); - this.fragmentManager = fragmentManager; - this.shareLinkRes = mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().getShareLinkResource(); - this.stackId = stackId; - this.mainViewModel = mainViewModel; + this.cardActionListener = cardActionListener; this.selectCardListener = selectCardListener; - this.scheme = ThemeUtils.createScheme(ContextCompat.getColor(this.activity, R.color.defaultBrand), this.activity); this.compactMode = getDefaultSharedPreferences(this.activity).getBoolean(this.activity.getString(R.string.pref_key_compact), false); - this.maxCoverImages = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.pref_key_cover_images), true) - ? activity.getResources().getInteger(R.integer.max_cover_images) - : 0; + this.maxCoverImages = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.pref_key_cover_images), true) ? activity.getResources().getInteger(R.integer.max_cover_images) : 0; setHasStableIds(true); - this.executor = executor; } @Override @@ -123,15 +100,22 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im @Override public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) { + if (account == null) { + throw new IllegalStateException("Tried to bind viewholder while account is still null"); + } + if (fullBoard == null) { + throw new IllegalStateException("Tried to bind viewholder while fullBoard is still null"); + } + @NonNull final var fullCard = cardList.get(position); - viewHolder.bind(fullCard, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), mainViewModel.currentBoardHasEditPermission(), R.menu.card_menu, this, counterMaxValue, scheme); + viewHolder.bind(fullCard, account, fullBoard.getBoard().getId(), fullBoard.board.isPermissionEdit(), R.menu.card_menu, this, counterMaxValue, utils); // Only enable details view if there is no one waiting for selecting a card. viewHolder.bindCardClickListener((v) -> { if (selectCardListener == null) { - activity.startActivity(EditActivity.createEditCardIntent(activity, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId())); + activity.startActivity(EditActivity.createEditCardIntent(activity, account, fullBoard.getBoard().getLocalId(), fullCard.getLocalId())); } else { - selectCardListener.onCardSelected(fullCard); + selectCardListener.onCardSelected(fullCard, fullBoard.getLocalId()); } }); @@ -139,7 +123,7 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im if (selectCardListener == null) { viewHolder.bindCardLongClickListener((v) -> { DeckLog.log("Starting drag and drop"); - v.startDrag(ClipData.newPlainText("cardid", String.valueOf(fullCard.getLocalId())), + v.startDragAndDrop(ClipData.newPlainText("cardid", String.valueOf(fullCard.getLocalId())), new View.DragShadowBuilder(v), new DraggedItemLocalState<>(fullCard, viewHolder.getDraggable(), this, position), 0 @@ -177,80 +161,51 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im notifyItemRemoved(position); } - public void setCardList(@NonNull List<FullCard> cardList) { - this.cardList.clear(); - this.cardList.addAll(cardList); - notifyDataSetChanged(); + public void setAccount(@NonNull Account account) { + this.account = account; } - @Override - public void applyTheme(int color) { - this.scheme = ThemeUtils.createScheme(color, activity); + public void setFullBoard(@NonNull FullBoard fullBoard) { + this.fullBoard = fullBoard; + } + + public void setCardList(@NonNull List<FullCard> cardList, @ColorInt int color) { + this.utils = ThemeUtils.of(color, activity); + this.cardList.clear(); + this.cardList.addAll(cardList); notifyDataSetChanged(); } @Override public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) { final int itemId = menuItem.getItemId(); - final var account = mainViewModel.getCurrentAccount(); if (itemId == R.id.share_link) { - final var 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() + activity.getString(shareLinkRes, mainViewModel.getCurrentBoardRemoteId(), fullCard.getCard().getId())); - activity.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle())); + if (fullBoard == null) { + DeckLog.warn("Can not share link to card", fullCard.getCard().getTitle(), "because fullBoard is null"); + return false; + } + cardActionListener.onShareLink(fullBoard, fullCard); return true; } else if (itemId == R.id.share_content) { - final var 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())); + cardActionListener.onShareContent(fullCard); } else if (itemId == R.id.action_card_assign) { - executor.submit(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())); + cardActionListener.onAssignCurrentUser(fullCard); return true; } else if (itemId == R.id.action_card_unassign) { - executor.submit(() -> mainViewModel.unassignUserFromCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())); + cardActionListener.onUnassignCurrentUser(fullCard); 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(), CardUtil.cardHasCommentsOrAttachments(fullCard)) - .show(fragmentManager, MoveCardDialogFragment.class.getSimpleName()); + if (fullBoard == null) { + DeckLog.warn("Can not move card", fullCard.getCard().getTitle(), "because fullBoard is null"); + return false; + } + cardActionListener.onMove(fullBoard, fullCard); return true; } else if (itemId == R.id.action_card_archive) { - mainViewModel.archiveCard(fullCard, new IResponseCallback<>() { - @Override - public void onResponse(FullCard response) { - DeckLog.info("Successfully archived", Card.class.getSimpleName(), fullCard.getCard().getTitle()); - } - - @Override - public void onError(Throwable throwable) { - IResponseCallback.super.onError(throwable); - activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName())); - } - }); + cardActionListener.onArchive(fullCard); return true; } else if (itemId == R.id.action_card_delete) { - mainViewModel.deleteCard(fullCard.getCard(), new IResponseCallback<>() { - @Override - public void onResponse(Void response) { - DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle()); - } - - @Override - public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { - IResponseCallback.super.onError(throwable); - activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName())); - } - } - }); + cardActionListener.onDelete(fullCard); return true; } 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 68c95bfb6..89462c997 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 @@ -5,6 +5,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.viewpager2.adapter.FragmentStateAdapter; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.ui.card.activities.CardActivityFragment; import it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentsFragment; import it.niedermann.nextcloud.deck.ui.card.comments.CardCommentsFragment; @@ -12,10 +13,15 @@ import it.niedermann.nextcloud.deck.ui.card.details.CardDetailsFragment; public class CardTabAdapter extends FragmentStateAdapter { + private final Account account; private boolean hasCommentsAbility = false; - public CardTabAdapter(final FragmentActivity fa) { + public CardTabAdapter( + @NonNull final FragmentActivity fa, + @NonNull final Account account + ) { super(fa); + this.account = account; } @NonNull @@ -23,12 +29,12 @@ public class CardTabAdapter extends FragmentStateAdapter { public Fragment createFragment(int position) { switch (position) { case 0: - return CardDetailsFragment.newInstance(); + return CardDetailsFragment.newInstance(account); case 1: return CardAttachmentsFragment.newInstance(); case 2: return hasCommentsAbility - ? CardCommentsFragment.newInstance() + ? CardCommentsFragment.newInstance(account) : CardActivityFragment.newInstance(); case 3: if (hasCommentsAbility) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java index fd93035de..cf947c90b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java @@ -18,7 +18,7 @@ import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.full.FullCard; -import scheme.Scheme; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; public class CompactCardViewHolder extends AbstractCardViewHolder { private final ItemCardCompactBinding binding; @@ -35,8 +35,8 @@ public class CompactCardViewHolder extends AbstractCardViewHolder { * Removes all {@link OnClickListener} and {@link OnLongClickListener} */ @Override - public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @NonNull Scheme scheme) { - super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, scheme); + public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @Nullable ThemeUtils utils) { + super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, utils); setupCoverImages(account, binding.coverImages, fullCard, Math.min(maxCoverImagesCount, 1)); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java index 2641bf3ad..dc6e5a956 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java @@ -18,7 +18,7 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.full.FullCard; -import scheme.Scheme; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; public class DefaultCardViewHolder extends AbstractCardViewHolder { private final ItemCardDefaultBinding binding; @@ -35,8 +35,8 @@ public class DefaultCardViewHolder extends AbstractCardViewHolder { * Removes all {@link OnClickListener} and {@link OnLongClickListener} */ @Override - public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @NonNull Scheme scheme) { - super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, scheme); + public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @Nullable ThemeUtils utils) { + super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, utils); final var context = itemView.getContext(); 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 895da6bd1..b49f38b0c 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 @@ -1,13 +1,9 @@ package it.niedermann.nextcloud.deck.ui.card; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; - import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.text.Editable; import android.text.InputFilter; -import android.text.TextWatcher; import android.view.Menu; import android.view.MenuItem; @@ -21,17 +17,20 @@ import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; 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.full.FullCard; import it.niedermann.nextcloud.deck.model.ocs.Version; -import it.niedermann.nextcloud.deck.ui.MainActivity; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.ui.main.MainActivity; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.util.CardUtil; +import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher; public class EditActivity extends AppCompatActivity { @@ -115,7 +114,12 @@ public class EditActivity extends AppCompatActivity { if (account == null) { throw new IllegalArgumentException(BUNDLE_KEY_ACCOUNT + " must not be null."); } - viewModel.setAccount(account); + + try { + viewModel.setAccount(account); + } catch (NextcloudFilesAppAccountNotFoundException e) { + throw new RuntimeException(e); + } final long cardLocalId = args.getLong(BUNDLE_KEY_CARD_LOCAL_ID); if (cardLocalId <= 0L) { @@ -127,25 +131,27 @@ public class EditActivity extends AppCompatActivity { throw new IllegalArgumentException(BUNDLE_KEY_BOARD_LOCAL_ID + " must be a positive integer but was " + boardLocalId); } - observeOnce(viewModel.getFullBoardById(account.getId(), boardLocalId), EditActivity.this, (fullBoard -> { - viewModel.setBoardColor(fullBoard.getBoard().getColor()); - viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit()); - invalidateOptionsMenu(); - observeOnce(viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardLocalId), EditActivity.this, (fullCard) -> { - if (fullCard == null) { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.card_not_found) - .setMessage(R.string.card_not_found_message) - .setPositiveButton(R.string.simple_close, (a, b) -> super.finish()) - .show(); - } else { - viewModel.initializeExistingCard(boardLocalId, fullCard, account.getServerDeckVersionAsObject().isSupported()); + new ReactiveLiveData<>(viewModel.getFullBoardById(account.getId(), boardLocalId)) + .observeOnce(EditActivity.this, fullBoard -> { + viewModel.setBoardColor(fullBoard.getBoard().getColor()); + viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit()); invalidateOptionsMenu(); - setupViewPager(); - setupTitle(); - } - }); - })); + new ReactiveLiveData<>(viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardLocalId)) + .observeOnce(EditActivity.this, fullCard -> { + if (fullCard == null) { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.card_not_found) + .setMessage(R.string.card_not_found_message) + .setPositiveButton(R.string.simple_close, (a, b) -> super.finish()) + .show(); + } else { + viewModel.initializeExistingCard(boardLocalId, fullCard, account.getServerDeckVersionAsObject().isSupported()); + invalidateOptionsMenu(); + setupViewPager(account); + setupTitle(); + } + }); + }); DeckLog.verbose("Finished loading intent data: { accountId =", viewModel.getAccount().getId(), "cardId =", cardLocalId, "}"); } @@ -212,11 +218,11 @@ public class EditActivity extends AppCompatActivity { } } - private void setupViewPager() { + private void setupViewPager(@NonNull Account account) { binding.tabLayout.removeAllTabs(); binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); - final var adapter = new CardTabAdapter(this); + final var adapter = new CardTabAdapter(this, account); final var mediator = new TabLayoutMediator(binding.tabLayout, binding.pager, (tab, position) -> { tab.setIcon(viewModel.hasCommentsAbility() ? tabIconsWithComments[position] @@ -243,20 +249,7 @@ public class EditActivity extends AppCompatActivity { binding.title.setFilters(new InputFilter[]{new InputFilter.LengthFilter(viewModel.getAccount().getServerDeckVersionAsObject().getCardTitleMaxLength())}); if (viewModel.canEdit()) { binding.title.setHint(R.string.edit); - binding.title.addTextChangedListener(new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - viewModel.getFullCard().getCard().setTitle(binding.title.getText().toString()); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable s) { - } - }); + binding.title.addTextChangedListener(new OnTextChangedWatcher(s -> viewModel.getFullCard().getCard().setTitle(binding.title.getText().toString()))); } else { binding.title.setEnabled(false); } 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 f4c652f96..7a553f80c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java @@ -1,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.card; import static androidx.lifecycle.Transformations.distinctUntilChanged; -import static androidx.lifecycle.Transformations.switchMap; import android.app.Application; import android.content.SharedPreferences; @@ -10,14 +9,17 @@ import android.text.TextUtils; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.preference.PreferenceManager; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + import java.io.File; import java.util.List; +import java.util.concurrent.CompletableFuture; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; @@ -32,9 +34,10 @@ import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; @SuppressWarnings("WeakerAccess") -public class EditCardViewModel extends AndroidViewModel { +public class EditCardViewModel extends BaseViewModel { private SyncManager syncManager; private Account account; @@ -51,7 +54,6 @@ public class EditCardViewModel extends AndroidViewModel { public EditCardViewModel(@NonNull Application application) { super(application); - this.syncManager = new SyncManager(application); this.boardColor$.setValue(ContextCompat.getColor(application, R.color.primary)); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application); } @@ -60,19 +62,22 @@ public class EditCardViewModel extends AndroidViewModel { * The result {@link LiveData} will emit <code>true</code> if the preview mode is enabled and <code>false</code> if the edit mode is enabled. */ public LiveData<Boolean> getDescriptionMode() { - return distinctUntilChanged(switchMap(distinctUntilChanged(new SharedPreferenceBooleanLiveData(sharedPreferences, getApplication().getString(R.string.shared_preference_description_preview), false)), (isPreview) -> { - // When we are in preview mode but the description of the card is empty, we explicitly switch to the edit mode - final var fullCard = getFullCard(); - if (fullCard == null) { - throw new IllegalStateException("Description mode must be queried after initializing " + EditCardViewModel.class.getSimpleName() + " with a card."); - } - if (isPreview && TextUtils.isEmpty(fullCard.getCard().getDescription())) { - descriptionIsPreview.setValue(false); - } else { - descriptionIsPreview.setValue(isPreview); - } - return descriptionIsPreview; - })); + return new ReactiveLiveData<>(new SharedPreferenceBooleanLiveData(sharedPreferences, getApplication().getString(R.string.shared_preference_description_preview), false)) + .distinctUntilChanged() + .flatMap(isPreview -> { + // When we are in preview mode but the description of the card is empty, we explicitly switch to the edit mode + final var fullCard = getFullCard(); + if (fullCard == null) { + throw new IllegalStateException("Description mode must be queried after initializing " + EditCardViewModel.class.getSimpleName() + " with a card."); + } + if (isPreview && TextUtils.isEmpty(fullCard.getCard().getDescription())) { + descriptionIsPreview.setValue(false); + } else { + descriptionIsPreview.setValue(isPreview); + } + return descriptionIsPreview; + }) + .distinctUntilChanged(); } /** @@ -108,12 +113,16 @@ public class EditCardViewModel extends AndroidViewModel { this.isSupportedVersion = isSupportedVersion; } - public void setAccount(@NonNull Account account) { + public void setAccount(@NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { this.account = account; - this.syncManager = new SyncManager(getApplication(), account.getName()); + this.syncManager = new SyncManager(getApplication(), account); hasCommentsAbility = account.getServerDeckVersionAsObject().supportsComments(); } + public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) { + return baseRepository.getCurrentBoardColor(accountId, boardId); + } + public boolean hasChanges() { if (fullCard == null) { DeckLog.info("Can not check for changes because fullCard is null → assuming no changes have been made yet."); @@ -155,7 +164,7 @@ public class EditCardViewModel extends AndroidViewModel { } public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) { - return syncManager.getFullBoardById(accountId, localId); + return baseRepository.getFullBoardById(accountId, localId); } public void createLabel(long accountId, Label label, long localBoardId, @NonNull IResponseCallback<Label> callback) { @@ -163,7 +172,7 @@ public class EditCardViewModel extends AndroidViewModel { } public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { - return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId); + return baseRepository.getFullCardWithProjectsByLocalId(accountId, cardLocalId); } /** @@ -186,10 +195,10 @@ public class EditCardViewModel extends AndroidViewModel { } public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) { - return syncManager.getCardByRemoteID(accountId, remoteId); + return baseRepository.getCardByRemoteID(accountId, remoteId); } public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { - return syncManager.getBoardByRemoteId(accountId, remoteId); + return baseRepository.getBoardByRemoteId(accountId, 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 f8fd967aa..decca0630 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 @@ -1,50 +1,89 @@ package it.niedermann.nextcloud.deck.ui.card; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; - +import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Color; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Filter; import androidx.activity.ComponentActivity; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + import java.util.Collection; +import java.util.List; import java.util.Random; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteLabelBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter; public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> { - @Nullable - private Label createLabel; - private String lastFilterText; - private boolean canManage = false; - public LabelAutoCompleteAdapter(@NonNull ComponentActivity activity, long accountId, long boardId, long cardId) { - super(activity, accountId, boardId, cardId); + @NonNull + protected final Context context; + @ColorInt + private final int createLabelColor; + private final ReactiveLiveData<Boolean> canManage$; + + public LabelAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) throws NextcloudFilesAppAccountNotFoundException { + super(activity, account, boardId); + this.context = activity; final String[] colors = activity.getResources().getStringArray(R.array.board_default_colors); - @ColorInt int createLabelColor = Color.parseColor(colors[new Random().nextInt(colors.length)]); - observeOnce(syncManager.getFullBoardById(accountId, boardId), activity, (fullBoard) -> { - if (fullBoard.getBoard().isPermissionManage()) { - canManage = true; - createLabel = new Label(); - createLabel.setLocalId(ITEM_CREATE); - createLabel.setBoardId(boardId); - createLabel.setAccountId(accountId); - createLabel.setColor(createLabelColor); - } - }); + createLabelColor = Color.parseColor(colors[new Random().nextInt(colors.length)]); + + canManage$ = new ReactiveLiveData<>(syncManager.getFullBoardById(account.getId(), boardId)) + .map(FullBoard::getBoard) + .map(Board::isPermissionManage); + + constraint$ + .flatMap(constraint -> TextUtils.isEmpty(constraint) + ? syncManager.findProposalsForLabelsToAssign(account.getId(), boardId, cardId) + : syncManager.searchNotYetAssignedLabelsByTitle(account, boardId, cardId, constraint)) + .map(this::filterExcluded) + .flatMap(this::addCreateLabelIfNeeded) + .distinctUntilChanged() + .observe(activity, this::publishResults); + } + + private ReactiveLiveData<List<Label>> addCreateLabelIfNeeded(@NonNull List<Label> labels) { + return canManage$ + .combineWith(() -> constraint$) + .map(args -> { + final var canManage = args.first; + final var constraint = args.second; + if (canManage && !TextUtils.isEmpty(constraint) && !labelTitleIsPresent(labels, constraint)) { + labels.add(createLabel(constraint)); + } + return labels; + }); + } + + private boolean labelTitleIsPresent(@NonNull Collection<Label> labels, @NonNull CharSequence title) { + return labels.stream().map(Label::getTitle).anyMatch(title::equals); + } + + @NonNull + private Label createLabel(String title) { + final var label = new Label(); + label.setLocalId(null); + label.setBoardId(boardId); + label.setAccountId(account.getId()); + label.setTitle(title); + label.setColor(createLabelColor); + return label; } @Override @@ -61,11 +100,15 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> { final int labelColor = label.getColor(); final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); - binding.label.setText(label.getTitle()); + if (label.getLocalId() == null) { + binding.label.setText(String.format(context.getString(R.string.label_add, label.getTitle()))); + } else { + binding.label.setText(label.getTitle()); + } binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); binding.label.setTextColor(color); - if (ITEM_CREATE == label.getLocalId()) { + if (label.getLocalId() == null) { final var plusIcon = DrawableCompat.wrap(ContextCompat.getDrawable(binding.label.getContext(), R.drawable.ic_plus)); DrawableCompat.setTint(plusIcon, color); binding.label.setChipIcon(plusIcon); @@ -75,49 +118,4 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> { return binding.getRoot(); } - - @Override - public Filter getFilter() { - return new AutoCompleteFilter() { - @Override - protected FilterResults performFiltering(CharSequence constraint) { - if (constraint != null) { - lastFilterText = constraint.toString(); - activity.runOnUiThread(() -> { - final var liveData = constraint.toString().trim().length() > 0 - ? syncManager.searchNotYetAssignedLabelsByTitle(accountId, boardId, cardId, constraint.toString()) - : syncManager.findProposalsForLabelsToAssign(accountId, boardId, cardId); - observeOnce(liveData, activity, (labels -> { - labels.removeAll(itemsToExclude); - final boolean constraintLengthGreaterZero = constraint.toString().trim().length() > 0; - if (canManage && constraintLengthGreaterZero && !labelTitleIsPresent(labels, constraint)) { - if (createLabel == null) { - throw new IllegalStateException("Owner has right to edit card, but createLabel is null"); - } - createLabel.setTitle(String.format(activity.getString(R.string.label_add), constraint)); - labels.add(createLabel); - } - filterResults.values = labels; - filterResults.count = labels.size(); - publishResults(constraint, filterResults); - })); - }); - } - return filterResults; - } - }; - } - - private static boolean labelTitleIsPresent(@NonNull Collection<Label> labels, @NonNull CharSequence title) { - for (final var label : labels) { - if (label.getTitle().contentEquals(title)) { - return true; - } - } - return false; - } - - public String getLastFilterText() { - return this.lastFilterText; - } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java index fd6231029..56ea190c6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java @@ -1,13 +1,12 @@ package it.niedermann.nextcloud.deck.ui.card; +import static androidx.core.content.ContextCompat.getMainExecutor; import static androidx.lifecycle.Transformations.distinctUntilChanged; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,7 +14,6 @@ import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.Toast; -import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; @@ -23,36 +21,32 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.DialogNewCardBinding; import it.niedermann.nextcloud.deck.exceptions.OfflineException; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.preparecreate.PrepareCreateViewModel; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; +import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher; -public class NewCardDialog extends DialogFragment implements DialogInterface.OnClickListener { +public class NewCardDialog extends ThemedDialogFragment implements DialogInterface.OnClickListener { + private NewCardViewModel newCardViewModel; private PrepareCreateViewModel viewModel; - private CreateCardListener createCardListener; + private DialogNewCardBinding binding; + private final MutableLiveData<Boolean> isPending = new MutableLiveData<>(false); - private static final String ARG_ACCOUNT = "account"; - private static final String ARG_BOARD_LOCAL_ID = "board_id"; - private static final String ARG_STACK_LOCAL_ID = "stack_id"; - private static final String ARG_BRAND = "brand"; + private static final String KEY_ACCOUNT = "account"; + private static final String KEY_BOARD_ID = "board_id"; + private static final String KEY_STACK_ID = "stack_id"; private Account account; - private long boardLocalId; - private long stackLocalId; - @ColorInt - private int color; - - private DialogNewCardBinding binding; - private final MutableLiveData<Boolean> isPending = new MutableLiveData<>(false); @Override public void onAttach(@NonNull Context context) { @@ -65,13 +59,13 @@ public class NewCardDialog extends DialogFragment implements DialogInterface.OnC } final var args = getArguments(); - if (args == null) { - throw new IllegalArgumentException("Provide " + ARG_ACCOUNT + ", " + ARG_BOARD_LOCAL_ID + " and " + ARG_STACK_LOCAL_ID); + + if (args == null || !args.containsKey(KEY_ACCOUNT)) { + throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided"); } - account = (Account) args.getSerializable(ARG_ACCOUNT); - boardLocalId = args.getLong(ARG_BOARD_LOCAL_ID); - stackLocalId = args.getLong(ARG_STACK_LOCAL_ID); - color = args.getInt(ARG_BRAND); + this.account = (Account) getArguments().getSerializable(KEY_ACCOUNT); + + newCardViewModel = new ViewModelProvider(requireActivity(), new SyncViewModel.Factory(requireActivity().getApplication(), account)).get(NewCardViewModel.class); viewModel = new ViewModelProvider(requireActivity()).get(PrepareCreateViewModel.class); } @@ -95,34 +89,16 @@ public class NewCardDialog extends DialogFragment implements DialogInterface.OnC dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> onClick(dialog, DialogInterface.BUTTON_NEGATIVE)); }); - final var utils = ThemeUtils.of(color, requireContext()); - - utils.material.colorTextInputLayout(binding.inputWrapper); - utils.platform.colorCircularProgressBar(binding.progressCircular); - - binding.input.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Nothing to do - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - final boolean inputIsValid = inputIsValid(binding.input.getText()); - if (inputIsValid) { - binding.inputWrapper.setError(null); - } - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(inputIsValid); - dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(inputIsValid); - } - - @Override - public void afterTextChanged(Editable s) { - // Nothing to do + binding.input.addTextChangedListener(new OnTextChangedWatcher(s -> { + final boolean inputIsValid = inputIsValid(binding.input.getText()); + if (inputIsValid) { + binding.inputWrapper.setError(null); } - }); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(inputIsValid); + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(inputIsValid); + })); - distinctUntilChanged(isPending).observe(this, (isPending) -> { + distinctUntilChanged(isPending).observe(this, isPending -> { if (isPending) { binding.inputWrapper.setVisibility(View.INVISIBLE); binding.progressCircular.setVisibility(View.VISIBLE); @@ -180,35 +156,30 @@ public class NewCardDialog extends DialogFragment implements DialogInterface.OnC isPending.setValue(true); final var currentUserInput = binding.input.getText(); if (inputIsValid(currentUserInput)) { - final var fullCard = viewModel.createFullCard(account.getServerDeckVersionAsObject(), currentUserInput.toString()); - viewModel.saveCard(account, boardLocalId, stackLocalId, fullCard, new IResponseCallback<>() { - @Override - public void onResponse(FullCard createdCard) { - requireActivity().runOnUiThread(() -> { - createCardListener.onCardCreated(createdCard); - - if (openOnSuccess) { - startActivity(EditActivity.createEditCardIntent(requireContext(), account, boardLocalId, createdCard.getLocalId())); - } - dismiss(); - }); + // TODO Check args in onAttach + final var args = getArguments(); + assert args != null; + newCardViewModel.createFullCard(account.getId(), args.getLong(KEY_BOARD_ID), args.getLong(KEY_STACK_ID), currentUserInput.toString()).whenCompleteAsync((fullCard, throwable) -> { + if (throwable != null) { + isPending.setValue(false); + if (throwable instanceof OfflineException) { + Toast.makeText(requireContext(), ((OfflineException) throwable).getReason().getMessage(), Toast.LENGTH_LONG).show(); + } else { + newCardViewModel.getCurrentAccount() + .thenAcceptAsync(account -> ExceptionDialogFragment + .newInstance(throwable, account) + .show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()), getMainExecutor(requireContext())); + } + } else { + createCardListener.onCardCreated(fullCard); + if (openOnSuccess) { + newCardViewModel + .createEditIntent(requireContext(), fullCard.getAccountId(), args.getLong(KEY_BOARD_ID), fullCard.getLocalId()) + .thenAcceptAsync(this::startActivity, getMainExecutor(requireContext())); + } + dismiss(); } - - @Override - public void onError(Throwable throwable) { - IResponseCallback.super.onError(throwable); - requireActivity().runOnUiThread(() -> { - isPending.setValue(false); - if (throwable instanceof OfflineException) { - Toast.makeText(requireContext(), ((OfflineException) throwable).getReason().getMessage(), Toast.LENGTH_LONG).show(); - } else { - ExceptionDialogFragment - .newInstance(throwable, account) - .show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } - }); - } - }); + }, getMainExecutor(requireContext())); } else { binding.inputWrapper.setError(getString(R.string.title_is_mandatory)); binding.input.requestFocus(); @@ -227,14 +198,24 @@ public class NewCardDialog extends DialogFragment implements DialogInterface.OnC return input != null && !input.toString().trim().isEmpty(); } - public static DialogFragment newInstance(@NonNull Account account, long boardLocalId, long stackLocalId, @ColorInt int brand) { - final var fragment = new NewCardDialog(); + public static DialogFragment newInstance(@NonNull Account account, long boardId, long stackId) { + final NewCardDialog dialog = new NewCardDialog(); + final var args = new Bundle(); - args.putSerializable(ARG_ACCOUNT, account); - args.putLong(ARG_BOARD_LOCAL_ID, boardLocalId); - args.putLong(ARG_STACK_LOCAL_ID, stackLocalId); - args.putInt(ARG_BRAND, brand); - fragment.setArguments(args); - return fragment; + args.putSerializable(KEY_ACCOUNT, account); + args.putLong(KEY_BOARD_ID, boardId); + args.putLong(KEY_STACK_ID, stackId); + dialog.setArguments(args); + + return dialog; + + } + + @Override + public void applyTheme(int color) { + final var utils = ThemeUtils.of(color, requireContext()); + + utils.material.colorTextInputLayout(binding.inputWrapper); + utils.platform.colorCircularProgressBar(binding.progressCircular, ColorRole.PRIMARY); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardViewModel.java new file mode 100644 index 000000000..517c2267c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardViewModel.java @@ -0,0 +1,76 @@ +package it.niedermann.nextcloud.deck.ui.card; + +import static java.util.concurrent.CompletableFuture.supplyAsync; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import it.niedermann.nextcloud.deck.api.IResponseCallback; +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.ui.viewmodel.SyncViewModel; + +public class NewCardViewModel extends SyncViewModel { + + public NewCardViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + super(application, account); + } + + public CompletableFuture<Account> getCurrentAccount() { + return baseRepository.getCurrentAccountId().thenApplyAsync(baseRepository::readAccountDirectly); + } + + public CompletableFuture<FullCard> createFullCard(long accountId, long boardId, long stackId, String content) { + final var result = new CompletableFuture<FullCard>(); + + supplyAsync(() -> baseRepository.readAccountDirectly(accountId)) + .thenAcceptAsync(account -> syncManager.createFullCard(accountId, boardId, stackId, createFullCard(account.getServerDeckVersionAsObject(), content), + new IResponseCallback<>() { + @Override + public void onResponse(FullCard response) { + result.complete(response); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + result.completeExceptionally(throwable); + } + })); + return result; + } + + private FullCard createFullCard(@NonNull Version version, @NonNull String content) { + if (TextUtils.isEmpty(content)) { + throw new IllegalArgumentException("Content must not be empty."); + } + final var fullCard = new FullCard(); + final var card = new Card(); + final int maxLength = version.getCardTitleMaxLength(); + if (content.length() > maxLength) { + card.setTitle(content.substring(0, maxLength).trim()); + card.setDescription(content.substring(maxLength).trim()); + } else { + card.setTitle(content); + card.setDescription(null); + } + fullCard.setCard(card); + return fullCard; + } + + public CompletionStage<Intent> createEditIntent(@NonNull Context context, long accountId, long boardId, long cardId) { + return supplyAsync(() -> baseRepository.readAccountDirectly(accountId)) + .thenApplyAsync(account -> EditActivity.createEditCardIntent(context, account, boardId, cardId)); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java index 1198f7ebd..d20332faf 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java @@ -1,7 +1,9 @@ package it.niedermann.nextcloud.deck.ui.card; +import androidx.annotation.NonNull; + import it.niedermann.nextcloud.deck.model.full.FullCard; public interface SelectCardListener { - void onCardSelected(FullCard fullCard); + void onCardSelected(@NonNull FullCard fullCard, long boardId); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java index 3f6c62a52..34a1bc177 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java @@ -1,40 +1,74 @@ package it.niedermann.nextcloud.deck.ui.card; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Filter; import androidx.activity.ComponentActivity; import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.Observer; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import java.util.List; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.android.util.DimensionUtil; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteUserBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData; import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter; -import it.niedermann.nextcloud.deck.util.ViewUtil; public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> { + + private static final long NO_CARD = Long.MIN_VALUE; @NonNull private final Account account; - private final UserSearchLiveData liveSearchForACL; - private LiveData<List<User>> liveData; - private Observer<List<User>> observer; - public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId) { + /** + * Use this constructor to find users to be added to the ACL of a board + */ + public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId) throws NextcloudFilesAppAccountNotFoundException { this(activity, account, boardId, NO_CARD); } - public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) { - super(activity, account.getId(), boardId, cardId); + /** + * Use this constructor to find users to be added to a specific card which are already in the ACL of the board + */ + public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) throws NextcloudFilesAppAccountNotFoundException { + super(activity, account, boardId); this.account = account; - this.liveSearchForACL = syncManager.searchUserByUidOrDisplayNameForACL(); + + final ReactiveLiveData<List<User>> results$; + + constraint$ + .filter(constraint -> !TextUtils.isEmpty(constraint)) + .debounce(300) + .observe(activity, constraint -> { + DeckLog.verbose("Triggering remote search"); + syncManager.triggerUserSearch(account, constraint); + }); + + if (cardId == NO_CARD) { + // No card means this adapter is used for searching users for Board ACL + results$ = constraint$.flatMap(constraint -> TextUtils.isEmpty(constraint) + ? syncManager.findProposalsForUsersToAssignForACL(account.getId(), boardId, activity.getResources().getInteger(R.integer.max_users_suggested)) + : syncManager.searchUserByUidOrDisplayNameForACL(account.getId(), boardId, constraint)); + } else { + // Card is given, so we are searching for users to assign to a card (limited to users whom the board is shared with) + results$ = constraint$.flatMap(constraint -> TextUtils.isEmpty(constraint) + ? syncManager.findProposalsForUsersToAssignForCards(account.getId(), boardId, cardId, activity.getResources().getInteger(R.integer.max_users_suggested)) + : syncManager.searchUserByUidOrDisplayNameForCards(account.getId(), boardId, cardId, constraint)); + } + + results$ + .map(this::filterExcluded) + .distinctUntilChanged() + .observe(activity, this::publishResults); } @Override @@ -47,41 +81,14 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> { binding = ItemAutocompleteUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); } - ViewUtil.addAvatar(binding.icon, account.getUrl(), getItem(position).getUid(), R.drawable.ic_person_grey600_24dp); + Glide.with(binding.icon.getContext()) + .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.icon.getContext(), R.dimen.avatar_size), getItem(position).getUid())) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(binding.icon); binding.label.setText(getItem(position).getDisplayname()); return binding.getRoot(); } - - @Override - public Filter getFilter() { - return new AutoCompleteFilter() { - @Override - protected FilterResults performFiltering(CharSequence constraint) { - if (constraint != null) { - activity.runOnUiThread(() -> { - final int constraintLength = constraint.toString().trim().length(); - if (cardId == NO_CARD) { - liveData = constraintLength > 0 - ? liveSearchForACL.search(accountId, boardId, constraint.toString()) - : syncManager.findProposalsForUsersToAssignForACL(accountId, boardId, activity.getResources().getInteger(R.integer.max_users_suggested)); - } else { - liveData = constraintLength > 0 - ? syncManager.searchUserByUidOrDisplayName(accountId, boardId, cardId, constraint.toString()) - : syncManager.findProposalsForUsersToAssign(accountId, boardId, cardId, activity.getResources().getInteger(R.integer.max_users_suggested)); - } - liveData.removeObservers(activity); - observer = users -> { - users.removeAll(itemsToExclude); - filterResults.values = users; - filterResults.count = users.size(); - publishResults(constraint, filterResults); - }; - liveData.observe(activity, observer); - }); - } - return filterResults; - } - }; - } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java index 0678357d4..f78edd3f7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java @@ -13,8 +13,8 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemActivityBinding; import it.niedermann.nextcloud.deck.model.enums.ActivityType; import it.niedermann.nextcloud.deck.model.ocs.Activity; +import it.niedermann.nextcloud.deck.ui.theme.DeckViewThemeUtils; import it.niedermann.nextcloud.deck.util.DateUtil; -import it.niedermann.nextcloud.deck.util.ViewUtil; public class CardActivityViewHolder extends RecyclerView.ViewHolder { public ItemActivityBinding binding; @@ -75,13 +75,13 @@ public class CardActivityViewHolder extends RecyclerView.ViewHolder { private static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @NonNull ActivityType type) { switch (type) { case ADD: - ViewUtil.setImageColor(context, imageView, R.color.activity_create); + DeckViewThemeUtils.setImageColor(context, imageView, R.color.activity_create); break; case DELETE: - ViewUtil.setImageColor(context, imageView, R.color.activity_delete); + DeckViewThemeUtils.setImageColor(context, imageView, R.color.activity_delete); break; default: - ViewUtil.setImageColor(context, imageView, R.color.grey600); + DeckViewThemeUtils.setImageColor(context, imageView, R.color.grey600); break; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java index 9a46cec65..6452883d3 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 @@ -1,11 +1,10 @@ package it.niedermann.nextcloud.deck.ui.card.assignee; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; +import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode; import android.app.Dialog; import android.content.Context; import android.graphics.Color; -import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; @@ -87,11 +86,11 @@ public class CardAssigneeDialog extends DialogFragment { final var circularProgressDrawable = new CircularProgressDrawable(context); circularProgressDrawable.setStrokeWidth(5f); circularProgressDrawable.setCenterRadius(30f); - circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY); + circularProgressDrawable.setColorSchemeColors(isDarkMode(context) ? Color.LTGRAY : Color.DKGRAY); circularProgressDrawable.start(); binding.avatar.post(() -> Glide.with(binding.avatar.getContext()) - .load(viewModel.getAccount().getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + binding.avatar.getWidth()) + .load(viewModel.getAccount().getAvatarUrl(binding.avatar.getWidth(), user.getUid())) .placeholder(circularProgressDrawable) .error(R.drawable.ic_person_grey600_24dp) .into(binding.avatar)); 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 ddc35f76e..7b00f2a84 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 @@ -7,10 +7,11 @@ import android.widget.ImageView; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; + import it.niedermann.android.util.ClipboardUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Account; @@ -33,7 +34,7 @@ public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder { setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), color); itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { menuInflater.inflate(R.menu.attachment_menu, menu); - if(EAttachmentType.DECK_FILE.equals(attachment.getType())) { + if (EAttachmentType.DECK_FILE.equals(attachment.getType())) { menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> { DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName()); return false; @@ -55,9 +56,9 @@ public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder { protected void setNotSyncedYetStatus(boolean synced, @ColorInt int color) { final var notSyncedYet = getNotSyncedYetStatusIcon(); - final var scheme = ThemeUtils.createScheme(color, notSyncedYet.getContext()); + final var utils = ThemeUtils.of(color, notSyncedYet.getContext()); - DrawableCompat.setTint(notSyncedYet.getDrawable(), scheme.getOnPrimaryContainer()); + utils.platform.colorImageView(notSyncedYet, ColorRole.PRIMARY); notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE); } 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 fa2ff1abe..61dc3e8ee 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 @@ -14,7 +14,6 @@ import static androidx.core.content.PermissionChecker.checkSelfPermission; import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED; import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN; import static java.net.HttpURLConnection.HTTP_CONFLICT; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; 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.FilesUtil.copyContentUriToTempFile; @@ -37,7 +36,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.SharedElementCallback; -import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; @@ -62,6 +60,7 @@ import id.zelory.compressor.constraint.FormatConstraint; import id.zelory.compressor.constraint.QualityConstraint; import id.zelory.compressor.constraint.ResolutionConstraint; import id.zelory.compressor.constraint.SizeConstraint; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; @@ -106,11 +105,6 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet private static final int REQUEST_CODE_PICK_CONTACT = 5; private static final int REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION = 6; - @ColorInt - private int accentColor; - @ColorInt - private int primaryColor; - private CardAttachmentAdapter adapter; private AbstractPickerAdapter<?> pickerAdapter; @@ -141,8 +135,6 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet } return true; }); - accentColor = ContextCompat.getColor(requireContext(), R.color.accent); - primaryColor = ContextCompat.getColor(requireContext(), R.color.primary); // This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance // See https://github.com/stefan-niedermann/nextcloud-deck/issues/478 @@ -249,11 +241,12 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet pickerAdapter = new GalleryAdapter(requireContext(), (uri, pair) -> { previewViewModel.prepareDialog(pair.first, pair.second); PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName()); - observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> { - if (submitPositive) { - onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)); - } - }); + new ReactiveLiveData<>(previewViewModel.getResult()) + .observeOnce(getViewLifecycleOwner(), submitPositive -> { + if (submitPositive) { + onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)); + } + }); }, this::openNativeCameraPicker, getViewLifecycleOwner()); if (binding.pickerRecyclerView.getItemDecorationCount() == 0) { binding.pickerRecyclerView.addItemDecoration(galleryItemDecoration); @@ -273,11 +266,12 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet pickerAdapter = new ContactAdapter(requireContext(), (uri, pair) -> { previewViewModel.prepareDialog(pair.first, pair.second); PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName()); - observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> { - if (submitPositive) { - onActivityResult(REQUEST_CODE_PICK_CONTACT, RESULT_OK, new Intent().setData(uri)); - } - }); + new ReactiveLiveData<>(previewViewModel.getResult()) + .observeOnce(getViewLifecycleOwner(), submitPositive -> { + if (submitPositive) { + onActivityResult(REQUEST_CODE_PICK_CONTACT, RESULT_OK, new Intent().setData(uri)); + } + }); }, this::openNativeContactPicker); removeGalleryItemDecoration(); binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); @@ -299,11 +293,12 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet // pickerAdapter = new FileAdapter(requireContext(), (uri, pair) -> { // previewViewModel.prepareDialog(pair.first, pair.second); // PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName()); -// observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> { -// if (submitPositive) { -// onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)); -// } -// }); +// new ReactiveLiveData<>(previewViewModel.getResult()) +// .observeOnce(getViewLifecycleOwner(), submitPositive -> { +// if (submitPositive) { +// onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)); +// } +// }); // }, this::openNativeFilePicker); // removeGalleryItemDecoration(); // binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); @@ -412,10 +407,11 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet } private void uploadNewAttachmentFromFile(@NonNull File fileToUpload, String mimeType) { + final int color = editViewModel.getAccount().getColor(); for (final var existingAttachment : editViewModel.getFullCard().getAttachments()) { final String existingPath = existingAttachment.getLocalPath(); if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) { - ThemedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show(); + ThemedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG, color).show(); return; } } @@ -444,17 +440,15 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet @Override public void onError(Throwable throwable) { - requireActivity().runOnUiThread(() -> { - if (throwable instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) throwable).getStatusCode() == HTTP_CONFLICT) { - IResponseCallback.super.onError(throwable); - // https://github.com/stefan-niedermann/nextcloud-deck/issues/534 - editViewModel.getFullCard().getAttachments().remove(a); - adapter.removeAttachment(a); - ThemedSnackbar.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()); - } - }); + if (throwable instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) throwable).getStatusCode() == HTTP_CONFLICT) { + IResponseCallback.super.onError(throwable); + // https://github.com/stefan-niedermann/nextcloud-deck/issues/534 + editViewModel.getFullCard().getAttachments().remove(a); + adapter.removeAttachment(a); + ThemedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG, color).show(); + } else { + ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", throwable), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } } }); } @@ -506,7 +500,7 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet @Override public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + if (SyncManager.isNoOnVoidError(throwable)) { IResponseCallback.super.onError(throwable); requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java index 532e59dce..60c92dde3 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java @@ -10,8 +10,9 @@ import androidx.fragment.app.DialogFragment; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Attachment; import it.niedermann.nextcloud.deck.ui.theme.DeleteAlertDialogBuilder; +import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment; -public class DeleteAttachmentDialogFragment extends DialogFragment { +public class DeleteAttachmentDialogFragment extends ThemedDialogFragment { private static final String KEY_ATTACHMENT = "attachment"; @@ -47,6 +48,11 @@ public class DeleteAttachmentDialogFragment extends DialogFragment { .create(); } + @Override + public void applyTheme(int color) { + + } + public static DialogFragment newInstance(Attachment attachment) { final var dialog = new DeleteAttachmentDialogFragment(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java index 113c28932..eafe97fe5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java @@ -2,7 +2,7 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog; import static android.view.View.GONE; import static android.view.View.VISIBLE; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; +import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode; import android.app.Dialog; import android.content.DialogInterface; @@ -39,14 +39,14 @@ public class PreviewDialog extends DialogFragment { final var context = requireContext(); this.imageBuilder$ = this.viewModel.getImageBuilder(); - this.imageBuilder$.observe(requireActivity(), builder -> { + this.imageBuilder$.observe(this, builder -> { if (builder == null) { binding.avatar.setVisibility(GONE); } else { final var circularProgressDrawable = new CircularProgressDrawable(context); circularProgressDrawable.setStrokeWidth(5f); circularProgressDrawable.setCenterRadius(30f); - circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY); + circularProgressDrawable.setColorSchemeColors(isDarkMode(context) ? Color.LTGRAY : Color.DKGRAY); circularProgressDrawable.start(); binding.avatar.setVisibility(VISIBLE); binding.avatar.post(() -> builder @@ -55,7 +55,7 @@ public class PreviewDialog extends DialogFragment { } }); this.title$ = this.viewModel.getTitle(); - this.title$.observe(requireActivity(), title -> { + this.title$.observe(this, title -> { if (TextUtils.isEmpty(title)) { binding.title.setVisibility(GONE); } else { 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 b392d5725..6c2f5b86c 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 @@ -1,13 +1,12 @@ package it.niedermann.nextcloud.deck.ui.card.comments; -import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor; - import android.content.Context; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; @@ -20,11 +19,14 @@ import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; -import scheme.Scheme; +import it.niedermann.nextcloud.deck.ui.theme.Themed; -public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHolder> { +public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHolder> implements Themed { - private final Scheme scheme; + @NonNull + private final Context context; + @Nullable + private ThemeUtils utils; @NonNull private final List<FullDeckComment> comments = new ArrayList<>(); @NonNull @@ -40,14 +42,22 @@ public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHol @NonNull private final CommentEditedListener editListener; - CardCommentsAdapter(@NonNull Context context, @NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager, CommentEditedListener editListener) { + CardCommentsAdapter( + @NonNull Context context, + @NonNull Account account, + @NonNull MenuInflater menuInflater, + @NonNull CommentDeletedListener deletedListener, + @NonNull CommentSelectAsReplyListener selectAsReplyListener, + @NonNull FragmentManager fragmentManager, + @NonNull CommentEditedListener editListener + ) { + this.context = context; this.account = account; this.menuInflater = menuInflater; this.deletedListener = deletedListener; this.selectAsReplyListener = selectAsReplyListener; this.fragmentManager = fragmentManager; this.editListener = editListener; - this.scheme = ThemeUtils.createScheme(readBrandMainColor(context), context); setHasStableIds(true); } @@ -65,7 +75,7 @@ public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHol @Override public void onBindViewHolder(@NonNull ItemCommentViewHolder holder, int position) { final var comment = comments.get(position); - holder.bind(comment, account, scheme, menuInflater, deletedListener, selectAsReplyListener, fragmentManager, (changedText) -> { + holder.bind(comment, account, utils, 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()); @@ -90,4 +100,10 @@ public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHol public int getItemCount() { return comments.size(); } + + @Override + public void applyTheme(int color) { + this.utils = ThemeUtils.of(color, context); + notifyDataSetChanged(); + } } 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 277eb82c2..ecefdc198 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 @@ -3,6 +3,7 @@ package it.niedermann.nextcloud.deck.ui.card.comments; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import android.view.KeyEvent; @@ -18,6 +19,9 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + import java.time.Instant; import it.niedermann.android.util.DimensionUtil; @@ -25,6 +29,7 @@ import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabCommentsBinding; +import it.niedermann.nextcloud.deck.model.Account; 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; @@ -32,30 +37,46 @@ 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.ui.theme.ThemeUtils; -import it.niedermann.nextcloud.deck.util.ViewUtil; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; public class CardCommentsFragment extends Fragment implements CommentEditedListener, CommentDeletedListener, CommentSelectAsReplyListener { + private static final String KEY_ACCOUNT = "account"; private FragmentCardEditTabCommentsBinding binding; - private EditCardViewModel mainViewModel; + private EditCardViewModel editCardViewModel; private CommentsViewModel commentsViewModel; private CardCommentsAdapter adapter; - public static Fragment newInstance() { - return new CardCommentsFragment(); + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + final var args = getArguments(); + + if (args == null || !args.containsKey(KEY_ACCOUNT)) { + throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided."); + } + + final var account = (Account) args.getSerializable(KEY_ACCOUNT); + + if (account == null) { + throw new IllegalArgumentException(KEY_ACCOUNT + " must not be null."); + } + + editCardViewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + commentsViewModel = new ViewModelProvider(this, new SyncViewModel.Factory(requireActivity().getApplication(), account)).get(CommentsViewModel.class); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentCardEditTabCommentsBinding.inflate(inflater, container, false); - mainViewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + binding = FragmentCardEditTabCommentsBinding.inflate(inflater, container, false); // This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance // See https://github.com/stefan-niedermann/nextcloud-deck/issues/478 - if (mainViewModel.getFullCard() == null) { + if (editCardViewModel.getFullCard() == null) { DeckLog.logError(new IllegalStateException("Cannot populate " + CardCommentsFragment.class.getSimpleName() + " because viewModel.getFullCard() is null")); if (requireActivity() instanceof EditActivity) { Toast.makeText(getContext(), R.string.error_edit_activity_killed_by_android, Toast.LENGTH_LONG).show(); @@ -66,12 +87,16 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe return binding.getRoot(); } - commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class); - adapter = new CardCommentsAdapter(requireContext(), mainViewModel.getAccount(), requireActivity().getMenuInflater(), this, this, getChildFragmentManager(), this); + adapter = new CardCommentsAdapter(requireContext(), editCardViewModel.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); + Glide.with(binding.avatar.getContext()) + .load(editCardViewModel.getAccount().getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details))) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(binding.avatar); commentsViewModel.getReplyToComment().observe(getViewLifecycleOwner(), (comment) -> { if (comment == null) { @@ -79,10 +104,9 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe } else { binding.replyCommentText.setMarkdownString(comment.getComment().getMessage()); binding.replyComment.setVisibility(VISIBLE); -// setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText); } }); - commentsViewModel.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(), + commentsViewModel.getFullCommentsForLocalCardId(editCardViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(), (comments) -> { if (comments != null && comments.size() > 0) { binding.emptyContentView.setVisibility(GONE); @@ -93,20 +117,20 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe binding.comments.setVisibility(GONE); } }); - if (mainViewModel.canEdit()) { + if (editCardViewModel.canEdit()) { binding.addCommentLayout.setVisibility(VISIBLE); binding.fab.setOnClickListener(v -> { // Do not handle empty comments (https://github.com/stefan-niedermann/nextcloud-deck/issues/440) if (!TextUtils.isEmpty(binding.message.getText().toString().trim())) { binding.emptyContentView.setVisibility(GONE); binding.comments.setVisibility(VISIBLE); - final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), mainViewModel.getAccount().getUserName(), Instant.now()); + final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), editCardViewModel.getAccount().getUserName(), Instant.now()); final FullDeckComment parent = commentsViewModel.getReplyToComment().getValue(); if (parent != null) { comment.setParentId(parent.getId()); commentsViewModel.setReplyToComment(null); } - commentsViewModel.addCommentToCard(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), comment); + commentsViewModel.addCommentToCard(editCardViewModel.getAccount().getId(), editCardViewModel.getFullCard().getLocalId(), comment); } binding.message.setText(null); }); @@ -116,7 +140,7 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe } return true; }); - binding.message.addTextChangedListener(new CardCommentsMentionProposer(getViewLifecycleOwner(), mainViewModel.getAccount(), mainViewModel.getBoardId(), binding.message, binding.mentionProposerWrapper, binding.mentionProposer)); + binding.message.addTextChangedListener(new CardCommentsMentionProposer(getViewLifecycleOwner(), editCardViewModel.getAccount(), editCardViewModel.getBoardId(), binding.message, binding.mentionProposerWrapper, binding.mentionProposer)); } else { binding.addCommentLayout.setVisibility(GONE); } @@ -126,11 +150,11 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - if (mainViewModel.canEdit()) { + if (editCardViewModel.canEdit()) { binding.message.requestFocus(); requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); } - mainViewModel.getBoardColor().observe(getViewLifecycleOwner(), this::applyTheme); + editCardViewModel.getBoardColor().observe(getViewLifecycleOwner(), this::applyTheme); } @Override @@ -141,12 +165,12 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe @Override public void onCommentEdited(Long id, String comment) { - commentsViewModel.updateComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), id, comment); + commentsViewModel.updateComment(editCardViewModel.getAccount().getId(), editCardViewModel.getFullCard().getLocalId(), id, comment); } @Override public void onCommentDeleted(Long localId) { - commentsViewModel.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId, new IResponseCallback<>() { + commentsViewModel.deleteComment(editCardViewModel.getAccount().getId(), editCardViewModel.getFullCard().getLocalId(), localId, new IResponseCallback<>() { @Override public void onResponse(Void response) { DeckLog.info("Successfully deleted comment with localId", localId); @@ -154,9 +178,9 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe @Override public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + if (SyncManager.isNoOnVoidError(throwable)) { IResponseCallback.super.onError(throwable); - requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, editCardViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } } }); @@ -173,4 +197,14 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe public void onSelectAsReply(FullDeckComment comment) { commentsViewModel.setReplyToComment(comment); } + + public static Fragment newInstance(@NonNull Account account) { + final var fragment = new CardCommentsFragment(); + + final var args = new Bundle(); + args.putSerializable(KEY_ACCOUNT, account); + fragment.setArguments(args); + + return fragment; + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java index 60aef597b..5b5dead49 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java @@ -1,9 +1,6 @@ package it.niedermann.nextcloud.deck.ui.card.comments; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; - import android.annotation.SuppressLint; -import android.net.Uri; import android.text.Editable; import android.text.TextWatcher; import android.view.View; @@ -21,18 +18,19 @@ import com.bumptech.glide.request.RequestOptions; import java.util.ArrayList; import java.util.List; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; import it.niedermann.nextcloud.deck.ui.card.comments.util.CommentsUtil; public class CardCommentsMentionProposer implements TextWatcher { private final int avatarSize; @NonNull - private final SyncManager syncManager; + private final BaseRepository baseRepository; @NonNull private final LinearLayout.LayoutParams layoutParams; @NonNull @@ -57,7 +55,7 @@ public class CardCommentsMentionProposer implements TextWatcher { this.editText = editText; this.mentionProposerWrapper = mentionProposerWrapper; this.mentionProposer = avatarProposer; - syncManager = new SyncManager(editText.getContext()); + baseRepository = new BaseRepository(editText.getContext()); avatarSize = DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.avatar_size_small); layoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize); layoutParams.setMarginEnd(DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.spacer_1x)); @@ -79,38 +77,39 @@ public class CardCommentsMentionProposer implements TextWatcher { this.users.clear(); } else { if (mentionProposal.first != null && mentionProposal.second != null) { - observeOnce(syncManager.searchUserByUidOrDisplayName(account.getId(), boardLocalId, -1L, mentionProposal.first), owner, (users) -> { - if (!users.equals(this.users)) { - mentionProposer.removeAllViews(); - if (users.size() > 0) { - mentionProposerWrapper.setVisibility(View.VISIBLE); - for (final var user : users) { - final var avatar = new ImageView(mentionProposer.getContext()); - avatar.setLayoutParams(layoutParams); - updateListenerOfView(avatar, s, mentionProposal, user); - - mentionProposer.addView(avatar); - - Glide.with(avatar.getContext()) - .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + avatarSize) - .placeholder(R.drawable.ic_person_grey600_24dp) - .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) - .into(avatar); + new ReactiveLiveData<>(baseRepository.searchUserByUidOrDisplayNameForCards(account.getId(), boardLocalId, -1L, mentionProposal.first)) + .observeOnce(owner, users -> { + if (!users.equals(this.users)) { + mentionProposer.removeAllViews(); + if (users.size() > 0) { + mentionProposerWrapper.setVisibility(View.VISIBLE); + for (final var user : users) { + final var avatar = new ImageView(mentionProposer.getContext()); + avatar.setLayoutParams(layoutParams); + updateListenerOfView(avatar, s, mentionProposal, user); + + mentionProposer.addView(avatar); + + Glide.with(avatar.getContext()) + .load(account.getAvatarUrl(avatarSize, user.getUid())) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(avatar); + } + } else { + mentionProposerWrapper.setVisibility(View.GONE); + } + this.users.clear(); + this.users.addAll(users); + } else { + int i = 0; + for (User user : users) { + updateListenerOfView(mentionProposer.getChildAt(i), s, mentionProposal, user); + i++; + } } - } else { - mentionProposerWrapper.setVisibility(View.GONE); - } - this.users.clear(); - this.users.addAll(users); - } else { - int i = 0; - for (User user : users) { - updateListenerOfView(mentionProposer.getChildAt(i), s, mentionProposal, user); - i++; - } - } - }); + }); } else { this.users.clear(); mentionProposer.removeAllViews(); 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 46ec36e0d..5918c9b41 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java @@ -1,31 +1,30 @@ package it.niedermann.nextcloud.deck.ui.card.comments; +import static androidx.lifecycle.Transformations.distinctUntilChanged; + import android.app.Application; import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + import java.util.List; import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.Account; 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 static androidx.lifecycle.Transformations.distinctUntilChanged; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; @SuppressWarnings("WeakerAccess") -public class CommentsViewModel extends AndroidViewModel { - - private final SyncManager syncManager; +public class CommentsViewModel extends SyncViewModel { private final MutableLiveData<FullDeckComment> replyToComment = new MutableLiveData<>(); - public CommentsViewModel(@NonNull Application application) { - super(application); - this.syncManager = new SyncManager(application); + public CommentsViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + super(application, account); } public void setReplyToComment(FullDeckComment replyToComment) { @@ -37,7 +36,7 @@ public class CommentsViewModel extends AndroidViewModel { } public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) { - return distinctUntilChanged(syncManager.getFullCommentsForLocalCardId(localCardId)); + return distinctUntilChanged(baseRepository.getFullCommentsForLocalCardId(localCardId)); } public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) { 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 f38e41f93..dc9b8dbb5 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 @@ -5,11 +5,15 @@ import android.view.MenuInflater; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.widget.TooltipCompat; -import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; + import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; @@ -23,9 +27,8 @@ import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.util.DateUtil; -import it.niedermann.nextcloud.deck.util.ViewUtil; -import scheme.Scheme; public class ItemCommentViewHolder extends RecyclerView.ViewHolder { private final ItemCommentBinding binding; @@ -38,8 +41,14 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { this.binding.message.setMovementMethod(LinkMovementMethod.getInstance()); } - public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @NonNull Scheme scheme, @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); + public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @Nullable ThemeUtils utils, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager, @NonNull Consumer<CharSequence> editListener) { + Glide.with(binding.avatar.getContext()) + .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), comment.getComment().getActorId())) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(binding.avatar); + final var mentions = new HashMap<String, String>(comment.getComment().getMentions().size()); for (final var mention : comment.getComment().getMentions()) { mentions.put(mention.getMentionId(), mention.getMentionDisplayName()); @@ -70,7 +79,7 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { return true; }); menu.findItem(android.R.id.edit).setOnMenuItemClickListener(item -> { - CardCommentsEditDialogFragment.newInstance(comment.getLocalId(), comment.getComment().getMessage()).show(fragmentManager, CardCommentsAdapter.class.getCanonicalName()); + CardCommentsEditDialogFragment.newInstance(comment.getLocalId(), comment.getComment().getMessage()).show(fragmentManager, CardCommentsEditDialogFragment.class.getCanonicalName()); return true; }); } else { @@ -80,7 +89,9 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { }); TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter)); - DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), scheme.getOnPrimaryContainer()); + if (utils != null) { + utils.platform.colorImageView(binding.notSyncedYet, ColorRole.PRIMARY); + } binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(comment.getStatusEnum()) ? View.VISIBLE : View.GONE); if (comment.getParent() == null) { @@ -88,7 +99,9 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { } else { final int commentParentMaxLines = itemView.getContext().getResources().getInteger(R.integer.comment_parent_max_lines); binding.parentContainer.setVisibility(View.VISIBLE); - binding.parentBorder.setBackgroundColor(scheme.getOnPrimaryContainer()); + if (utils != null) { + utils.platform.colorViewBackground(binding.parentBorder); + } binding.parent.setText(comment.getParent().getMessage()); binding.parent.setOnClickListener((v) -> { final boolean previouslyCollapsed = binding.parent.getMaxLines() == commentParentMaxLines; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java index 84e795dd2..dd5bfa4af 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java @@ -5,14 +5,17 @@ import androidx.annotation.Nullable; import androidx.core.util.Consumer; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.util.ViewUtil; public class AssigneeViewHolder extends RecyclerView.ViewHolder { - private ItemAssigneeBinding binding; + private final ItemAssigneeBinding binding; @SuppressWarnings("WeakerAccess") public AssigneeViewHolder(ItemAssigneeBinding binding) { @@ -21,7 +24,12 @@ public class AssigneeViewHolder extends RecyclerView.ViewHolder { } public void bind(@NonNull Account account, @NonNull User user, @Nullable Consumer<User> onClickListener) { - ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), R.drawable.ic_person_grey600_24dp); + Glide.with(binding.avatar.getContext()) + .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), user.getUid())) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(binding.avatar); if (onClickListener != null) { itemView.setOnClickListener((v) -> onClickListener.accept(user)); } 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 b76fae043..04874cee9 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 @@ -27,6 +27,7 @@ import androidx.recyclerview.widget.GridLayoutManager; import com.google.android.material.chip.Chip; import com.google.android.material.snackbar.Snackbar; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.wdullaer.materialdatetimepicker.date.DatePickerDialog; import com.wdullaer.materialdatetimepicker.date.DatePickerDialog.OnDateSetListener; import com.wdullaer.materialdatetimepicker.time.TimePickerDialog; @@ -47,6 +48,7 @@ import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabDetailsBinding; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.full.FullCard; @@ -68,9 +70,16 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, private AssigneeAdapter adapter; private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); + private static final String KEY_ACCOUNT = "account"; - public static Fragment newInstance() { - return new CardDetailsFragment(); + public static Fragment newInstance(@NonNull Account account) { + final var fragment = new CardDetailsFragment(); + + final var args = new Bundle(); + args.putSerializable(KEY_ACCOUNT, account); + fragment.setArguments(args); + + return fragment; } @Override @@ -80,6 +89,12 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, binding = FragmentCardEditTabDetailsBinding.inflate(inflater, container, false); viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + final var args = getArguments(); + + if (args == null || !args.containsKey(KEY_ACCOUNT)) { + throw new IllegalStateException(KEY_ACCOUNT + " must be provided"); + } + // This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance // See https://github.com/stefan-niedermann/nextcloud-deck/issues/478 if (viewModel.getFullCard() == null) { @@ -92,7 +107,7 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, avatarLayoutParams.setMargins(0, 0, DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x), 0); setupAssignees(); - setupLabels(); + setupLabels((Account) args.getSerializable(KEY_ACCOUNT)); setupDueDate(); setupDescription(); setupProjects(); @@ -198,8 +213,9 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, } else { date = LocalDate.now(); } - ThemedDatePickerDialog.newInstance(this, date.getYear(), date.getMonthValue(), date.getDayOfMonth()) - .show(getChildFragmentManager(), ThemedDatePickerDialog.class.getCanonicalName()); + viewModel.getCurrentBoardColor(viewModel.getAccount().getId(), viewModel.getBoardId()) + .thenAcceptAsync(color -> ThemedDatePickerDialog.newInstance(this, date.getYear(), date.getMonthValue(), date.getDayOfMonth(), color) + .show(getChildFragmentManager(), ThemedDatePickerDialog.class.getCanonicalName()), ContextCompat.getMainExecutor(requireContext())); }); binding.dueDateTime.setOnClickListener(v -> { @@ -209,8 +225,9 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, } else { time = LocalTime.now(); } - ThemedTimePickerDialog.newInstance(this, time.getHour(), time.getMinute(), true) - .show(getChildFragmentManager(), ThemedTimePickerDialog.class.getCanonicalName()); + viewModel.getCurrentBoardColor(viewModel.getAccount().getId(), viewModel.getBoardId()) + .thenAcceptAsync(color -> ThemedTimePickerDialog.newInstance(this, time.getHour(), time.getMinute(), true, color) + .show(getChildFragmentManager(), ThemedTimePickerDialog.class.getCanonicalName()), ContextCompat.getMainExecutor(requireContext())); }); binding.clearDueDate.setOnClickListener(v -> { @@ -226,29 +243,30 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, } } - private void setupLabels() { + private void setupLabels(@NonNull Account account) { final long accountId = viewModel.getAccount().getId(); final long boardId = viewModel.getBoardId(); binding.labelsGroup.removeAllViews(); if (viewModel.canEdit()) { Long localCardId = viewModel.getFullCard().getCard().getLocalId(); localCardId = localCardId == null ? -1 : localCardId; - binding.labels.setAdapter(new LabelAutoCompleteAdapter(requireActivity(), accountId, boardId, localCardId)); + try { + binding.labels.setAdapter(new LabelAutoCompleteAdapter(requireActivity(), account, boardId, localCardId)); + } catch (NextcloudFilesAppAccountNotFoundException e) { + ExceptionDialogFragment.newInstance(e, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + // TODO Handle error + } binding.labels.setOnItemClickListener((adapterView, view, position, id) -> { final var label = (Label) adapterView.getItemAtPosition(position); - if (LabelAutoCompleteAdapter.ITEM_CREATE == label.getLocalId()) { - final Label newLabel = new Label(label); - newLabel.setBoardId(boardId); - newLabel.setTitle(((LabelAutoCompleteAdapter) binding.labels.getAdapter()).getLastFilterText()); - newLabel.setLocalId(null); - viewModel.createLabel(accountId, newLabel, boardId, new IResponseCallback<>() { + if (label.getLocalId() == null) { + viewModel.createLabel(accountId, label, boardId, new IResponseCallback<>() { @Override public void onResponse(Label response) { requireActivity().runOnUiThread(() -> { - newLabel.setLocalId(response.getLocalId()); + label.setLocalId(response.getLocalId()); ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(response); viewModel.getFullCard().getLabels().add(response); - binding.labelsGroup.addView(createChipFromLabel(newLabel)); + binding.labelsGroup.addView(createChipFromLabel(label)); binding.labelsGroup.setVisibility(VISIBLE); }); } @@ -256,8 +274,9 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, @Override public void onError(Throwable throwable) { IResponseCallback.super.onError(throwable); - requireActivity().runOnUiThread(() -> ThemedSnackbar.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()); + viewModel.getCurrentBoardColor(viewModel.getAccount().getId(), viewModel.getBoardId()) + .thenAcceptAsync(color -> ThemedSnackbar.make(requireView(), getString(R.string.error_create_label, label.getTitle()), Snackbar.LENGTH_LONG, color) + .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())).show(), ContextCompat.getMainExecutor(requireContext())); } }); } else { @@ -291,7 +310,7 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, chip.setOnCloseIconClickListener(v -> { binding.labelsGroup.removeView(chip); viewModel.getFullCard().getLabels().remove(label); - ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(label); + ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).doNotLongerExclude(label); }); } try { @@ -311,7 +330,7 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, } private void setupAssignees() { - adapter = new AssigneeAdapter((user) -> CardAssigneeDialog.newInstance(user).show(getChildFragmentManager(), CardAssigneeDialog.class.getSimpleName()), viewModel.getAccount()); + adapter = new AssigneeAdapter(user -> CardAssigneeDialog.newInstance(user).show(getChildFragmentManager(), CardAssigneeDialog.class.getSimpleName()), viewModel.getAccount()); binding.assignees.setAdapter(adapter); binding.assignees.post(() -> { @Px final int gutter = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x); @@ -323,7 +342,12 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, if (viewModel.canEdit()) { Long localCardId = viewModel.getFullCard().getCard().getLocalId(); localCardId = localCardId == null ? -1 : localCardId; - binding.people.setAdapter(new UserAutoCompleteAdapter(requireActivity(), viewModel.getAccount(), viewModel.getBoardId(), localCardId)); + try { + binding.people.setAdapter(new UserAutoCompleteAdapter(requireActivity(), viewModel.getAccount(), viewModel.getBoardId(), localCardId)); + } catch (NextcloudFilesAppAccountNotFoundException e) { + ExceptionDialogFragment.newInstance(e, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + // TODO Handle error + } binding.people.setOnItemClickListener((adapterView, view, position, id) -> { final var user = (User) adapterView.getItemAtPosition(position); viewModel.getFullCard().getAssignedUsers().add(user); @@ -404,11 +428,15 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener, public void onUnassignUser(@NonNull User user) { viewModel.getFullCard().getAssignedUsers().remove(user); adapter.removeUser(user); - ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user); - ThemedSnackbar.make(requireView(), getString(R.string.unassigned_user, user.getDisplayname()), Snackbar.LENGTH_LONG).setAction(R.string.simple_undo, v1 -> { - viewModel.getFullCard().getAssignedUsers().add(user); - ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user); - adapter.addUser(user); - }).show(); + ((UserAutoCompleteAdapter) binding.people.getAdapter()).doNotLongerExclude(user); + + viewModel.getCurrentBoardColor(viewModel.getAccount().getId(), viewModel.getBoardId()) + .thenAcceptAsync(color -> ThemedSnackbar.make(requireView(), getString(R.string.unassigned_user, user.getDisplayname()), Snackbar.LENGTH_LONG, color) + .setAction(R.string.simple_undo, v1 -> { + viewModel.getFullCard().getAssignedUsers().add(user); + ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user); + adapter.addUser(user); + }) + .show(), ContextCompat.getMainExecutor(requireContext())); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java index 58e104973..a69ddf80e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java @@ -73,7 +73,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment { .create(); } - public static DialogFragment newInstance(Throwable throwable, @Nullable Account account) { + public static DialogFragment newInstance(@NonNull Throwable throwable, @Nullable Account account) { final var fragment = new ExceptionDialogFragment(); final var args = new Bundle(); args.putSerializable(KEY_THROWABLE, throwable); 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 1e52271e4..c57c80d24 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 @@ -18,6 +18,7 @@ import androidx.core.util.Consumer; import androidx.recyclerview.widget.RecyclerView; import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotSupportedException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import com.nextcloud.android.sso.exceptions.TokenMismatchException; @@ -75,6 +76,11 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { add(R.string.error_dialog_tip_token_mismatch_retry); 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 NextcloudFilesAppAccountNotFoundException) { + // TODO we can give better hints here... + add(R.string.error_dialog_tip_token_mismatch_retry); + add(R.string.error_dialog_tip_clear_storage_might_help); + add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO); } else if (throwable instanceof NextcloudFilesAppNotSupportedException) { add(R.string.error_dialog_min_version, new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.nextcloud.client")) .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_update_files_app)); 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 87427209d..08512c5c6 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 @@ -5,6 +5,7 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; @@ -66,7 +67,7 @@ public class FilterDialogFragment extends ThemedDialogFragment { tab.setIcon(draft.getDueType() != EDueType.NO_FILTER ? indicator : null); break; default: - throw new IllegalStateException("position must be between 0 and 2"); + throw new IllegalStateException("position must be between 0 and 2 but was " + position); } }); tab.setText(tabTitles[position]); @@ -104,7 +105,7 @@ public class FilterDialogFragment extends ThemedDialogFragment { } @Override - public void applyTheme(int color) { + public void applyTheme(@ColorInt int color) { final var utils = ThemeUtils.of(color, requireContext()); utils.deck.themeTabLayout(binding.tabLayout, Color.TRANSPARENT); @@ -128,7 +129,7 @@ public class FilterDialogFragment extends ThemedDialogFragment { case 2: return new FilterDueTypeFragment(); default: - throw new IllegalArgumentException("position must be between 0 and 2"); + throw new IllegalArgumentException("position must be between 0 and 2 but was " + position); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java index dab5bb9f7..7d218e19e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java @@ -1,8 +1,10 @@ package it.niedermann.nextcloud.deck.ui.filter; +import android.os.Build; import android.view.LayoutInflater; import android.view.ViewGroup; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; @@ -11,6 +13,8 @@ import java.util.Arrays; import it.niedermann.nextcloud.deck.databinding.ItemFilterDuetypeBinding; import it.niedermann.nextcloud.deck.model.enums.EDueType; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdapter.DueTypeViewHolder> { @NonNull @@ -18,14 +22,16 @@ public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdap private int selectedDueTypePosition; @Nullable private final SelectionListener<EDueType> selectionListener; + @ColorInt + private final int color; @SuppressWarnings("WeakerAccess") - public FilterDueTypeAdapter(@NonNull EDueType selectedDueType, @Nullable SelectionListener<EDueType> selectionListener) { + public FilterDueTypeAdapter(@NonNull EDueType selectedDueType, @Nullable SelectionListener<EDueType> selectionListener, @ColorInt int color) { super(); this.selectedDueTypePosition = Arrays.binarySearch(dueTypes, selectedDueType); this.selectionListener = selectionListener; + this.color = color; setHasStableIds(true); - notifyDataSetChanged(); } @NonNull @@ -49,8 +55,8 @@ public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdap return dueTypes.length; } - class DueTypeViewHolder extends RecyclerView.ViewHolder { - private ItemFilterDuetypeBinding binding; + class DueTypeViewHolder extends RecyclerView.ViewHolder implements Themed { + private final ItemFilterDuetypeBinding binding; DueTypeViewHolder(@NonNull ItemFilterDuetypeBinding binding) { super(binding.getRoot()); @@ -60,6 +66,7 @@ public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdap void bind(final EDueType dueType) { binding.dueType.setText(dueType.toString(binding.dueType.getContext())); itemView.setSelected(dueTypes[selectedDueTypePosition].equals(dueType)); + applyTheme(color); itemView.setOnClickListener(view -> { final int oldSelection = selectedDueTypePosition; @@ -80,5 +87,13 @@ public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdap notifyItemChanged(oldSelection); }); } + + @Override + public void applyTheme(int color) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final var utils = ThemeUtils.of(color, itemView.getContext()); + utils.deck.colorSelectedCheck(binding.selectedCheck.getContext(), binding.selectedCheck.getDrawable()); + } + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java index ac23faf6c..c173433e1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java @@ -28,7 +28,8 @@ public class FilterDueTypeFragment extends Fragment implements SelectionListener filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); binding.dueType.setItemAnimator(null); - binding.dueType.setAdapter(new FilterDueTypeAdapter(requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getDueType(), this)); + filterViewModel.getCurrentBoardColor$().observe(getViewLifecycleOwner(), + color -> binding.dueType.setAdapter(new FilterDueTypeAdapter(requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getDueType(), this, color))); return binding.getRoot(); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java index 83b8f8f3e..194540139 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java @@ -1,21 +1,26 @@ package it.niedermann.nextcloud.deck.ui.filter; import android.content.res.ColorStateList; +import android.os.Build; import android.view.LayoutInflater; import android.view.ViewGroup; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemFilterLabelBinding; import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; @SuppressWarnings("WeakerAccess") public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapter.LabelViewHolder> { @@ -27,8 +32,10 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte private static final Label NOT_ASSIGNED = null; @Nullable private final SelectionListener<Label> selectionListener; + @ColorInt + private final int color; - public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, boolean noAssignedLabel, @Nullable SelectionListener<Label> selectionListener) { + public FilterLabelsAdapter(@NonNull Collection<Label> labels, @NonNull Collection<Label> selectedLabels, boolean noAssignedLabel, @Nullable SelectionListener<Label> selectionListener, @ColorInt int color) { super(); this.labels.add(NOT_ASSIGNED); this.labels.addAll(labels); @@ -37,8 +44,8 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte } this.selectedLabels.addAll(selectedLabels); this.selectionListener = selectionListener; + this.color = color; setHasStableIds(true); - notifyDataSetChanged(); } @Override @@ -67,8 +74,8 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte return labels.size(); } - class LabelViewHolder extends RecyclerView.ViewHolder { - private ItemFilterLabelBinding binding; + class LabelViewHolder extends RecyclerView.ViewHolder implements Themed { + private final ItemFilterLabelBinding binding; LabelViewHolder(@NonNull ItemFilterLabelBinding binding) { super(binding.getRoot()); @@ -80,9 +87,10 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte binding.label.setText(label.getTitle()); final int labelColor = label.getColor(); binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); - final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); - binding.label.setTextColor(color); + final int textColor = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); + binding.label.setTextColor(textColor); itemView.setSelected(selectedLabels.contains(label)); + applyTheme(color); bindClickListener(label); } @@ -93,6 +101,7 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte binding.label.setChipBackgroundColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.primary))); binding.label.setRippleColor(null); itemView.setSelected(selectedLabels.contains(NOT_ASSIGNED)); + applyTheme(color); bindClickListener(NOT_ASSIGNED); } @@ -113,5 +122,13 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte } }); } + + @Override + public void applyTheme(int color) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final var utils = ThemeUtils.of(color, itemView.getContext()); + utils.deck.colorSelectedCheck(binding.selectedCheck.getContext(), binding.selectedCheck.getDrawable()); + } + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java index b4eb17d0e..a69ced0ea 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java @@ -1,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.filter; import static java.util.Objects.requireNonNull; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import android.os.Bundle; import android.view.LayoutInflater; @@ -13,9 +12,9 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.databinding.DialogFilterLabelsBinding; import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.ui.MainViewModel; public class FilterLabelsFragment extends Fragment implements SelectionListener<Label> { @@ -26,18 +25,20 @@ public class FilterLabelsFragment extends Fragment implements SelectionListener< public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { final var binding = DialogFilterLabelsBinding.inflate(requireActivity().getLayoutInflater()); - final var mainViewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); - observeOnce(filterViewModel.findProposalsForLabelsToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (labels) -> { - binding.labels.setNestedScrollingEnabled(false); - binding.labels.setAdapter(new FilterLabelsAdapter( - labels, - requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(), - requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedLabel(), - this)); - }); + new ReactiveLiveData<>(filterViewModel.findProposalsForLabelsToAssign()) + .combineWith(() -> filterViewModel.getCurrentBoardColor$()) + .observeOnce(getViewLifecycleOwner(), pair -> { + binding.labels.setNestedScrollingEnabled(false); + binding.labels.setAdapter(new FilterLabelsAdapter( + pair.first, + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(), + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedLabel(), + this, + pair.second)); + }); return binding.getRoot(); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java index 4b75b985f..b042954be 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java @@ -1,28 +1,31 @@ package it.niedermann.nextcloud.deck.ui.filter; +import android.os.Build; import android.view.LayoutInflater; import android.view.ViewGroup; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.Px; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemFilterUserBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.util.ViewUtil; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; @SuppressWarnings("WeakerAccess") public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.UserViewHolder> { - @Px - final int avatarSize; @NonNull private final Account account; @Nullable @@ -33,10 +36,11 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us private final List<User> selectedUsers = new ArrayList<>(); @Nullable private final SelectionListener<User> selectionListener; + @ColorInt + private final int color; - public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, boolean noAssignedUser, @Nullable SelectionListener<User> selectionListener) { + public FilterUserAdapter(@NonNull Account account, @NonNull Collection<User> users, @NonNull Collection<User> selectedUsers, boolean noAssignedUser, @Nullable SelectionListener<User> selectionListener, @ColorInt int color) { super(); - this.avatarSize = avatarSize; this.account = account; this.users.add(NOT_ASSIGNED); this.users.addAll(users); @@ -45,8 +49,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us } this.selectedUsers.addAll(selectedUsers); this.selectionListener = selectionListener; + this.color = color; setHasStableIds(true); - notifyDataSetChanged(); } @Override @@ -75,8 +79,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us return users.size(); } - class UserViewHolder extends RecyclerView.ViewHolder { - private ItemFilterUserBinding binding; + class UserViewHolder extends RecyclerView.ViewHolder implements Themed { + private final ItemFilterUserBinding binding; UserViewHolder(@NonNull ItemFilterUserBinding binding) { super(binding.getRoot()); @@ -85,8 +89,14 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us void bind(@NonNull final User user) { binding.title.setText(user.getDisplayname()); - ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp); + Glide.with(binding.avatar.getContext()) + .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), user.getUid())) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(binding.avatar); itemView.setSelected(selectedUsers.contains(user)); + applyTheme(color); bindClickListener(user); } @@ -96,6 +106,7 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us .load(R.drawable.ic_baseline_block_24) .into(binding.avatar); itemView.setSelected(selectedUsers.contains(NOT_ASSIGNED)); + applyTheme(color); bindClickListener(NOT_ASSIGNED); } @@ -116,5 +127,13 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us } }); } + + @Override + public void applyTheme(int color) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final var utils = ThemeUtils.of(color, itemView.getContext()); + utils.deck.colorSelectedCheck(binding.selectedCheck.getContext(), binding.selectedCheck.getDrawable()); + } + } } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java index 59768de39..977e4bbd2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java @@ -1,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.filter; import static java.util.Objects.requireNonNull; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import android.os.Bundle; import android.view.LayoutInflater; @@ -10,14 +9,13 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; -import it.niedermann.android.util.DimensionUtil; -import it.niedermann.nextcloud.deck.R; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.databinding.DialogFilterAssigneesBinding; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.ui.MainViewModel; public class FilterUserFragment extends Fragment implements SelectionListener<User> { @@ -28,20 +26,20 @@ public class FilterUserFragment extends Fragment implements SelectionListener<Us @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = DialogFilterAssigneesBinding.inflate(requireActivity().getLayoutInflater()); - final var mainViewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); - observeOnce(filterViewModel.findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> { - binding.users.setNestedScrollingEnabled(false); - binding.users.setAdapter(new FilterUserAdapter( - DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size), - mainViewModel.getCurrentAccount(), - users, - requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(), - requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedUser(), - this)); - }); + new ReactiveLiveData<>(filterViewModel.findProposalsForUsersToAssign()) + .combineWith(() -> filterViewModel.getCurrentBoardColor$()) + .observeOnce(getViewLifecycleOwner(), pair -> { + binding.users.setNestedScrollingEnabled(false); + filterViewModel.getCurrentAccount().thenAcceptAsync(account -> binding.users.setAdapter(new FilterUserAdapter( + account, pair.first, + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(), + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedUser(), + this, + pair.second)), ContextCompat.getMainExecutor(requireContext())); + }); return binding.getRoot(); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java index acdab52e7..09f2f4fa1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java @@ -1,29 +1,26 @@ package it.niedermann.nextcloud.deck.ui.filter; -import static androidx.lifecycle.Transformations.distinctUntilChanged; -import static androidx.lifecycle.Transformations.map; - import android.app.Application; import androidx.annotation.IntRange; import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.util.List; +import java.util.concurrent.CompletableFuture; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.enums.EDueType; import it.niedermann.nextcloud.deck.model.internal.FilterInformation; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; @SuppressWarnings("WeakerAccess") -public class FilterViewModel extends AndroidViewModel { - - private final SyncManager syncManager; +public class FilterViewModel extends BaseViewModel { @IntRange(from = 0, to = 2) private int currentFilterTab = 0; @@ -35,7 +32,10 @@ public class FilterViewModel extends AndroidViewModel { public FilterViewModel(@NonNull Application application) { super(application); - this.syncManager = new SyncManager(application); + } + + public CompletableFuture<Account> getCurrentAccount() { + return baseRepository.getCurrentAccountId().thenApplyAsync(baseRepository::readAccountDirectly); } public void publishFilterInformationDraft() { @@ -60,7 +60,9 @@ public class FilterViewModel extends AndroidViewModel { @NonNull public LiveData<Boolean> hasActiveFilter() { - return distinctUntilChanged(map(getFilterInformation(), FilterInformation::hasActiveFilter)); + return new ReactiveLiveData<>(getFilterInformation()) + .map(FilterInformation::hasActiveFilter) + .distinctUntilChanged(); } public void createFilterInformationDraft() { @@ -130,11 +132,22 @@ public class FilterViewModel extends AndroidViewModel { return this.currentFilterTab; } - public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId) { - return syncManager.findProposalsForUsersToAssign(accountId, boardId, -1L, -1); + // TODO Use in Filter fragments + public LiveData<Integer> getCurrentBoardColor$() { + return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$()) + .combineWith(baseRepository::getCurrentBoardId$) + .flatMap(ids -> baseRepository.getBoardColor$(ids.first, ids.second)); + } + + public LiveData<List<User>> findProposalsForUsersToAssign() { + return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$()) + .combineWith(baseRepository::getCurrentBoardId$) + .flatMap(ids -> baseRepository.findProposalsForUsersToAssignForCards(ids.first, ids.second, -1L, -1)); } - public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) { - return syncManager.findProposalsForLabelsToAssign(accountId, boardId, -1L); + public LiveData<List<Label>> findProposalsForLabelsToAssign() { + return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$()) + .combineWith(baseRepository::getCurrentBoardId$) + .flatMap(ids -> baseRepository.findProposalsForLabelsToAssign(ids.first, ids.second, -1L)); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/DrawerMenuInflater.java index 1683a8ef4..f85a5e490 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/DrawerMenuInflater.java @@ -1,79 +1,94 @@ -package it.niedermann.nextcloud.deck.util; +package it.niedermann.nextcloud.deck.ui.main; import android.view.Menu; import android.view.MenuItem; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatImageButton; import androidx.appcompat.widget.PopupMenu; -import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; +import java.util.HashMap; import java.util.List; +import java.util.Map; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener; import it.niedermann.nextcloud.deck.ui.board.DeleteBoardDialogFragment; -import it.niedermann.nextcloud.deck.ui.board.EditBoardDialogFragment; import it.niedermann.nextcloud.deck.ui.board.accesscontrol.AccessControlDialogFragment; +import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment; import it.niedermann.nextcloud.deck.ui.board.managelabels.ManageLabelsDialogFragment; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; -public class DrawerMenuUtil { +public class DrawerMenuInflater<T extends FragmentActivity & ArchiveBoardListener> { public static final int MENU_ID_ABOUT = -1; public static final int MENU_ID_ADD_BOARD = -2; public static final int MENU_ID_SETTINGS = -3; public static final int MENU_ID_ARCHIVED_BOARDS = -4; public static final int MENU_ID_UPCOMING_CARDS = -5; - private DrawerMenuUtil() { - throw new UnsupportedOperationException("This class must not get instantiated"); + private final T activity; + private final Menu menu; + + public DrawerMenuInflater(@NonNull T activity, @NonNull Menu menu) { + this.activity = activity; + this.menu = menu; } - public static <T extends FragmentActivity & ArchiveBoardListener> void inflateBoards( - @NonNull T context, - @NonNull Menu menu, - @NonNull List<Board> boards, + public Map<Integer, Long> inflateBoards( + @NonNull Account account, + @NonNull List<FullBoard> fullBoards, + @ColorInt int color, boolean hasArchivedBoards, boolean currentServerVersionIsSupported) { - menu.add(Menu.NONE, MENU_ID_UPCOMING_CARDS, Menu.NONE, R.string.widget_upcoming_title).setIcon(R.drawable.calendar_blank_grey600_24dp); + + final var utils = ThemeUtils.of(color, activity); + final var navigationMap = new HashMap<Integer, Long>(); + + menu.clear(); + menu.add(Menu.NONE, MENU_ID_UPCOMING_CARDS, Menu.NONE, R.string.widget_upcoming_title).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.calendar_blank_grey600_24dp)); + int index = 0; - for (final var board : boards) { + for (final var fullBoard : fullBoards) { + navigationMap.put(index, fullBoard.getLocalId()); final var menuItem = menu - .add(Menu.NONE, index++, Menu.NONE, board.getTitle()).setIcon(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, board.getColor())) + .add(Menu.NONE, index++, Menu.NONE, fullBoard.getBoard().getTitle()).setIcon(utils.deck.getColoredBoardDrawable(activity, fullBoard.getBoard().getColor())) .setCheckable(true); if (currentServerVersionIsSupported) { - if (board.isPermissionManage()) { - final var contextMenu = new AppCompatImageButton(context); + if (fullBoard.getBoard().isPermissionManage()) { + final var contextMenu = new AppCompatImageButton(activity); contextMenu.setBackgroundDrawable(null); - contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600))); + contextMenu.setImageDrawable(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_menu)); contextMenu.setOnClickListener((v) -> { - final var popup = new PopupMenu(context, contextMenu); + final var popup = new PopupMenu(activity, contextMenu); popup.getMenuInflater().inflate(R.menu.navigation_context_menu, popup.getMenu()); final int SHARE_BOARD_ID = -1; - if (board.isPermissionShare()) { + if (fullBoard.getBoard().isPermissionShare()) { popup.getMenu().add(Menu.NONE, SHARE_BOARD_ID, 5, R.string.share_board); } popup.setOnMenuItemClickListener((MenuItem item) -> { - final String editBoard = context.getString(R.string.edit_board); + final String editBoard = activity.getString(R.string.edit_board); int itemId = item.getItemId(); if (itemId == SHARE_BOARD_ID) { - AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName()); + AccessControlDialogFragment.newInstance(account, fullBoard.getLocalId()).show(activity.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName()); return true; } else if (itemId == R.id.edit_board) { - EditBoardDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard); + EditBoardDialogFragment.newInstance(account, fullBoard.getLocalId()).show(activity.getSupportFragmentManager(), editBoard); return true; } else if (itemId == R.id.manage_labels) { - ManageLabelsDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard); + ManageLabelsDialogFragment.newInstance(account, fullBoard.getLocalId()).show(activity.getSupportFragmentManager(), editBoard); return true; } else if (itemId == R.id.clone_board) { - context.onClone(board); + activity.onClone(account, fullBoard.getBoard()); return true; } else if (itemId == R.id.archive_board) { - context.onArchive(board); + activity.onArchive(fullBoard.getBoard()); return true; } else if (itemId == R.id.delete_board) { - DeleteBoardDialogFragment.newInstance(board).show(context.getSupportFragmentManager(), DeleteBoardDialogFragment.class.getCanonicalName()); + DeleteBoardDialogFragment.newInstance(fullBoard.getBoard()).show(activity.getSupportFragmentManager(), DeleteBoardDialogFragment.class.getCanonicalName()); return true; } return false; @@ -81,25 +96,27 @@ public class DrawerMenuUtil { popup.show(); }); menuItem.setActionView(contextMenu); - } else if (board.isPermissionShare()) { - final var contextMenu = new AppCompatImageButton(context); + } else if (fullBoard.getBoard().isPermissionShare()) { + final var contextMenu = new AppCompatImageButton(activity); contextMenu.setBackgroundDrawable(null); - contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, ContextCompat.getColor(context, R.color.grey600))); - contextMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName())); + contextMenu.setImageDrawable(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_share_grey600_18dp)); + contextMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(account, fullBoard.getLocalId()).show(activity.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName())); menuItem.setActionView(contextMenu); } } } if (hasArchivedBoards) { - menu.add(Menu.NONE, MENU_ID_ARCHIVED_BOARDS, Menu.NONE, R.string.archived_boards).setIcon(ViewUtil.getTintedImageView(context, R.drawable.ic_archive_white_24dp, ContextCompat.getColor(context, R.color.grey600))); + menu.add(Menu.NONE, MENU_ID_ARCHIVED_BOARDS, Menu.NONE, R.string.archived_boards).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_archive_white_24dp)); } if (currentServerVersionIsSupported) { - menu.add(Menu.NONE, MENU_ID_ADD_BOARD, Menu.NONE, R.string.add_board).setIcon(R.drawable.ic_add_grey_24dp); + menu.add(Menu.NONE, MENU_ID_ADD_BOARD, Menu.NONE, R.string.add_board).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_add_grey_24dp)); } - menu.add(Menu.NONE, MENU_ID_SETTINGS, Menu.NONE, R.string.simple_settings).setIcon(R.drawable.ic_settings_grey600_24dp); - menu.add(Menu.NONE, MENU_ID_ABOUT, Menu.NONE, R.string.about).setIcon(R.drawable.ic_info_outline_grey600_24dp); + menu.add(Menu.NONE, MENU_ID_SETTINGS, Menu.NONE, R.string.simple_settings).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_settings_grey600_24dp)); + menu.add(Menu.NONE, MENU_ID_ABOUT, Menu.NONE, R.string.about).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_info_outline_grey600_24dp)); + + return navigationMap; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java new file mode 100644 index 000000000..282b07dac --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java @@ -0,0 +1,928 @@ +package it.niedermann.nextcloud.deck.ui.main; + +import static java.util.Collections.emptyList; + +import android.animation.AnimatorInflater; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkRequest; +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; + +import androidx.activity.OnBackPressedCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.AnyThread; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.splashscreen.SplashScreen; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModelProvider; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.tabs.TabLayoutMediator; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; +import com.nextcloud.android.sso.exceptions.UnknownErrorException; + +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import it.niedermann.android.crosstabdnd.CrossTabDragAndDrop; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.android.tablayouthelper.TabLayoutHelper; +import it.niedermann.android.tablayouthelper.TabTitleGenerator; +import it.niedermann.android.util.ColorUtil; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.api.ResponseCallback; +import it.niedermann.nextcloud.deck.databinding.ActivityMainBinding; +import it.niedermann.nextcloud.deck.databinding.NavHeaderMainBinding; +import it.niedermann.nextcloud.deck.exceptions.OfflineException; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.internal.FilterInformation; +import it.niedermann.nextcloud.deck.model.ocs.Capabilities; +import it.niedermann.nextcloud.deck.model.ocs.Version; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.ImportAccountActivity; +import it.niedermann.nextcloud.deck.ui.StackChangeCallback; +import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog; +import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener; +import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener; +import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment; +import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardListener; +import it.niedermann.nextcloud.deck.ui.card.CardAdapter; +import it.niedermann.nextcloud.deck.ui.card.CreateCardListener; +import it.niedermann.nextcloud.deck.ui.card.NewCardDialog; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.ui.filter.FilterDialogFragment; +import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel; +import it.niedermann.nextcloud.deck.ui.settings.PreferencesViewModel; +import it.niedermann.nextcloud.deck.ui.stack.DeleteStackDialogFragment; +import it.niedermann.nextcloud.deck.ui.stack.DeleteStackListener; +import it.niedermann.nextcloud.deck.ui.stack.EditStackDialogFragment; +import it.niedermann.nextcloud.deck.ui.stack.EditStackListener; +import it.niedermann.nextcloud.deck.ui.stack.OnScrollListener; +import it.niedermann.nextcloud.deck.ui.stack.StackAdapter; +import it.niedermann.nextcloud.deck.ui.stack.StackFragment; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.ThemedSnackbar; +import it.niedermann.nextcloud.deck.util.CustomAppGlideModule; +import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher; + +public class MainActivity extends AppCompatActivity implements DeleteStackListener, EditStackListener, DeleteBoardListener, EditBoardListener, ArchiveBoardListener, OnScrollListener, CreateCardListener { + + protected ActivityMainBinding binding; + private NavHeaderMainBinding headerBinding; + private PreferencesViewModel preferencesViewModel; + protected MainViewModel mainViewModel; + private FilterViewModel filterViewModel; + private StackAdapter stackAdapter; + private DrawerMenuInflater<MainActivity> drawerMenuInflater; + private Menu listMenu; + private ConnectivityManager.NetworkCallback networkCallback; + private MainActivityNavigationHandler navigationHandler; + @Nullable + private TabLayoutMediator mediator; + @Nullable + private TabLayoutHelper tabLayoutHelper; + private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START); + } else if (binding.searchToolbar.getVisibility() == View.VISIBLE) { + hideFilterTextToolbar(); + } else { + finish(); + } + } + }; + + private final ActivityResultLauncher<Intent> importAccountLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() != RESULT_OK) { + finish(); + } + }); + + @Override + protected void onCreate(Bundle savedInstanceState) { + SplashScreen.installSplashScreen(this); + + super.onCreate(savedInstanceState); + + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); + + binding = ActivityMainBinding.inflate(getLayoutInflater()); + headerBinding = NavHeaderMainBinding.bind(binding.navigationView.getHeaderView(0)); + + setTheme(R.style.AppTheme); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + + final var toggle = new ActionBarDrawerToggle(this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + binding.drawerLayout.addDrawerListener(toggle); + toggle.syncState(); + + mainViewModel = new ViewModelProvider(this).get(MainViewModel.class); + preferencesViewModel = new ViewModelProvider(this).get(PreferencesViewModel.class); + filterViewModel = new ViewModelProvider(this).get(FilterViewModel.class); + + navigationHandler = new MainActivityNavigationHandler(this, binding.drawerLayout, mainViewModel::saveCurrentBoardId); + binding.navigationView.setNavigationItemSelectedListener(navigationHandler); + + stackAdapter = new StackAdapter(this); + binding.viewPager.setAdapter(stackAdapter); + binding.viewPager.setOffscreenPageLimit(2); + binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); + binding.filterText.addTextChangedListener(new OnTextChangedWatcher(filterViewModel::setFilterText)); + binding.enableSearch.setOnClickListener(v -> showFilterTextToolbar()); + binding.toolbar.setOnClickListener(v -> showFilterTextToolbar()); + binding.accountSwitcher.setOnClickListener(v -> AccountSwitcherDialog.newInstance().show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName())); + + headerBinding.copyDebugLogs.setOnClickListener((v) -> { + try { + DeckLog.shareLogAsFile(this); + } catch (Exception e) { + showExceptionDialog(e, null); + } + }); + + final var dragAndDrop = new CrossTabDragAndDrop<StackFragment, CardAdapter, FullCard>(getResources(), ViewCompat.getLayoutDirection(binding.getRoot()) == ViewCompat.LAYOUT_DIRECTION_LTR); + dragAndDrop.register(binding.viewPager, binding.stackTitles, getSupportFragmentManager()); + dragAndDrop.addItemMovedByDragListener((movedCard, stackId, position) -> { + mainViewModel.reorder(movedCard, stackId, position); + DeckLog.info("Card", movedCard.getCard().getTitle(), "was moved to Stack", stackId, "on position", position); + }); + + final var listMenuPopup = new PopupMenu(this, binding.listMenuButton); + listMenu = listMenuPopup.getMenu(); + getMenuInflater().inflate(R.menu.list_menu, listMenu); + listMenuPopup.setOnMenuItemClickListener(this::onOptionsItemSelected); + binding.listMenuButton.setOnClickListener((v) -> listMenuPopup.show()); + + getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); + + drawerMenuInflater = new DrawerMenuInflater<>(this, binding.navigationView.getMenu()); + + preferencesViewModel.isDebugModeEnabled$().observe(this, enabled -> headerBinding.copyDebugLogs.setVisibility(enabled ? View.VISIBLE : View.GONE)); + filterViewModel.hasActiveFilter().observe(this, hasActiveFilter -> binding.filterIndicator.setVisibility(hasActiveFilter ? View.VISIBLE : View.GONE)); + + // Flag to distinguish user initiated stack changes from stack changes derived by changing the board + final var boardChanged = new AtomicBoolean(true); + final var stackChangeCallback = new StackChangeCallback(stackAdapter, + binding.viewPager, + binding.fab, + binding.swipeRefreshLayout, + listMenu, + stack -> mainViewModel.saveCurrentStackId(stack.getAccountId(), stack.getBoardId(), stack.getLocalId())); + + final var hasAccounts$ = new ReactiveLiveData<>(mainViewModel.hasAccounts()); + + hasAccounts$ + .filter(hasAccounts -> !hasAccounts) + .observe(this, () -> importAccountLauncher.launch(ImportAccountActivity.createIntent(this))); + + hasAccounts$ + .filter(hasAccounts -> hasAccounts) + .tap(() -> binding.viewPager.unregisterOnPageChangeCallback(stackChangeCallback)) + .tap(() -> binding.viewPager.registerOnPageChangeCallback(stackChangeCallback)) + .flatMap(() -> mainViewModel.getCurrentAccount$()) + .flatMap(account -> { + try { + applyAccount(account); + } catch (NextcloudFilesAppAccountNotFoundException e) { + showExceptionDialog(e, account); + // There is not much we can do here. Hide everything because this Exception means that our SyncManager instance is invalid + applyBoards(account, false, emptyList()); + applyStacks(null, null, null); + return new MutableLiveData<>(); + } + applyAccountTheme(account.getColor()); + return new ReactiveLiveData<>(mainViewModel.getBoards(account.getId())) + .map(boardsAndArchived -> applyBoards(account, boardsAndArchived.second, boardsAndArchived.first)) + .flatMap(navigationMap -> new ReactiveLiveData<>(mainViewModel.getCurrentFullBoard(account.getId())) + .combineWith(() -> new MutableLiveData<>(navigationMap))) + .flatMap(args -> { + applyBoard(account, args.second, args.first); + @Nullable final var currentBoard = args.first; + if (currentBoard == null) { + applyStacks(null, null, emptyList()); + return new MutableLiveData<>(null); + } else { + return new ReactiveLiveData<>(mainViewModel.getStacks(account.getId(), currentBoard.getLocalId())) + .flatMap(stacks -> { + binding.viewPager.unregisterOnPageChangeCallback(stackChangeCallback); + boardChanged.set(true); + applyStacks(account, currentBoard.getLocalId(), stacks); + return mainViewModel.getCurrentStackId$(account.getId(), currentBoard.getLocalId()); + }); + + } + } + ); + }) + .observe(this, currentStackId -> { + stackChangeCallback.updateMoveItemVisibility(); + if (boardChanged.getAndSet(false)) { + applyStack(currentStackId); + binding.viewPager.registerOnPageChangeCallback(stackChangeCallback); + } + }); + } + + private void applyAccount(@NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + DeckLog.verbose("= Apply Account", account); + mainViewModel.recreateSyncManager(account); + registerAutoSyncOnNetworkAvailable(account); + navigationHandler.setCurrentAccount(account); + + if (account.isMaintenanceEnabled()) { + refreshCapabilities(account, null); + } + + Glide + .with(binding.accountSwitcher.getContext()) + .load(account.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", account.getName() + ":", account.isMaintenanceEnabled()); + binding.infoBox.setVisibility(account.isMaintenanceEnabled() ? View.VISIBLE : View.GONE); + if (account.getServerDeckVersionAsObject().isSupported()) { + binding.infoBoxVersionNotSupported.setVisibility(View.GONE); + } else { + binding.infoBoxVersionNotSupported.setText(getString(R.string.info_box_version_not_supported, account.getServerDeckVersionAsObject(), Version.minimumSupported().getOriginalVersion())); + binding.infoBoxVersionNotSupported.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(account.getUrl() + getString(R.string.url_fragment_update_deck))))); + binding.infoBoxVersionNotSupported.setVisibility(View.VISIBLE); + } + + binding.swipeRefreshLayout.setOnRefreshListener(() -> { + DeckLog.info("Triggered manual refresh"); + CustomAppGlideModule.clearCache(this); + + DeckLog.verbose("Trigger refresh capabilities for", account); + refreshCapabilities(account, () -> { + DeckLog.verbose("Trigger synchronization for", account); + mainViewModel.synchronize(account, new IResponseCallback<>() { + @Override + public void onResponse(Boolean response) { + DeckLog.info("End of synchronization for " + account + " → Stop spinner."); + runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false)); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + DeckLog.info("End of synchronization for " + account + " → Stop spinner."); + showSyncFailedSnackbar(account, throwable); + runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false)); + } + }); + }); + }); + } + + private Map<Integer, Long> applyBoards(@NonNull Account account, boolean hasArchivedBoards, @Nullable List<FullBoard> fullBoards) { + DeckLog.verbose("=== Apply Boards", fullBoards, "for", account); + filterViewModel.clearFilterInformation(true); + binding.navigationView.setItemIconTintList(null); + + final Map<Integer, Long> navigationMap; + + if (fullBoards == null || fullBoards.isEmpty()) { + binding.emptyContentViewBoards.setVisibility(View.VISIBLE); + navigationMap = drawerMenuInflater.inflateBoards(account, emptyList(), account.getColor(), hasArchivedBoards, account.getServerDeckVersionAsObject().isSupported()); + + } else { + binding.emptyContentViewBoards.setVisibility(View.GONE); + navigationMap = drawerMenuInflater.inflateBoards(account, fullBoards, account.getColor(), hasArchivedBoards, account.getServerDeckVersionAsObject().isSupported()); + } + + navigationHandler.updateNavigationMap(navigationMap); + return navigationMap; + } + + protected void applyBoard(@NonNull Account account, @NonNull Map<Integer, Long> navigationMap, @Nullable FullBoard currentBoard) { + DeckLog.verbose("===== Apply Board", currentBoard); + if (currentBoard == null) { + applyBoardTheme(account.getColor()); + showEditButtonsIfPermissionsGranted(false, false); + + binding.toolbar.setTitle(R.string.app_name_short); + binding.filterText.setHint(R.string.app_name_short); + binding.fab.setText(R.string.add_board); + binding.fab.setOnClickListener(v -> { + binding.fab.hide(); + EditBoardDialogFragment.newInstance(account).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName()); + }); + } else { + applyBoardTheme(currentBoard.getBoard().getColor()); + showEditButtonsIfPermissionsGranted(true, currentBoard.board.isPermissionEdit()); + + binding.toolbar.setTitle(currentBoard.getBoard().getTitle()); + binding.filterText.setHint(getString(R.string.search_in, currentBoard.getBoard().getTitle())); + binding.fab.setText(R.string.add_list); + binding.fab.setOnClickListener(v -> { + binding.fab.hide(); + EditStackDialogFragment.newInstance(currentBoard.getAccountId(), currentBoard.getLocalId()).show(getSupportFragmentManager(), EditStackDialogFragment.class.getSimpleName()); + }); + + navigationMap + .entrySet() + .stream() + .filter(entry -> currentBoard.getLocalId().equals(entry.getValue())) + .map(Map.Entry::getKey) + .findFirst() + .ifPresent(menuItemId -> binding.navigationView.setCheckedItem(menuItemId)); + } + + } + + private void applyStacks(@Nullable Account account, @Nullable Long boardId, @Nullable List<Stack> stacks) { + DeckLog.verbose("======= Apply Stacks", stacks, "for Board", boardId); + final boolean noStacksAvailable = stacks == null || stacks.isEmpty(); + + listMenu.findItem(R.id.archive_cards).setVisible(!noStacksAvailable); + listMenu.findItem(R.id.rename_list).setVisible(!noStacksAvailable); + listMenu.findItem(R.id.delete_list).setVisible(!noStacksAvailable); + + if (account == null || noStacksAvailable) { + binding.emptyContentViewStacks.setVisibility(View.VISIBLE); + + stackAdapter.setStacks(account, boardId, emptyList()); + setStackMediator(new TabLayoutMediator(binding.stackTitles, binding.viewPager, (tab, position) -> tab.setText("ERROR"))); + } else { + binding.emptyContentViewStacks.setVisibility(View.GONE); + + binding.fab.setText(R.string.add_card); + binding.fab.setOnClickListener(v -> { + binding.fab.hide(); + final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); + NewCardDialog.newInstance(account, stack.getBoardId(), stack.getLocalId()).show(getSupportFragmentManager(), NewCardDialog.class.getSimpleName()); + }); + + stackAdapter.setStacks(account, boardId, stacks); + + final TabTitleGenerator tabTitleGenerator = position -> { + if (stacks.size() > position) { + return stacks.get(position).getTitle(); + } else { + DeckLog.warn("Could not generate tab title for position " + position + " because list size is only " + stacks.size()); + return "ERROR"; + } + }; + setStackMediator(new TabLayoutMediator(binding.stackTitles, binding.viewPager, (tab, position) -> tab.setText(tabTitleGenerator.getTitle(position)))); + updateTabLayoutHelper(tabTitleGenerator); + } + } + + private void applyStack(@Nullable Long stackId) { + DeckLog.verbose("========= Apply Stack", stackId); + if (stackId != null) { + try { + binding.viewPager.setCurrentItem(stackAdapter.getPosition(stackId), false); + } catch (NoSuchElementException e) { + DeckLog.warn(e); + } + } + } + + private void applyBoardTheme(@ColorInt int color) { + final var utils = ThemeUtils.of(color, this); + + utils.deck.themeTabLayout(binding.stackTitles); + utils.material.themeExtendedFAB(binding.fab); + utils.androidx.themeSwipeRefreshLayout(binding.swipeRefreshLayout); + utils.platform.colorEditText(binding.filterText); + utils.platform.tintDrawable(this, binding.filterIndicator.getDrawable()); + binding.emptyContentViewStacks.applyTheme(color); + } + + private void applyAccountTheme(@ColorInt int accountColor) { + final var utils = ThemeUtils.of(accountColor, this); + + utils.platform.colorNavigationView(binding.navigationView, false); + binding.emptyContentViewBoards.applyTheme(accountColor); + + @ColorInt final int headerTextColor = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(accountColor); + headerBinding.headerView.setBackgroundColor(accountColor); + headerBinding.appName.setTextColor(headerTextColor); + DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor); + DrawableCompat.setTint(headerBinding.copyDebugLogs.getDrawable(), headerTextColor); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + this.binding = null; + this.headerBinding = null; + if (tabLayoutHelper != null) { + tabLayoutHelper.release(); + } + } + + @Override + public void onCreateStack(long accountId, long boardId, String stackName) { + mainViewModel.createStack(accountId, boardId, stackName, new IResponseCallback<>() { + @Override + public void onResponse(FullStack response) { + binding.viewPager.post(() -> { + try { + binding.viewPager.setCurrentItem(stackAdapter.getPosition(response.getLocalId())); + mainViewModel.saveCurrentStackId(response.getEntity().getAccountId(), response.getEntity().getBoardId(), response.getEntity().getLocalId()); + } catch (NoSuchElementException e) { + DeckLog.logError(e); + } + }); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + mainViewModel.getCurrentBoardColor(accountId, boardId) + .thenAcceptAsync(color -> ThemedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(throwable.getLocalizedMessage()), Snackbar.LENGTH_LONG, color) + .setAction(R.string.simple_more, v -> showExceptionDialog(throwable, accountId)) + .setAnchorView(binding.fab) + .show(), ContextCompat.getMainExecutor(MainActivity.this)); + } + }); + } + + @Override + public void onUpdateStack(long localStackId, String stackName) { + mainViewModel.updateStackTitle(localStackId, stackName, new IResponseCallback<>() { + @Override + public void onResponse(FullStack response) { + DeckLog.info("Successfully updated", Stack.class.getSimpleName(), "to", stackName); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, null); + } + }); + } + + @Override + public void onCreateBoard(@NonNull Account account, String title, @ColorInt int color) { + final var boardToCreate = new Board(title, color); + boardToCreate.setPermissionEdit(true); + boardToCreate.setPermissionManage(true); + + mainViewModel.createBoard(account, boardToCreate, new IResponseCallback<>() { + @Override + public void onResponse(FullBoard response) { + runOnUiThread(() -> { + if (response != null) { + mainViewModel.saveCurrentBoardId(response.getAccountId(), response.getLocalId()); + EditStackDialogFragment.newInstance(response.getAccountId(), response.getLocalId()).show(getSupportFragmentManager(), EditStackDialogFragment.class.getSimpleName()); + } + }); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + ThemedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG, account.getColor()) + .setAction(R.string.simple_more, v -> showExceptionDialog(throwable, account)) + .setAnchorView(binding.fab) + .show(); + } + }); + } + + @Override + public void onUpdateBoard(FullBoard fullBoard) { + mainViewModel.updateBoard(fullBoard, new IResponseCallback<>() { + @Override + public void onResponse(FullBoard response) { + DeckLog.info("Successfully updated board", fullBoard.getBoard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, fullBoard.getAccountId()); + } + }); + } + + private void refreshCapabilities(final Account account, @Nullable Runnable runAfter) { + DeckLog.verbose("Refreshing capabilities for", account.getName()); + mainViewModel.refreshCapabilities(new ResponseCallback<>(account) { + @Override + public void onResponse(Capabilities response) { + DeckLog.verbose("Finished refreshing capabilities for", account.getName(), "successfully."); + if (response.isMaintenanceEnabled()) { + DeckLog.verbose("Maintenance mode is enabled."); + } else { + DeckLog.verbose("Maintenance mode is disabled."); + // 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 (account.getServerDeckVersionAsObject().isSupported() && !response.getDeckVersion().isSupported()) { + ActivityCompat.recreate(MainActivity.this); + } + } + + if (runAfter != null) { + runAfter.run(); + } + } + + @Override + public void onError(Throwable throwable) { + DeckLog.warn("Error on refreshing capabilities for", account.getName(), "(" + throwable.getMessage() + ")."); + if (throwable.getClass() == OfflineException.class || 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()); + } + + if (runAfter != null) { + runAfter.run(); + } + } + }); + } + + @UiThread + private void updateTabLayoutHelper(@NonNull TabTitleGenerator tabTitleGenerator) { + if (this.tabLayoutHelper == null) { + this.tabLayoutHelper = new TabLayoutHelper(binding.stackTitles, binding.viewPager, tabTitleGenerator); + } else { + tabLayoutHelper.setTabTitleGenerator(tabTitleGenerator); + } + } + + @UiThread + private void setStackMediator(@NonNull final TabLayoutMediator newMediator) { + if (mediator != null) { + mediator.detach(); + } + newMediator.attach(); + this.mediator = newMediator; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.archive_cards) { + final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); + final var stackLocalId = stack.getLocalId(); + mainViewModel.countCardsInStack(stack.getAccountId(), stackLocalId, numberOfCards -> runOnUiThread(() -> + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.archive_cards) + .setMessage(getString(FilterInformation.hasActiveFilter(filterViewModel.getFilterInformation().getValue()) + ? R.string.do_you_want_to_archive_all_cards_of_the_filtered_list + : R.string.do_you_want_to_archive_all_cards_of_the_list, stack.getTitle())) + .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> { + final var filterInformation = filterViewModel.getFilterInformation().getValue(); + mainViewModel.archiveCardsInStack(stack.getAccountId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation, new IResponseCallback<>() { + @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.isNoOnVoidError(throwable)) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, stack.getAccountId()); + } + } + }); + }) + .setNeutralButton(android.R.string.cancel, null) + .create() + .show() + )); + return true; + } else if (itemId == R.id.add_list) { + final Long accountId = stackAdapter.getAccount() == null ? null : stackAdapter.getAccount().getId(); + if (accountId == null) { + DeckLog.warn("Can not launch stack dialog: accountId of stackAdapter is null."); + return false; + } + final Long boardId = stackAdapter.getBoardId(); + if (boardId == null) { + DeckLog.warn("Can not launch stack dialog: boardId of stackAdapter is null."); + return false; + } + EditStackDialogFragment.newInstance(accountId, boardId).show(getSupportFragmentManager(), EditStackDialogFragment.class.getSimpleName()); + return true; + } else if (itemId == R.id.rename_list) { + final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); + new ReactiveLiveData<>(mainViewModel.getStack(stack.getAccountId(), stack.getLocalId())) + .observeOnce(MainActivity.this, fullStack -> EditStackDialogFragment + .newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle()) + .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); + return true; + } else if (itemId == R.id.move_list_left || itemId == R.id.move_list_right) { + final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); + // TODO error handling + mainViewModel.reorderStack(stack.getAccountId(), stack.getBoardId(), stack.getLocalId(), itemId == R.id.move_list_right); + return true; + } else if (itemId == R.id.delete_list) { + final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); + mainViewModel.countCardsInStack(stack.getAccountId(), stack.getLocalId(), numberOfCards -> runOnUiThread(() -> { + if (numberOfCards != null && numberOfCards > 0) { + DeleteStackDialogFragment.newInstance(stack.getAccountId(), stack.getBoardId(), stack.getLocalId(), numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName()); + } else { + onDeleteStack(stack.getAccountId(), stack.getBoardId(), stack.getLocalId()); + } + })); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void showEditButtonsIfPermissionsGranted(boolean currentBoardIsAvailable, boolean currentBoardHasEditPermission) { + if (currentBoardIsAvailable) { + if (!currentBoardHasEditPermission) { + binding.fab.hide(); + binding.listMenuButton.setVisibility(View.GONE); + binding.emptyContentViewStacks.hideDescription(); + } else { + binding.fab.show(); + binding.listMenuButton.setVisibility(View.VISIBLE); + binding.emptyContentViewStacks.showDescription(); + } + } else { + binding.fab.show(); + binding.listMenuButton.setVisibility(View.GONE); + binding.emptyContentViewStacks.showDescription(); + } + } + + @Override + public void onScrollUp() { + binding.fab.extend(); + } + + @Override + public void onScrollDown() { + binding.fab.shrink(); + } + + @Override + public void onBottomReached() { + binding.fab.extend(); + } + + private void showFilterTextToolbar() { + binding.toolbar.setVisibility(View.GONE); + binding.searchToolbar.setVisibility(View.VISIBLE); + binding.searchToolbar.setNavigationOnClickListener(v1 -> onBackPressedCallback.handleOnBackPressed()); + binding.enableSearch.setVisibility(View.GONE); + binding.filterText.requestFocus(); + final var imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(binding.filterText, InputMethodManager.SHOW_IMPLICIT); + binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_on)); + } + + private void hideFilterTextToolbar() { + binding.filterText.setText(null); + final var imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); + binding.searchToolbar.setVisibility(View.GONE); + binding.enableSearch.setVisibility(View.VISIBLE); + binding.toolbar.setVisibility(View.VISIBLE); + binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_off)); + } + + private void registerAutoSyncOnNetworkAvailable(@NonNull Account account) { + final var connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + final var builder = new NetworkRequest.Builder(); + + if (connectivityManager != null) { + if (networkCallback != null) { + connectivityManager.unregisterNetworkCallback(networkCallback); + } + + networkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + DeckLog.log("Got Network connection"); + mainViewModel.synchronize(account, new IResponseCallback<>() { + @Override + public void onResponse(Boolean response) { + DeckLog.log("Auto-Sync after connection available successful"); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + if (throwable.getClass() == OfflineException.class || throwable instanceof OfflineException) { + DeckLog.error("Do not show sync failed snackbar because it is an ", OfflineException.class.getSimpleName(), "- assuming the user has wi-fi disabled but \"Sync only on wi-fi\" enabled"); + } else if (throwable.getClass() == UnknownErrorException.class || throwable instanceof UnknownErrorException) { + DeckLog.error("Do not show sync failed snackbar because it is an ", UnknownErrorException.class.getSimpleName(), "- assuming a not reachable server or infrastructure issues"); + } else { + showSyncFailedSnackbar(account, throwable); + } + } + }); + } + + @Override + public void onLost(@NonNull Network network) { + DeckLog.log("Network lost"); + } + }; + connectivityManager.registerNetworkCallback(builder.build(), networkCallback); + } + } + + /** + * Find a StackFragment by it's ID, may return null. + * + * @param stackId ID of the stack to find + * @return Instance of StackFragment + */ + @Nullable + public StackFragment findStackFragmentById(long stackId) { + return (StackFragment) getSupportFragmentManager().findFragmentByTag("f" + stackId); + } + + /** + * This method is called when a new Card is created + * + * @param createdCard The new Card's data + */ + @Override + public void onCardCreated(FullCard createdCard) { + final var card = createdCard.getCard(); + DeckLog.log("Card Created! Title:" + card.getTitle() + " in stack ID: " + card.getStackId()); + + // Scroll the given StackFragment to the bottom, so the new Card is in view. + final var fragment = findStackFragmentById(card.getStackId()); + if (fragment != null) { + fragment.scrollToBottom(); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + this.binding.fab.show(); + } + + @Override + public void onDeleteStack(long accountId, long boardId, long stackId) { + mainViewModel.deleteStack(accountId, boardId, stackId, new IResponseCallback<>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted stack with local id", stackId, "and remote id", stackId); + } + + @Override + public void onError(Throwable throwable) { + if (SyncManager.isNoOnVoidError(throwable)) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, accountId); + } + } + }); + } + + @Override + public void onBoardDeleted(Board board) { + mainViewModel.deleteBoard(board, new IResponseCallback<>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted board", board.getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (SyncManager.isNoOnVoidError(throwable)) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, board.getAccountId()); + } + } + }); + + binding.drawerLayout.closeDrawer(GravityCompat.START); + } + + @Override + public void onArchive(@NonNull Board board) { + mainViewModel.archiveBoard(board, new IResponseCallback<>() { + @Override + public void onResponse(FullBoard response) { + DeckLog.info("Successfully archived board", board.getTitle()); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, board.getAccountId()); + } + }); + } + + @Override + public void onClone(@NonNull Account account, @NonNull Board board) { + final String[] cloneOptions = {getString(R.string.clone_cards)}; + final boolean[] checkedItems = {false}; + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.clone_board) + .setMultiChoiceItems(cloneOptions, checkedItems, (dialog, which, isChecked) -> checkedItems[0] = isChecked) + .setPositiveButton(R.string.simple_clone, (dialog, which) -> { + binding.drawerLayout.closeDrawer(GravityCompat.START); + final var snackbar = ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.cloning_board, board.getTitle()), Snackbar.LENGTH_INDEFINITE, board.getColor()) + .setAnchorView(binding.fab); + snackbar.show(); + mainViewModel.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0], new IResponseCallback<>() { + @Override + public void onResponse(FullBoard response) { + runOnUiThread(() -> { + snackbar.dismiss(); + mainViewModel.saveCurrentBoardId(response.getAccountId(), response.getLocalId()); + ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.successfully_cloned_board, response.getBoard().getTitle()), Snackbar.LENGTH_LONG, response.getBoard().getColor()) + .setAction(R.string.edit, v -> EditBoardDialogFragment.newInstance(account, response.getLocalId()).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName())) + .setAnchorView(binding.fab) + .show(); + }); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + runOnUiThread(() -> { + snackbar.dismiss(); + showExceptionDialog(throwable, board.getAccountId()); + }); + } + }); + }) + .setNeutralButton(android.R.string.cancel, null) + .show(); + } + + + /** + * Displays a {@link ThemedSnackbar} for an exception of a failed sync, but only if the cause wasn't maintenance mode (this should be handled by a TextView instead of a snackbar). + * + * @param throwable the cause of the failed sync + */ + @AnyThread + private void showSyncFailedSnackbar(@NonNull Account account, @NonNull Throwable throwable) { + if (!(throwable instanceof NextcloudHttpRequestFailedException) || ((NextcloudHttpRequestFailedException) throwable).getStatusCode() != HttpURLConnection.HTTP_UNAVAILABLE) { + runOnUiThread(() -> { + if (binding != null) { // Can be null in case the activity has been destroyed before the synchronization process has been finished + ThemedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG, account.getColor()) + .setAction(R.string.simple_more, v -> showExceptionDialog(throwable, account)) + .setAnchorView(binding.fab) + .show(); + } + }); + } + } + + @AnyThread + protected void showExceptionDialog(@NonNull Throwable throwable, long accountId) { + mainViewModel.getAccount(accountId).thenAccept(account -> showExceptionDialog(throwable, account)); + } + + @AnyThread + protected void showExceptionDialog(@NonNull Throwable throwable, @Nullable Account account) { + runOnUiThread(() -> ExceptionDialogFragment + .newInstance(throwable, account) + .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + } + +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivityNavigationHandler.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivityNavigationHandler.java new file mode 100644 index 000000000..6850b70fb --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivityNavigationHandler.java @@ -0,0 +1,120 @@ +package it.niedermann.nextcloud.deck.ui.main; + +import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_ABOUT; +import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_ADD_BOARD; +import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_ARCHIVED_BOARDS; +import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_SETTINGS; +import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_UPCOMING_CARDS; + +import android.app.Activity; +import android.content.Intent; +import android.view.MenuItem; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; + +import com.google.android.material.navigation.NavigationView; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.ui.about.AboutActivity; +import it.niedermann.nextcloud.deck.ui.archivedboards.ArchivedBoardsActivity; +import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment; +import it.niedermann.nextcloud.deck.ui.settings.SettingsActivity; +import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsActivity; + +public class MainActivityNavigationHandler implements NavigationView.OnNavigationItemSelectedListener { + + /** + * Keys: {@link MenuItem#getItemId()} + * Values: {@link FullBoard#getLocalId()} + */ + private final Map<Integer, Long> navigationMap = new HashMap<>(); + private final AppCompatActivity activity; + private final DrawerLayout drawerLayout; + private final BiConsumer<Long, Long> onBoardSelected; + private final ActivityResultLauncher<Intent> settingsLauncher; + @Nullable + private Account account = null; + + public MainActivityNavigationHandler( + @NonNull AppCompatActivity activity, + @NonNull DrawerLayout drawerLayout, + @NonNull BiConsumer<Long, Long> onBoardSelected + ) { + this.activity = activity; + this.drawerLayout = drawerLayout; + this.onBoardSelected = onBoardSelected; + this.settingsLauncher = activity.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + ActivityCompat.recreate(activity); + } + }); + } + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case MENU_ID_ABOUT: + if (account == null) { + DeckLog.warn("Current account is null, can not launch dialog to create board."); + return false; + } + activity.startActivity(AboutActivity.createIntent(activity, account)); + break; + case MENU_ID_SETTINGS: + if (account == null) { + DeckLog.warn("Current account is null, can not launch dialog to create board."); + return false; + } + settingsLauncher.launch(SettingsActivity.createIntent(activity, account)); + break; + case MENU_ID_ADD_BOARD: + if (account == null) { + DeckLog.warn("Current account is null, can not launch dialog to create board."); + return false; + } + EditBoardDialogFragment.newInstance(account).show(activity.getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName()); + break; + case MENU_ID_ARCHIVED_BOARDS: + if (account == null) { + DeckLog.warn("Current account is null, can not launch dialog to create board."); + return false; + } + activity.startActivity(ArchivedBoardsActivity.createIntent(activity, account)); + break; + case MENU_ID_UPCOMING_CARDS: + activity.startActivity(UpcomingCardsActivity.createIntent(activity)); + break; + default: + if (account == null) { + DeckLog.warn("Current account is null, can not launch dialog to create board."); + return false; + } + onBoardSelected.accept(account.getId(), navigationMap.get(item.getItemId())); + break; + } + drawerLayout.closeDrawer(GravityCompat.START); + return true; + } + + public void updateNavigationMap(@NonNull Map<Integer, Long> navigationMap) { + this.navigationMap.clear(); + this.navigationMap.putAll(navigationMap); + } + + public void setCurrentAccount(@NonNull Account account) { + this.account = account; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java new file mode 100644 index 000000000..38edb933d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java @@ -0,0 +1,256 @@ +package it.niedermann.nextcloud.deck.ui.main; + +import static java.util.concurrent.CompletableFuture.supplyAsync; + +import android.app.Application; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import java.io.File; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +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; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.internal.FilterInformation; +import it.niedermann.nextcloud.deck.model.ocs.Capabilities; +import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; + +@SuppressWarnings("WeakerAccess") +public class MainViewModel extends BaseViewModel { + + @Nullable + private SyncManager syncManager; + + public MainViewModel(@NonNull Application application) { + super(application); + } + + public void recreateSyncManager(@NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + try { + this.syncManager = new SyncManager(getApplication(), account); + } catch (NextcloudFilesAppAccountNotFoundException e) { + this.syncManager = null; + throw e; + } + } + + private Exception getInvalidSyncManagerException() { + return new IllegalStateException("SyncManager is null"); + } + + public void synchronize(@NonNull Account account, @NonNull IResponseCallback<Boolean> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.synchronize(ResponseCallback.from(account, callback)); + } + } + + public void refreshCapabilities(@NonNull ResponseCallback<Capabilities> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.refreshCapabilities(callback); + } + } + + public LiveData<Boolean> hasAccounts() { + return baseRepository.hasAccounts(); + } + + public CompletableFuture<Account> getAccount(long accountId) { + return supplyAsync(() -> baseRepository.readAccountDirectly(accountId), executor); + } + + public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) { + return baseRepository.getCurrentBoardColor(accountId, boardId); + } + + public void saveCurrentBoardId(long accountId, long boardId) { + baseRepository.saveCurrentBoardId(accountId, boardId); + } + + public void createBoard(@NonNull Account account, @NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.createBoard(account, board, callback); + } + } + + public void updateBoard(@NonNull FullBoard board, @NonNull IResponseCallback<FullBoard> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.updateBoard(board, callback); + } + } + + public void archiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.archiveBoard(board, callback); + } + } + + public void cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards, @NonNull IResponseCallback<FullBoard> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards, callback); + } + } + + public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback<Void> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.deleteBoard(board, callback); + } + } + + public void saveCurrentStackId(long accountId, long boardId, long stackId) { + baseRepository.saveCurrentStackId(accountId, boardId, stackId); + } + + public void createStack(long accountId, long boardId, @NonNull String title, @NonNull IResponseCallback<FullStack> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.createStack(accountId, boardId, title, callback); + } + } + + public LiveData<FullStack> getStack(long accountId, long localStackId) { + if (syncManager == null) { + return new MutableLiveData<>(); + } + return syncManager.getStack(accountId, localStackId); + } + + public void reorderStack(long accountId, long boardId, long stackLocalId, boolean moveToRight) { + if (syncManager == null) { + DeckLog.logError(getInvalidSyncManagerException()); + } else { + syncManager.reorderStack(accountId, boardId, stackLocalId, moveToRight); + } + } + + public void updateStackTitle(long localStackId, @NonNull String newTitle, @NonNull IResponseCallback<FullStack> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.updateStackTitle(localStackId, newTitle, callback); + } + } + + public void deleteStack(long accountId, long boardId, long stackLocalId, @NonNull IResponseCallback<Void> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.deleteStack(accountId, boardId, stackLocalId, callback); + } + } + + public void reorder(@NonNull FullCard movedCard, long newStackId, int newIndex) { + if (syncManager == null) { + DeckLog.logError(getInvalidSyncManagerException()); + } else { + syncManager.reorder(movedCard.getAccountId(), movedCard, newStackId, newIndex); + } + } + + public void countCardsInStack(long accountId, long stackId, @NonNull IResponseCallback<Integer> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.countCardsInStackDirectly(accountId, stackId, callback); + } + } + + public void archiveCardsInStack(long accountId, long stackId, @NonNull FilterInformation filterInformation, @NonNull IResponseCallback<Void> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.archiveCardsInStack(accountId, stackId, filterInformation, callback); + } + } + + public void updateCard(@NonNull FullCard fullCard, @NonNull IResponseCallback<FullCard> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.updateCard(fullCard, callback); + } + } + + public void addCommentToCard(long accountId, String message, long cardId) { + if (syncManager == null) { + DeckLog.logError(getInvalidSyncManagerException()); + } else { + supplyAsync(() -> syncManager.readAccountDirectly(accountId)) + .thenAcceptAsync(account -> syncManager.addCommentToCard(account.getId(), cardId, new DeckComment(message, account.getUserName(), Instant.now()))); + } + } + + public void addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file, @NonNull IResponseCallback<Attachment> callback) { + if (syncManager == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file, callback); + } + } + + public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) { + if (syncManager == null) { + DeckLog.logError(getInvalidSyncManagerException()); + } else { + syncManager.addOrUpdateSingleCardWidget(widgetId, accountId, boardId, localCardId); + } + } + + public LiveData<Account> getCurrentAccount$() { + return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$()) + .flatMap(baseRepository::readAccount); + } + + public LiveData<Pair<List<FullBoard>, Boolean>> getBoards(long accountId) { + return new ReactiveLiveData<>(baseRepository.getFullBoards(accountId, false)) + .combineWith(() -> baseRepository.hasArchivedBoards(accountId)); + } + + public LiveData<FullBoard> getCurrentFullBoard(long accountId) { + return new ReactiveLiveData<>(baseRepository.getCurrentBoardId$(accountId)) + .flatMap(boardId -> baseRepository.getFullBoardById(accountId, boardId)); + } + + public LiveData<List<Stack>> getStacks(long accountId, long boardId) { + return new ReactiveLiveData<>(baseRepository.getStacksForBoard(accountId, boardId)) + .distinctUntilChanged(); + } + + public LiveData<Long> getCurrentStackId$(long accountId, long boardId) { + return baseRepository.getCurrentStackId$(accountId, boardId); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java index 1eabbd1b6..3a3465099 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java @@ -1,6 +1,10 @@ package it.niedermann.nextcloud.deck.ui.manageaccounts; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + import android.net.Uri; +import android.os.Build; import android.text.TextUtils; import android.view.View; @@ -16,10 +20,7 @@ import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; - -import static android.view.View.GONE; -import static android.view.View.VISIBLE; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; public class ManageAccountViewHolder extends RecyclerView.ViewHolder { @@ -38,7 +39,7 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder { ); binding.accountHost.setText(Uri.parse(account.getUrl()).getHost()); Glide.with(itemView.getContext()) - .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) + .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) @@ -56,5 +57,10 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder { } else { binding.currentAccountIndicator.setVisibility(GONE); } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final var utils = ThemeUtils.of(account.getColor(), itemView.getContext()); + utils.deck.colorSelectedCheck(binding.currentAccountIndicator.getContext(), binding.currentAccountIndicator.getDrawable()); + } } } 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 a843551c1..d1ce2e14e 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,5 @@ package it.niedermann.nextcloud.deck.ui.manageaccounts; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId; -import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; - import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -11,8 +8,10 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.databinding.ActivityManageAccountsBinding; import it.niedermann.nextcloud.deck.model.Account; @@ -34,7 +33,7 @@ public class ManageAccountsActivity extends AppCompatActivity { setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - adapter = new ManageAccountAdapter((account) -> viewModel.setNewAccount(account), (accountPair) -> { + adapter = new ManageAccountAdapter((account) -> viewModel.saveCurrentAccount(account), (accountPair) -> { if (accountPair.first != null) { viewModel.deleteAccount(accountPair.first.getId()); } else { @@ -42,29 +41,25 @@ public class ManageAccountsActivity extends AppCompatActivity { } Account newAccount = accountPair.second; if (newAccount != null) { - viewModel.setNewAccount(newAccount); + viewModel.saveCurrentAccount(newAccount); } else { Log.i(TAG, "Got delete account request, but new account is null. Maybe last account has been deleted?"); } }); binding.accounts.setAdapter(adapter); - observeOnce(viewModel.readAccount(readCurrentAccountId(this)), this, (account -> { - adapter.setCurrentAccount(account); - viewModel.readAccounts().observe(this, (localAccounts -> { - if (localAccounts.size() == 0) { - Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName()); - finish(); - } else { - adapter.setAccounts(localAccounts); - } - })); - })); - } - - @Override - public void onBackPressed() { - onSupportNavigateUp(); + viewModel.getCurrentAccountId().thenAcceptAsync(accountId -> new ReactiveLiveData<>(viewModel.readAccount(accountId)) + .observeOnce(this, account -> { + adapter.setCurrentAccount(account); + viewModel.readAccounts().observe(this, (localAccounts -> { + if (localAccounts.size() == 0) { + Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName()); + finish(); + } else { + adapter.setAccounts(localAccounts); + } + })); + }), ContextCompat.getMainExecutor(this)); } @Override 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 d97cf6b16..567732ddf 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 @@ -1,42 +1,40 @@ package it.niedermann.nextcloud.deck.ui.manageaccounts; -import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccount; - import android.app.Application; import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import java.util.List; +import java.util.concurrent.CompletableFuture; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; @SuppressWarnings("WeakerAccess") -public class ManageAccountsViewModel extends AndroidViewModel { - - private SyncManager syncManager; +public class ManageAccountsViewModel extends BaseViewModel { public ManageAccountsViewModel(@NonNull Application application) { super(application); - this.syncManager = new SyncManager(application); } public LiveData<Account> readAccount(long id) { - return syncManager.readAccount(id); + return baseRepository.readAccount(id); } public LiveData<List<Account>> readAccounts() { - return syncManager.readAccounts(); + return baseRepository.readAccounts(); + } + + public CompletableFuture<Long> getCurrentAccountId() { + return baseRepository.getCurrentAccountId(); } - public void setNewAccount(@NonNull Account account) { - saveCurrentAccount(getApplication(), account); - syncManager = new SyncManager(getApplication()); + public void saveCurrentAccount(@NonNull Account account) { + baseRepository.saveCurrentAccount(account); } public void deleteAccount(long id) { - syncManager.deleteAccount(id); + baseRepository.deleteAccount(id); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java index 5dd430d1d..8656d5bbb 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 @@ -3,16 +3,21 @@ package it.niedermann.nextcloud.deck.ui.movecard; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import android.app.Dialog; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogMoveCardBinding; @@ -23,9 +28,9 @@ import it.niedermann.nextcloud.deck.ui.pickstack.PickStackFragment; import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener; import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; -import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment; +import it.niedermann.nextcloud.deck.ui.theme.Themed; -public class MoveCardDialogFragment extends ThemedDialogFragment implements PickStackListener { +public class MoveCardDialogFragment extends DialogFragment implements Themed, PickStackListener { private static final String KEY_ORIGIN_ACCOUNT_ID = "account_id"; private static final String KEY_ORIGIN_BOARD_LOCAL_ID = "board_local_id"; @@ -37,6 +42,7 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick private String originCardTitle; private Long originCardLocalId; private boolean originCardHasAttachmentsOrComments; + private View dialogView; private DialogMoveCardBinding binding; private PickStackViewModel viewModel; @@ -74,10 +80,12 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick originCardTitle = args.getString(KEY_ORIGIN_CARD_TITLE); } - @Nullable + @NonNull @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - binding = DialogMoveCardBinding.inflate(inflater); + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final var dialogBuilder = new MaterialAlertDialogBuilder(requireContext()); + + binding = DialogMoveCardBinding.inflate(LayoutInflater.from(requireContext())); 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()); @@ -85,14 +93,24 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick dismiss(); }); binding.cancel.setOnClickListener((v) -> dismiss()); - return binding.getRoot(); + dialogView = binding.getRoot(); + return dialogBuilder + .setView(dialogView) + .create(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return this.dialogView; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); getChildFragmentManager() .beginTransaction() - .add(R.id.fragment_container, PickStackFragment.newInstance(false)) + .replace(R.id.fragment_container, PickStackFragment.newInstance(false), PickStackFragment.class.getSimpleName()) .commit(); } @@ -106,12 +124,13 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) { this.selectedAccount = account; this.selectedBoard = board; + this.selectedStack = stack; - if (board != null) { - applyTheme(board.getColor()); - } + applyTheme(board == null + ? ContextCompat.getColor(requireContext(), R.color.accent) + : board.getColor() + ); - this.selectedStack = stack; if (board == null || stack == null) { binding.submit.setEnabled(false); binding.moveWarning.setVisibility(GONE); @@ -122,7 +141,7 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick } @Override - public void applyTheme(int color) { + public void applyTheme(@ColorInt int color) { final var utils = ThemeUtils.of(color, requireContext()); utils.material.colorMaterialButtonText(binding.cancel); 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 23ed6fd6d..5784cbdf4 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,9 +1,6 @@ package it.niedermann.nextcloud.deck.ui.pickstack; -import static androidx.lifecycle.Transformations.switchMap; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentBoardId; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentStackId; +import static java.util.Collections.emptyList; import android.content.Context; import android.os.Bundle; @@ -14,24 +11,30 @@ import android.widget.ArrayAdapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; import androidx.fragment.app.Fragment; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.databinding.FragmentPickStackBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.Stack; -import it.niedermann.nextcloud.deck.ui.ImportAccountActivity; import it.niedermann.nextcloud.deck.ui.preparecreate.AccountAdapter; import it.niedermann.nextcloud.deck.ui.preparecreate.BoardAdapter; +import it.niedermann.nextcloud.deck.ui.preparecreate.PickStackAdapter; import it.niedermann.nextcloud.deck.ui.preparecreate.SelectedListener; -import it.niedermann.nextcloud.deck.ui.preparecreate.StackAdapter; +import it.niedermann.nextcloud.deck.ui.theme.Themed; -public class PickStackFragment extends Fragment { +public class PickStackFragment extends Fragment implements Themed, PickStackListener { private FragmentPickStackBinding binding; private PickStackViewModel viewModel; @@ -41,68 +44,15 @@ public class PickStackFragment extends Fragment { private PickStackListener pickStackListener; private boolean showBoardsWithoutEditPermission = false; - private long lastAccountId; - private long lastBoardId; - private long lastStackId; - private ArrayAdapter<Account> accountAdapter; - private ArrayAdapter<Board> boardAdapter; - private ArrayAdapter<Stack> stackAdapter; + private ArrayAdapter<Account> pickAccountAdapter; + private ArrayAdapter<Board> pickBoardAdapter; + private PickStackAdapter pickStackAdapter; - @Nullable - private LiveData<List<Board>> boardsLiveData; - @NonNull - private final Observer<List<Board>> boardsObserver = (boards) -> { - boardAdapter.clear(); - boardAdapter.addAll(boards); - binding.boardSelect.setEnabled(true); - - if (boards.size() > 0) { - binding.boardSelect.setEnabled(true); - - Board boardToSelect = null; - for (final var board : boards) { - if (board.getLocalId() == lastBoardId) { - boardToSelect = board; - break; - } - } - if (boardToSelect == null) { - boardToSelect = boards.get(0); - } - binding.boardSelect.setSelection(boardAdapter.getPosition(boardToSelect)); - } else { - binding.boardSelect.setEnabled(false); - pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), null, null); - } - }; - - @Nullable - private LiveData<List<Stack>> stacksLiveData; - @NonNull - private final Observer<List<Stack>> stacksObserver = (stacks) -> { - stackAdapter.clear(); - stackAdapter.addAll(stacks); - - if (stacks.size() > 0) { - binding.stackSelect.setEnabled(true); - - Stack stackToSelect = null; - for (final var stack : stacks) { - if (stack.getLocalId() == lastStackId) { - stackToSelect = stack; - break; - } - } - if (stackToSelect == null) { - stackToSelect = stacks.get(0); - } - binding.stackSelect.setSelection(stackAdapter.getPosition(stackToSelect)); - } else { - binding.stackSelect.setEnabled(false); - pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), null); - } - }; + private final ReactiveLiveData<Void> selectionChanged$ = new ReactiveLiveData<>(); + private final AtomicReference<Long> selectedAccount = new AtomicReference<>(); + private final Map<Long, Long> selectedBoard = new HashMap<>(); + private final Map<Pair<Long, Long>, Long> selectedStack = new HashMap<>(); @Override public void onAttach(@NonNull Context context) { @@ -127,74 +77,202 @@ public class PickStackFragment extends Fragment { binding = FragmentPickStackBinding.inflate(getLayoutInflater()); viewModel = new ViewModelProvider(requireActivity()).get(PickStackViewModel.class); - accountAdapter = new AccountAdapter(requireContext()); - binding.accountSelect.setAdapter(accountAdapter); + pickAccountAdapter = new AccountAdapter(requireContext()); + binding.accountSelect.setAdapter(pickAccountAdapter); binding.accountSelect.setEnabled(false); - boardAdapter = new BoardAdapter(requireContext()); - binding.boardSelect.setAdapter(boardAdapter); - binding.stackSelect.setEnabled(false); - stackAdapter = new StackAdapter(requireContext()); - binding.stackSelect.setAdapter(stackAdapter); - binding.stackSelect.setEnabled(false); - - switchMap(viewModel.hasAccounts(), hasAccounts -> { - if (hasAccounts) { - return viewModel.readAccounts(); - } else { - // TODO After successfully importing the account, the creation will throw a TokenMissMatchException - Recreate SyncManager? - startActivity(ImportAccountActivity.createIntent(requireContext())); - return null; - } - }).observe(getViewLifecycleOwner(), (List<Account> accounts) -> { - if (accounts == null || accounts.size() == 0) { - throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry"); - } - - lastAccountId = readCurrentAccountId(requireContext()); - lastBoardId = readCurrentBoardId(requireContext(), lastAccountId); - lastStackId = readCurrentStackId(requireContext(), lastAccountId, lastBoardId); - - accountAdapter.clear(); - accountAdapter.addAll(accounts); - binding.accountSelect.setEnabled(true); + binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { + selectedAccount.set(parent.getSelectedItemId()); + selectionChanged$.setValue(null); + }); - for (Account account : accounts) { - if (account.getId() == lastAccountId) { - binding.accountSelect.setSelection(accountAdapter.getPosition(account)); - break; - } - } + pickBoardAdapter = new BoardAdapter(requireContext()); + binding.boardSelect.setAdapter(pickBoardAdapter); + binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { + selectedBoard.put(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId()); + selectionChanged$.setValue(null); }); - binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> - updateLiveDataSource(boardsLiveData, boardsObserver, showBoardsWithoutEditPermission - ? viewModel.getBoards(parent.getSelectedItemId()) - : viewModel.getBoardsWithEditPermission(parent.getSelectedItemId()))); + pickStackAdapter = new PickStackAdapter(stack -> { + selectedStack.put(new Pair<>(binding.accountSelect.getSelectedItemId(), binding.boardSelect.getSelectedItemId()), stack.getLocalId()); + selectionChanged$.setValue(null); + }); + binding.stackSelect.setAdapter(pickStackAdapter); + + selectionChanged$ + .flatMap(() -> viewModel.readAccounts()) + .flatMap(accounts -> { + binding.accountSelect.setEnabled(false); + binding.boardSelect.setEnabled(false); + setAccounts(accounts); + + return getSelectedAccount() + .map(accountIdToSelect -> selectAccount(accounts, accountIdToSelect)) + .flatMap(accountId -> getBoards(accountId, showBoardsWithoutEditPermission) + .flatMap(boards -> { + binding.boardSelect.setEnabled(false); + setBoards(boards); - binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> - updateLiveDataSource(stacksLiveData, stacksObserver, viewModel.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId()))); + return getSelectedBoard(accountId) + .map(boardId -> selectBoard(boards, boardId)) + .flatMap(boardId -> getStacks(accountId, boardId) + .flatMap(stacks -> { + setStacks(stacks); - binding.stackSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> - pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), (Stack) parent.getSelectedItem())); + return getSelectedStack(accountId, boardId) + .map(stackId -> selectStack(stacks, stackId)); + })); + })); + + }).observe(this); + + selectionChanged$.setValue(null); return binding.getRoot(); } + private ReactiveLiveData<Long> getSelectedAccount() { + if (selectedAccount.get() == null) { + return new ReactiveLiveData<>(viewModel.getCurrentAccountId$()); + } else { + return new ReactiveLiveData<>(selectedAccount.get()); + } + } + + private void setAccounts(@NonNull Collection<Account> accounts) { + pickAccountAdapter.clear(); + pickAccountAdapter.addAll(accounts); + + if (accounts.size() > 1) { + binding.accountSelect.setVisibility(View.VISIBLE); + binding.accountSelect.setEnabled(true); + } else { + binding.accountSelect.setVisibility(View.GONE); + } + } + + + @Nullable + private Long selectAccount(@NonNull Collection<Account> accounts, @Nullable Long accountIdToSelect) { + final var matchingAccount = accounts + .stream() + .filter(account -> Objects.equals(account.getId(), accountIdToSelect)) + .findAny() + .or(() -> accounts.stream().findAny()); + + if (matchingAccount.isPresent()) { + binding.accountSelect.setSelection(pickAccountAdapter.getPosition(matchingAccount.get())); + return matchingAccount.get().getId(); + } else { + return null; + } + } + + private ReactiveLiveData<Long> getSelectedBoard(@Nullable Long accountId) { + if (accountId == null) { + return new ReactiveLiveData<>(null); + } else if (selectedBoard.containsKey(accountId)) { + return new ReactiveLiveData<>(Objects.requireNonNull(selectedBoard.get(accountId))); + } else { + return new ReactiveLiveData<>(viewModel.getCurrentBoardId$(accountId)); + } + } + + private ReactiveLiveData<List<Board>> getBoards(@Nullable Long accountId, boolean showBoardsWithoutEditPermission) { + if (accountId == null) { + return new ReactiveLiveData<>(emptyList()); + } else if (showBoardsWithoutEditPermission) { + return new ReactiveLiveData<>(viewModel.getNotArchivedBoards(accountId)); + } else { + return new ReactiveLiveData<>(viewModel.getBoardsWithEditPermission(accountId)); + } + } + + private void setBoards(@NonNull Collection<Board> boards) { + pickBoardAdapter.clear(); + pickBoardAdapter.addAll(boards); + + if (boards.size() > 1) { + binding.boardSelect.setVisibility(View.VISIBLE); + binding.boardSelect.setEnabled(true); + } else { + binding.boardSelect.setVisibility(View.GONE); + } + } + + @Nullable + private Long selectBoard(@NonNull Collection<Board> boards, @Nullable Long boardIdToSelect) { + final var matchingBoard = boards + .stream() + .filter(board -> Objects.equals(board.getLocalId(), boardIdToSelect)) + .findAny() + .or(() -> boards.stream().findAny()); + + if (matchingBoard.isPresent()) { + binding.boardSelect.setSelection(pickBoardAdapter.getPosition(matchingBoard.get())); + applyTheme(matchingBoard.get().getColor()); + return matchingBoard.get().getLocalId(); + } else { + onStackPicked((Account) binding.accountSelect.getSelectedItem(), null, null); + return null; + } + } + + private ReactiveLiveData<List<Stack>> getStacks(@Nullable Long accountId, @Nullable Long boardId) { + if (accountId == null || boardId == null) { + return new ReactiveLiveData<>(emptyList()); + } else { + return new ReactiveLiveData<>(viewModel.getStacksForBoard(accountId, boardId)); + } + } + + private void setStacks(@NonNull Collection<Stack> stacks) { + pickStackAdapter.setStacks(stacks); + } + + private ReactiveLiveData<Long> getSelectedStack(@Nullable Long accountId, @Nullable Long boardId) { + if (selectedStack.containsKey(new Pair<>(accountId, boardId))) { + return new ReactiveLiveData<>(Objects.requireNonNull(selectedStack.get(new Pair<>(accountId, boardId)))); + } else if (accountId == null || boardId == null) { + return new ReactiveLiveData<>(null); + } else { + return new ReactiveLiveData<>(viewModel.getCurrentStackId$(accountId, boardId)); + } + } + + private Long selectStack(@NonNull Collection<Stack> stacks, @Nullable Long stackIdToSelect) { + final var matchingStack = stacks + .stream() + .filter(stack -> Objects.equals(stack.getLocalId(), stackIdToSelect)) + .findAny() + .or(() -> stacks.stream().findAny()); + + if (matchingStack.isPresent()) { + pickStackAdapter.setSelection(matchingStack.get()); + onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), matchingStack.get()); + return matchingStack.get().getLocalId(); + } else { + onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), null); + return null; + } + } + + @Override + public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) { + DeckLog.verbose("Picked account", account.getName()); + DeckLog.verbose("Picked board", board == null ? "null" : board.getTitle()); + DeckLog.verbose("Picked stack", stack == null ? "null" : stack.getTitle()); + pickStackListener.onStackPicked(account, board, stack); + } + @Override public void onDestroy() { super.onDestroy(); this.binding = null; } - /** - * Updates the source of the given liveData and de- and reregisters the given observer. - */ - private <T> void updateLiveDataSource(@Nullable LiveData<T> liveData, Observer<T> observer, LiveData<T> newSource) { - if (liveData != null) { - liveData.removeObserver(observer); - } - liveData = newSource; - liveData.observe(getViewLifecycleOwner(), observer); + @Override + public void applyTheme(int color) { + pickStackAdapter.applyTheme(color); } public static PickStackFragment newInstance(boolean showBoardsWithoutEditPermission) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java index 4999ec6f4..c4bbcb205 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java @@ -1,10 +1,11 @@ package it.niedermann.nextcloud.deck.ui.pickstack; +import static androidx.lifecycle.Transformations.distinctUntilChanged; + import android.app.Application; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -13,14 +14,10 @@ import java.util.List; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.Stack; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; - -import static androidx.lifecycle.Transformations.distinctUntilChanged; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; @SuppressWarnings("WeakerAccess") -public class PickStackViewModel extends AndroidViewModel { - - private final SyncManager syncManager; +public class PickStackViewModel extends BaseViewModel { private Account selectedAccount; @Nullable @@ -34,7 +31,18 @@ public class PickStackViewModel extends AndroidViewModel { public PickStackViewModel(@NonNull Application application) { super(application); - this.syncManager = new SyncManager(application); + } + + public LiveData<Long> getCurrentAccountId$() { + return baseRepository.getCurrentAccountId$(); + } + + public LiveData<Long> getCurrentBoardId$(long accountId) { + return baseRepository.getCurrentBoardId$(accountId); + } + + public LiveData<Long> getCurrentStackId$(long accountId, long boardId) { + return baseRepository.getCurrentStackId$(accountId, boardId); } public LiveData<Boolean> submitButtonEnabled() { @@ -81,22 +89,22 @@ public class PickStackViewModel extends AndroidViewModel { } public LiveData<Boolean> hasAccounts() { - return syncManager.hasAccounts(); + return baseRepository.hasAccounts(); } public LiveData<List<Account>> readAccounts() { - return syncManager.readAccounts(); + return baseRepository.readAccounts(); } - public LiveData<List<Board>> getBoards(long accountId) { - return syncManager.getBoards(accountId); + public LiveData<List<Board>> getNotArchivedBoards(long accountId) { + return baseRepository.getBoards(accountId, false); } public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) { - return syncManager.getBoardsWithEditPermission(accountId); + return baseRepository.getBoardsWithEditPermission(accountId); } public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { - return syncManager.getStacksForBoard(accountId, localBoardId); + return baseRepository.getStacksForBoard(accountId, localBoardId); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java index 78dff7711..bf919cd66 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 @@ -16,7 +16,6 @@ import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateAccountBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; public class AccountAdapter extends AbstractAdapter<Account> { @@ -40,17 +39,17 @@ public class AccountAdapter extends AbstractAdapter<Account> { binding = ItemPrepareCreateAccountBinding.bind(convertView); } - final var item = getItem(position); - if (item != null) { - binding.username.setText(item.getUserDisplayName()); + final var account = getItem(position); + if (account != null) { + binding.username.setText(account.getUserDisplayName()); try { - binding.instance.setText(new URL(item.getUrl()).getHost()); + binding.instance.setText(new URL(account.getUrl()).getHost()); } catch (Throwable t) { - binding.instance.setText(item.getUrl()); + binding.instance.setText(account.getUrl()); } Glide.with(getContext()) - .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size)))) + .load(account.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/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java index e0a7c68c0..87713a105 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java @@ -5,12 +5,13 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateBoardBinding; import it.niedermann.nextcloud.deck.model.Board; -import it.niedermann.nextcloud.deck.util.ViewUtil; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; public class BoardAdapter extends AbstractAdapter<Board> { @@ -37,7 +38,8 @@ public class BoardAdapter extends AbstractAdapter<Board> { final var board = getItem(position); if (board != null) { binding.boardTitle.setText(board.getTitle()); - binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, board.getColor())); + final var utils = ThemeUtils.of(ContextCompat.getColor(getContext(), R.color.defaultBrand), getContext()); + binding.avatar.setImageDrawable(utils.deck.getColoredBoardDrawable(binding.avatar.getContext(), board.getColor())); } else { DeckLog.logError(new IllegalArgumentException("No item for position " + position)); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackAdapter.java new file mode 100644 index 000000000..ab25c8ba9 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackAdapter.java @@ -0,0 +1,95 @@ +package it.niedermann.nextcloud.deck.ui.preparecreate; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.ui.theme.Themed; + +public class PickStackAdapter extends RecyclerView.Adapter<PickStackViewHolder> implements Themed { + + @Nullable + @ColorInt + Integer color; + @Nullable + private Stack selectedStack = null; + private final List<Stack> stacks = new ArrayList<>(); + @NonNull + private final Consumer<Stack> onStackSelectedListener; + + public PickStackAdapter(@NonNull Consumer<Stack> onStackSelectedListener) { + this.onStackSelectedListener = onStackSelectedListener; + setHasStableIds(true); + } + + @NonNull + @Override + public PickStackViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new PickStackViewHolder(ItemPrepareCreateStackBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull PickStackViewHolder holder, int position) { + holder.bind(stacks.get(position), stack -> { + setSelection(stack); + onStackSelectedListener.accept(selectedStack); + }, selectedStack, color); + } + + @Override + public int getItemCount() { + return stacks.size(); + } + + @Override + public long getItemId(int position) { + return stacks.get(position).getLocalId(); + } + + public void setStacks(@NonNull Collection<Stack> stacks) { + this.stacks.clear(); + this.selectedStack = null; + this.stacks.addAll(stacks); + notifyDataSetChanged(); + } + + public void setSelection(@NonNull Stack stack) { + final var previousPosition = getPosition(selectedStack); + final var nextPosition = getPosition(stack); + this.selectedStack = stack; + previousPosition.ifPresent(this::notifyItemChanged); + nextPosition.ifPresent(this::notifyItemChanged); + } + + private Optional<Integer> getPosition(@Nullable Stack stack) { + if (stack == null) { + return Optional.empty(); + } + + for (int i = 0; i < stacks.size(); i++) { + if (stacks.get(i).getLocalId().equals(stack.getLocalId())) { + return Optional.of(i); + } + } + + return Optional.empty(); + } + + @Override + public void applyTheme(int color) { + this.color = color; + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackViewHolder.java new file mode 100644 index 000000000..499c55871 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackViewHolder.java @@ -0,0 +1,46 @@ +package it.niedermann.nextcloud.deck.ui.preparecreate; + +import android.os.Build; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.function.Consumer; + +import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; + +class PickStackViewHolder extends RecyclerView.ViewHolder implements Themed { + + private final ItemPrepareCreateStackBinding binding; + + public PickStackViewHolder(@NonNull ItemPrepareCreateStackBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Stack stack, @NonNull Consumer<Stack> onStackSelected, @Nullable Stack selectedStack, @Nullable @ColorInt Integer color) { + binding.stackTitle.setText(stack.getTitle()); + itemView.setSelected(stack.getLocalId().equals(selectedStack == null ? -1 : selectedStack.getLocalId())); + itemView.setOnClickListener(view -> { + if (!itemView.isSelected()) { + onStackSelected.accept(stack); + } + }); + if (color != null) { + applyTheme(color); + } + } + + @Override + public void applyTheme(@ColorInt int color) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final var utils = ThemeUtils.of(color, itemView.getContext()); + utils.deck.colorSelectedCheck(binding.selectedCheck.getContext(), binding.selectedCheck.getDrawable()); + } + } +}
\ No newline at end of file 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 659190cab..c27a05006 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,5 @@ package it.niedermann.nextcloud.deck.ui.preparecreate; -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 android.annotation.SuppressLint; import android.content.Intent; import android.os.Bundle; @@ -58,9 +54,9 @@ public class PrepareCreateActivity extends PickStackActivity { viewModel.saveCard(account, boardId, stackId, fullCard, new IResponseCallback<>() { @Override public void onResponse(FullCard response) { - saveCurrentAccount(PrepareCreateActivity.this, account); - saveCurrentBoardId(PrepareCreateActivity.this, account.getId(), boardId); - saveCurrentStackId(PrepareCreateActivity.this, account.getId(), boardId, stackId); + viewModel.saveCurrentAccount(account); + viewModel.saveCurrentBoardId(account.getId(), boardId); + viewModel.saveCurrentStackId(account.getId(), boardId, stackId); callback.onResponse(null); startActivity(EditActivity.createEditCardIntent(PrepareCreateActivity.this, account, boardId, response.getLocalId())); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java index fb3bd5c91..90524c22a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java @@ -5,7 +5,8 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.AndroidViewModel; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.Account; @@ -13,16 +14,33 @@ 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.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; @SuppressWarnings("WeakerAccess") -public class PrepareCreateViewModel extends AndroidViewModel { +public class PrepareCreateViewModel extends BaseViewModel { public PrepareCreateViewModel(@NonNull Application application) { super(application); } + public void saveCurrentAccount(@NonNull Account account) { + baseRepository.saveCurrentAccount(account); + } + + public void saveCurrentBoardId(long accountId, long boardId) { + baseRepository.saveCurrentBoardId(accountId, boardId); + } + + public void saveCurrentStackId(long accountId, long boardId, long stackId) { + baseRepository.saveCurrentStackId(accountId, boardId, stackId); + } + public void saveCard(@NonNull Account account, long boardLocalId, long stackLocalId, @NonNull FullCard fullCard, @NonNull IResponseCallback<FullCard> callback) { - new SyncManager(getApplication(), account.getName()).createFullCard(account.getId(), boardLocalId, stackLocalId, fullCard, callback); + try { + new SyncManager(getApplication(), account).createFullCard(account.getId(), boardLocalId, stackLocalId, fullCard, callback); + } catch (NextcloudFilesAppAccountNotFoundException e) { + callback.onError(e); + } } @SuppressWarnings("ConstantConditions") diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java deleted file mode 100644 index e3e0ad5b5..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java +++ /dev/null @@ -1,44 +0,0 @@ -package it.niedermann.nextcloud.deck.ui.preparecreate; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; - -import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding; -import it.niedermann.nextcloud.deck.model.Stack; - -public class StackAdapter extends AbstractAdapter<Stack> { - - @SuppressWarnings("WeakerAccess") - public StackAdapter(@NonNull Context context) { - super(context, R.layout.item_prepare_create_stack); - } - - @Override - protected long getItemId(@NonNull Stack item) { - return item.getLocalId(); - } - - @NonNull - @Override - public View getView(int position, View convertView, @NonNull ViewGroup parent) { - final ItemPrepareCreateStackBinding binding; - if (convertView == null) { - binding = ItemPrepareCreateStackBinding.inflate(inflater, parent, false); - } else { - binding = ItemPrepareCreateStackBinding.bind(convertView); - } - - final Stack item = getItem(position); - if (item != null) { - binding.stackTitle.setText(item.getTitle()); - } else { - DeckLog.logError(new IllegalArgumentException("No item for position " + position)); - } - return binding.getRoot(); - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/PreferencesViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/PreferencesViewModel.java new file mode 100644 index 000000000..298beebb4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/PreferencesViewModel.java @@ -0,0 +1,39 @@ +package it.niedermann.nextcloud.deck.ui.settings; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import it.niedermann.nextcloud.deck.persistence.PreferencesRepository; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; + +public class PreferencesViewModel extends BaseViewModel { + + private final PreferencesRepository preferencesRepository; + + public PreferencesViewModel(@NonNull Application application) { + this(application, new PreferencesRepository(application)); + } + + public PreferencesViewModel(@NonNull Application application, @NonNull PreferencesRepository preferencesRepository) { + super(application); + this.preferencesRepository = preferencesRepository; + } + + public LiveData<Long> getCurrentAccountId$() { + return baseRepository.getCurrentAccountId$(); + } + + public LiveData<Integer> getAccountColor(long accountId) { + return baseRepository.getAccountColor(accountId); + } + + public void setAppTheme(int setting) { + preferencesRepository.setAppTheme(setting); + } + + public LiveData<Boolean> isDebugModeEnabled$() { + return preferencesRepository.isDebugModeEnabled$(); + } +} 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 07df2ac20..172b31fd1 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 @@ -8,12 +8,15 @@ 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.model.Account; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; -public class SettingsActivity extends AppCompatActivity { +public class SettingsActivity extends AppCompatActivity implements Themed { + private static final String KEY_ACCOUNT = "account"; private ActivitySettingsBinding binding; @Override @@ -21,16 +24,18 @@ public class SettingsActivity extends AppCompatActivity { super.onCreate(savedInstanceState); Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + if (!getIntent().hasExtra(KEY_ACCOUNT)) { + throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided"); + } + + final var account = (Account) getIntent().getSerializableExtra(KEY_ACCOUNT); + binding = ActivitySettingsBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); + applyTheme(account.getColor()); setSupportActionBar(binding.toolbar); - - setResult(RESULT_OK); - getSupportFragmentManager() - .beginTransaction() - .add(R.id.settings_layout, new SettingsFragment()) - .commit(); + setContentView(binding.getRoot()); + setResult(RESULT_CANCELED); } @Override @@ -45,8 +50,17 @@ public class SettingsActivity extends AppCompatActivity { this.binding = null; } + @Override + public void applyTheme(int color) { + final var utils = ThemeUtils.of(color, this); + +// utils.platform.themeStatusBar(this); +// utils.material.themeToolbar(binding.toolbar); + } + @NonNull - public static Intent createIntent(@NonNull Context context) { - return new Intent(context, SettingsActivity.class); + public static Intent createIntent(@NonNull Context context, @NonNull Account account) { + return new Intent(context, SettingsActivity.class) + .putExtra(KEY_ACCOUNT, account); } } 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 cc01781e6..79a03ab71 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,7 +1,5 @@ package it.niedermann.nextcloud.deck.ui.settings; -import static it.niedermann.nextcloud.deck.DeckApplication.setAppTheme; - import android.app.Activity; import android.os.Bundle; import android.view.View; @@ -9,10 +7,13 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; +import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; -import it.niedermann.nextcloud.deck.DeckApplication; +import java.util.stream.Stream; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker; @@ -20,6 +21,7 @@ import it.niedermann.nextcloud.deck.ui.theme.ThemedSwitchPreference; public class SettingsFragment extends PreferenceFragmentCompat { + private PreferencesViewModel preferencesViewModel; private ThemedSwitchPreference wifiOnlyPref; private ThemedSwitchPreference compactPref; private ThemedSwitchPreference coverImagesPref; @@ -31,6 +33,8 @@ public class SettingsFragment extends PreferenceFragmentCompat { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.settings, rootKey); + preferencesViewModel = new ViewModelProvider(requireActivity()).get(PreferencesViewModel.class); + wifiOnlyPref = findPreference(getString(R.string.pref_key_wifi_only)); coverImagesPref = findPreference(getString(R.string.pref_key_cover_images)); compactPref = findPreference(getString(R.string.pref_key_compact)); @@ -61,7 +65,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { final var themePref = findPreference(getString(R.string.pref_key_dark_theme)); if (themePref != null) { themePref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { - setAppTheme(Integer.parseInt((String) newValue)); + preferencesViewModel.setAppTheme(Integer.parseInt((String) newValue)); requireActivity().setResult(Activity.RESULT_OK); ActivityCompat.recreate(requireActivity()); return true; @@ -75,13 +79,15 @@ public class SettingsFragment extends PreferenceFragmentCompat { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - DeckApplication.readCurrentAccountColor().observe(getViewLifecycleOwner(), (mainColor) -> { - wifiOnlyPref.applyTheme(mainColor); - compactPref.applyTheme(mainColor); - coverImagesPref.applyTheme(mainColor); - compressImageAttachmentsPref.applyTheme(mainColor); - debuggingPref.applyTheme(mainColor); - eTagPref.applyTheme(mainColor); - }); + new ReactiveLiveData<>(preferencesViewModel.getCurrentAccountId$()) + .flatMap(preferencesViewModel::getAccountColor) + .observe(getViewLifecycleOwner(), color -> Stream.of( + wifiOnlyPref, + compactPref, + coverImagesPref, + compressImageAttachmentsPref, + debuggingPref, + eTagPref) + .forEach(pref -> pref.applyTheme(color))); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java index 852735352..c0ab59e75 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java @@ -49,9 +49,9 @@ public class ShareProgressDialogFragment extends ThemedDialogFragment { public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - viewModel.getMax().observe(requireActivity(), (nextMax) -> binding.progress.setMax(nextMax)); + viewModel.getMax().observe(this, binding.progress::setMax); - viewModel.getProgress().observe(requireActivity(), (progress) -> { + viewModel.getProgress().observe(this, progress -> { binding.progress.setProgress(progress); binding.progressText.setText(getString(R.string.progress_count, progress, viewModel.getMaxValue())); final Integer currentMaxValue = viewModel.getMaxValue(); @@ -63,7 +63,7 @@ public class ShareProgressDialogFragment extends ThemedDialogFragment { } }); - viewModel.getExceptions().observe(requireActivity(), (exceptions) -> { + viewModel.getExceptions().observe(this, (exceptions) -> { final int exceptionsCount = exceptions.size(); if (exceptionsCount > 0) { binding.errorCounter.setText(getResources().getQuantityString(R.plurals.progress_error_count, exceptionsCount, exceptionsCount)); @@ -81,7 +81,7 @@ public class ShareProgressDialogFragment extends ThemedDialogFragment { } }); - viewModel.getDuplicateAttachments().observe(requireActivity(), (duplicates) -> { + viewModel.getDuplicateAttachments().observe(this, (duplicates) -> { final int duplicatesCount = duplicates.size(); if (duplicatesCount > 0) { final var params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1f); 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 30e002c9c..b7547be0b 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 @@ -8,7 +8,6 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; -import android.view.Menu; import android.widget.Toast; import androidx.annotation.NonNull; @@ -19,22 +18,21 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import java.io.File; -import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.exceptions.UploadAttachmentFailedException; +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.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; -import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; -import it.niedermann.nextcloud.deck.ui.MainActivity; import it.niedermann.nextcloud.deck.ui.card.SelectCardListener; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; +import it.niedermann.nextcloud.deck.ui.main.MainActivity; import it.niedermann.nextcloud.deck.util.MimeTypeUtil; public class ShareTargetActivity extends MainActivity implements SelectCardListener { @@ -76,12 +74,12 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe binding.toolbar.setSubtitle(receivedText); } } catch (Throwable throwable) { - ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + showExceptionDialog(throwable, null); } } @Override - public void onCardSelected(FullCard fullCard) { + public void onCardSelected(@NonNull FullCard fullCard, long boardId) { if (cardSelected) { return; } @@ -94,7 +92,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe } } catch (Throwable throwable) { cardSelected = false; - ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + showExceptionDialog(throwable, fullCard.getAccountId()); } } @@ -175,15 +173,13 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe IResponseCallback.super.onError(throwable); runOnUiThread(() -> { cardSelected = false; - ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + showExceptionDialog(throwable, fullCard.getAccountId()); }); } }); break; case 1: - final var currentAccount = mainViewModel.getCurrentAccount(); - final var comment = new DeckComment(receivedText.trim(), currentAccount.getUserName(), Instant.now()); - mainViewModel.addCommentToCard(currentAccount.getId(), fullCard.getLocalId(), comment); + mainViewModel.addCommentToCard(fullCard.getAccountId(), receivedText.trim(), fullCard.getLocalId()); Toast.makeText(getApplicationContext(), getString(R.string.share_success, "\"" + receivedText + "\"", "\"" + fullCard.getCard().getTitle() + "\""), Toast.LENGTH_LONG).show(); finish(); break; @@ -192,14 +188,8 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe } @Override - protected void setCurrentBoard(@NonNull Board board) { - super.setCurrentBoard(board); + protected void applyBoard(@NonNull Account account, @NonNull Map<Integer, Long> navigationMap, @Nullable FullBoard currentBoard) { + super.applyBoard(account, navigationMap, currentBoard); binding.toolbar.setTitle(R.string.simple_select); - showEditButtonsIfPermissionsGranted(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return true; } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java index 4120e5e00..65e449686 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java @@ -9,13 +9,18 @@ import androidx.fragment.app.DialogFragment; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.ui.theme.DeleteAlertDialogBuilder; +import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment; -public class DeleteStackDialogFragment extends DialogFragment { +public class DeleteStackDialogFragment extends ThemedDialogFragment { + private static final String KEY_ACCOUNT_ID = "account_id"; + private static final String KEY_BOARD_ID = "board_id"; private static final String KEY_STACK_ID = "stack_id"; private static final String KEY_NUMBER_CARDS = "number_cards"; private DeleteStackListener deleteStackListener; + private long accountId; + private long boardId; private long stackId; private int numberCards; @@ -30,12 +35,14 @@ public class DeleteStackDialogFragment extends DialogFragment { final Bundle args = getArguments(); - if (args == null || !args.containsKey(KEY_STACK_ID) || !args.containsKey(KEY_NUMBER_CARDS)) { - throw new IllegalArgumentException("Please provide at least " + KEY_STACK_ID + " and " + KEY_NUMBER_CARDS + " as arguments"); - } else { - this.stackId = args.getLong(KEY_STACK_ID); - this.numberCards = args.getInt(KEY_NUMBER_CARDS); + if (args == null || !args.containsKey(KEY_ACCOUNT_ID) || !args.containsKey(KEY_BOARD_ID) || !args.containsKey(KEY_STACK_ID) || !args.containsKey(KEY_NUMBER_CARDS)) { + throw new IllegalArgumentException("Please provide at least " + KEY_ACCOUNT_ID + ", " + KEY_BOARD_ID + ", " + KEY_STACK_ID + " and " + KEY_NUMBER_CARDS + " as arguments"); } + + this.accountId = args.getLong(KEY_ACCOUNT_ID); + this.boardId = args.getLong(KEY_BOARD_ID); + this.stackId = args.getLong(KEY_STACK_ID); + this.numberCards = args.getInt(KEY_NUMBER_CARDS); } @NonNull @@ -44,15 +51,22 @@ public class DeleteStackDialogFragment extends DialogFragment { return new DeleteAlertDialogBuilder(requireContext()) .setTitle(R.string.delete_list) .setMessage(getResources().getQuantityString(R.plurals.do_you_want_to_delete_the_current_list, numberCards, numberCards)) - .setPositiveButton(R.string.simple_delete, (dialog, whichButton) -> deleteStackListener.onStackDeleted(stackId)) + .setPositiveButton(R.string.simple_delete, (dialog, whichButton) -> deleteStackListener.onDeleteStack(accountId, boardId, stackId)) .setNeutralButton(android.R.string.cancel, null) .create(); } - public static DialogFragment newInstance(long stackId, int numberCards) { + @Override + public void applyTheme(int color) { + + } + + public static DialogFragment newInstance(long accountId, long boardId, long stackId, int numberCards) { final var dialog = new DeleteStackDialogFragment(); final var args = new Bundle(); + args.putLong(KEY_ACCOUNT_ID, accountId); + args.putLong(KEY_BOARD_ID, boardId); args.putLong(KEY_STACK_ID, stackId); args.putInt(KEY_NUMBER_CARDS, numberCards); dialog.setArguments(args); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java index f0b5d68f7..032140722 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java @@ -1,5 +1,5 @@ package it.niedermann.nextcloud.deck.ui.stack; public interface DeleteStackListener { - void onStackDeleted(long stackLocalId); + void onDeleteStack(long accountId, long boardId, long stackId); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java index 4b37a3924..829df99ea 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java @@ -4,8 +4,6 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -24,14 +22,20 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogStackCreateBinding; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment; +import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher; public class EditStackDialogFragment extends ThemedDialogFragment implements DialogInterface.OnClickListener { + + private static final String KEY_ACCOUNT_ID = "account_id"; + private static final String KEY_BOARD_ID = "board_id"; private static final String KEY_STACK_ID = "stack_id"; private static final String KEY_OLD_TITLE = "old_title"; private EditStackListener editStackListener; - private DialogStackCreateBinding binding; + private Bundle args; + private boolean createMode; + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); @@ -51,12 +55,18 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia .setView(binding.getRoot()) .setNeutralButton(android.R.string.cancel, null); - final var args = getArguments(); + args = getArguments(); if (args == null) { + throw new IllegalArgumentException("Pass either " + KEY_ACCOUNT_ID + " and " + KEY_BOARD_ID + " for creating a new stack or " + KEY_STACK_ID + " for editing an existing stack."); + } + + if (args.getLong(KEY_STACK_ID, -1) == -1) { + createMode = true; builder.setTitle(R.string.add_list) .setPositiveButton(R.string.simple_add, null); } else { + createMode = false; binding.input.setText(args.getString(KEY_OLD_TITLE)); builder.setTitle(R.string.rename_list) .setPositiveButton(R.string.simple_rename, null); @@ -69,26 +79,13 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> onClick(dialog, DialogInterface.BUTTON_POSITIVE)); }); - binding.input.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Nothing to do - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - final boolean inputIsValid = inputIsValid(binding.input.getText()); - if (inputIsValid) { - binding.inputWrapper.setError(null); - } - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(inputIsValid); - } - - @Override - public void afterTextChanged(Editable s) { - // Nothing to do + binding.input.addTextChangedListener(new OnTextChangedWatcher(s -> { + final boolean inputIsValid = inputIsValid(binding.input.getText()); + if (inputIsValid) { + binding.inputWrapper.setError(null); } - }); + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(inputIsValid); + })); binding.input.setOnEditorActionListener((textView, actionId, event) -> { //noinspection SwitchStatementWithTooFewBranches @@ -118,21 +115,6 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia this.binding = null; } - public static DialogFragment newInstance() { - return new EditStackDialogFragment(); - } - - public static DialogFragment newInstance(long stackId, @Nullable String oldTitle) { - final var dialog = new EditStackDialogFragment(); - - final var args = new Bundle(); - args.putLong(KEY_STACK_ID, stackId); - args.putString(KEY_OLD_TITLE, oldTitle); - - dialog.setArguments(args); - return dialog; - } - @Override public void applyTheme(int color) { final var utils = ThemeUtils.of(color, requireContext()); @@ -142,16 +124,13 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia @Override public void onClick(DialogInterface dialog, int which) { - final var args = getArguments(); - final var createMode = args == null; - //noinspection SwitchStatementWithTooFewBranches switch (which) { case DialogInterface.BUTTON_POSITIVE: final var currentUserInput = binding.input.getText(); if (inputIsValid(currentUserInput)) { if (createMode) { - editStackListener.onCreateStack(binding.input.getText().toString()); + editStackListener.onCreateStack(args.getLong(KEY_ACCOUNT_ID), args.getLong(KEY_BOARD_ID), binding.input.getText().toString()); } else { editStackListener.onUpdateStack(args.getLong(KEY_STACK_ID), binding.input.getText().toString()); } @@ -166,7 +145,35 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia } } + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + editStackListener.onDismiss(dialog); + } + private static boolean inputIsValid(@Nullable CharSequence input) { return input != null && !input.toString().trim().isEmpty(); } + + public static DialogFragment newInstance(long accountId, long boardId) { + final var dialog = new EditStackDialogFragment(); + + final var args = new Bundle(); + args.putLong(KEY_ACCOUNT_ID, accountId); + args.putLong(KEY_BOARD_ID, boardId); + + dialog.setArguments(args); + return dialog; + } + + public static DialogFragment newInstance(long stackId, @Nullable String oldTitle) { + final var dialog = new EditStackDialogFragment(); + + final var args = new Bundle(); + args.putLong(KEY_STACK_ID, stackId); + args.putString(KEY_OLD_TITLE, oldTitle); + + dialog.setArguments(args); + return dialog; + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java index 8615d7a85..b4c731bfc 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java @@ -1,7 +1,9 @@ package it.niedermann.nextcloud.deck.ui.stack; -public interface EditStackListener { - void onCreateStack(String title); +import android.content.DialogInterface; + +public interface EditStackListener extends DialogInterface.OnDismissListener { + void onCreateStack(long accountId, long boardId, String title); void onUpdateStack(long stackId, String title); }
\ No newline at end of file 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 4a26164dd..37f6a3ee7 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 @@ -1,6 +1,7 @@ package it.niedermann.nextcloud.deck.ui.stack; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.viewpager2.adapter.FragmentStateAdapter; @@ -9,9 +10,15 @@ import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Stack; public class StackAdapter extends FragmentStateAdapter { + + @Nullable + private Account account; + @Nullable + private Long boardId; @NonNull private final List<Stack> stackList = new ArrayList<>(); @@ -29,22 +36,6 @@ public class StackAdapter extends FragmentStateAdapter { } /** - * @return the position of the direct neighbour of the given {@param position} if available. Prefers neighbours to the start of the wanted, but might also return a neighbour to the end. - * @throws NoSuchElementException in case this is the only {@link Stack}. - */ - public int getNeighbourPosition(int position) throws NoSuchElementException, IndexOutOfBoundsException { - if (position >= stackList.size()) { - throw new IndexOutOfBoundsException("Position " + position + " is not in the current stack list."); - } - if (stackList.size() < 2) { - throw new NoSuchElementException("There is no neighbour."); - } - return position > 0 - ? position - 1 - : position + 1; - } - - /** * @return the position of the {@link Stack} where {@link Stack#getLocalId()} equals {@param stackLocalId}. * @throws NoSuchElementException in case the searched {@param stackLocalId} is not in the list. */ @@ -75,12 +66,28 @@ public class StackAdapter extends FragmentStateAdapter { @NonNull @Override public Fragment createFragment(int position) { - return StackFragment.newInstance(stackList.get(position).getLocalId()); + final var stack = stackList.get(position); + if (account == null) { + throw new NullPointerException("Account in " + StackAdapter.class.getSimpleName() + " is null, can not create " + StackFragment.class.getSimpleName()); + } + return StackFragment.newInstance(account, stack.getBoardId(), stack.getLocalId()); } - public void setStacks(@NonNull List<Stack> stacks) { + public void setStacks(@Nullable Account account, @Nullable Long boardId, @NonNull List<Stack> stacks) { + this.account = account; + this.boardId = boardId; this.stackList.clear(); this.stackList.addAll(stacks); notifyDataSetChanged(); } + + @Nullable + public Account getAccount() { + return account; + } + + @Nullable + public Long getBoardId() { + return boardId; + } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java index f090ac59e..302dbb0e3 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 @@ -1,54 +1,62 @@ package it.niedermann.nextcloud.deck.ui.stack; +import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN; + import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import java.util.List; - import it.niedermann.android.crosstabdnd.DragAndDropTab; -import it.niedermann.android.util.DimensionUtil; -import it.niedermann.nextcloud.deck.DeckApplication; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.FragmentStackBinding; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; -import it.niedermann.nextcloud.deck.ui.MainViewModel; +import it.niedermann.nextcloud.deck.ui.card.CardActionListener; import it.niedermann.nextcloud.deck.ui.card.CardAdapter; import it.niedermann.nextcloud.deck.ui.card.SelectCardListener; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel; +import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment; import it.niedermann.nextcloud.deck.ui.movecard.MoveCardListener; +import it.niedermann.nextcloud.deck.ui.theme.Themed; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; +import it.niedermann.nextcloud.deck.util.CardUtil; -public class StackFragment extends Fragment implements DragAndDropTab<CardAdapter>, MoveCardListener { +public class StackFragment extends Fragment implements Themed, DragAndDropTab<CardAdapter>, MoveCardListener, CardActionListener { + private static final String KEY_ACCOUNT = "account"; + private static final String KEY_BOARD_ID = "boardId"; private static final String KEY_STACK_ID = "stackId"; private FragmentStackBinding binding; - private MainViewModel mainViewModel; + private StackViewModel stackViewModel; private FragmentActivity activity; private OnScrollListener onScrollListener; @Nullable private CardAdapter adapter = null; - private LiveData<List<FullCard>> cardsLiveData; + private Account account; + private long boardId; private long stackId; @Override @@ -56,12 +64,23 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte super.onAttach(context); final var args = requireArguments(); + + if (!args.containsKey(KEY_ACCOUNT)) { + throw new IllegalArgumentException(KEY_ACCOUNT + " is required."); + } + account = (Account) args.getSerializable(KEY_ACCOUNT); + + if (!args.containsKey(KEY_BOARD_ID)) { + throw new IllegalArgumentException(KEY_BOARD_ID + " is required."); + } + boardId = args.getLong(KEY_BOARD_ID); + if (!args.containsKey(KEY_STACK_ID)) { throw new IllegalArgumentException(KEY_STACK_ID + " is required."); } - stackId = args.getLong(KEY_STACK_ID); + if (context instanceof OnScrollListener) { this.onScrollListener = (OnScrollListener) context; } @@ -71,23 +90,11 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { activity = requireActivity(); binding = FragmentStackBinding.inflate(inflater, container, false); - mainViewModel = new ViewModelProvider(activity).get(MainViewModel.class); - - final var filterViewModel = new ViewModelProvider(activity).get(FilterViewModel.class); + stackViewModel = new ViewModelProvider(requireActivity(), new SyncViewModel.Factory(this.requireActivity().getApplication(), account)).get(StackViewModel.class); - // This might be a zombie fragment with an empty MainViewModel after Android killed the activity (but not the fragment instance - // See https://github.com/stefan-niedermann/nextcloud-deck/issues/478 - if (mainViewModel.getCurrentAccount() == null) { - DeckLog.logError(new IllegalStateException("Cannot populate " + StackFragment.class.getSimpleName() + " because mainViewModel.getCurrentAccount() is null")); - return binding.getRoot(); - } + applyTheme(account.getColor()); - adapter = new CardAdapter(requireActivity(), getChildFragmentManager(), stackId, mainViewModel, - (requireActivity() instanceof SelectCardListener) - ? (SelectCardListener) requireActivity() - : null); - binding.recyclerView.setAdapter(adapter); - binding.loadingSpinner.show(); + final var filterViewModel = new ViewModelProvider(activity).get(FilterViewModel.class); if (onScrollListener != null) { binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -104,42 +111,39 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte }); } - if (!mainViewModel.currentBoardHasEditPermission()) { - binding.emptyContentView.hideDescription(); - binding.recyclerView.setPadding( - binding.recyclerView.getPaddingTop(), - binding.recyclerView.getPaddingEnd(), - DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x), - binding.recyclerView.getPaddingStart() - ); - } - - final Observer<List<FullCard>> cardsObserver = (fullCards) -> activity.runOnUiThread(() -> { - binding.loadingSpinner.hide(); - if (fullCards != null && fullCards.size() > 0) { - binding.emptyContentView.setVisibility(View.GONE); - adapter.setCardList(fullCards); + stackViewModel.currentBoardHasEditPermission(account.getId(), boardId).observe(getViewLifecycleOwner(), hasEditPermission -> { + if (hasEditPermission) { + binding.emptyContentView.showDescription(); } else { - binding.emptyContentView.setVisibility(View.VISIBLE); + binding.emptyContentView.hideDescription(); } }); - cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterViewModel.getFilterInformation().getValue()); - cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver); + @Nullable final var selectCardListener = (activity instanceof SelectCardListener) ? (SelectCardListener) activity : null; - filterViewModel.getFilterInformation().observe(getViewLifecycleOwner(), (filterInformation -> { - cardsLiveData.removeObserver(cardsObserver); - cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterInformation); - cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver); - })); + adapter = new CardAdapter(activity, this, selectCardListener); + binding.recyclerView.setAdapter(adapter); - return binding.getRoot(); - } + new ReactiveLiveData<>(stackViewModel.getAccount(account.getId())) + .tap(() -> binding.loadingSpinner.show()) + .tap(account -> adapter.setAccount(account)) + .flatMap(account -> stackViewModel.getFullBoard(account.getId(), boardId)) + .tap(fullBoard -> adapter.setFullBoard(fullBoard)) + .flatMap(filterViewModel::getFilterInformation) + .flatMap(filterInformation -> stackViewModel.getFullCardsForStack(account.getId(), stackId, filterInformation)) + .combineWith(() -> stackViewModel.getBoardColor$(account.getId(), boardId)) + .observe(getViewLifecycleOwner(), pair -> { + binding.loadingSpinner.hide(); + if (pair.first != null && !pair.first.isEmpty()) { + binding.emptyContentView.setVisibility(View.GONE); + assert adapter != null; + adapter.setCardList(pair.first, pair.second); + } else { + binding.emptyContentView.setVisibility(View.VISIBLE); + } + }); - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - DeckApplication.readCurrentBoardColor().observe(getViewLifecycleOwner(), this::applyTheme); + return binding.getRoot(); } @Override @@ -159,25 +163,9 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte return binding.recyclerView; } - private void applyTheme(int color) { - if (this.adapter != null) { - this.adapter.applyTheme(color); - } - } - - public static Fragment newInstance(long stackId) { - final var fragment = new StackFragment(); - - final var args = new Bundle(); - args.putLong(KEY_STACK_ID, stackId); - fragment.setArguments(args); - - return fragment; - } - @Override public void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) { - mainViewModel.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, new IResponseCallback<>() { + stackViewModel.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, new IResponseCallback<>() { @Override public void onResponse(Void response) { DeckLog.log("Moved", Card.class.getSimpleName(), originCardLocalId, "to", Stack.class.getSimpleName(), targetStackLocalId); @@ -186,7 +174,7 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte @Override public void onError(Throwable throwable) { IResponseCallback.super.onError(throwable); - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + if (SyncManager.isNoOnVoidError(throwable)) { ExceptionDialogFragment.newInstance(throwable, null).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } } @@ -216,4 +204,106 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte } }); } + + @Override + public void onArchive(@NonNull FullCard fullCard) { + stackViewModel.archiveCard(fullCard, new IResponseCallback<>() { + @Override + public void onResponse(FullCard response) { + DeckLog.info("Successfully archived", Card.class.getSimpleName(), fullCard.getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, fullCard.getAccountId()); + } + }); + } + + @Override + public void onDelete(@NonNull FullCard fullCard) { + stackViewModel.deleteCard(fullCard.getCard(), new IResponseCallback<>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (SyncManager.isNoOnVoidError(throwable)) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, fullCard.getAccountId()); + } + } + }); + } + + @Override + public void onAssignCurrentUser(@NonNull FullCard fullCard) { + stackViewModel.assignUserToCard(fullCard); + } + + @Override + public void onUnassignCurrentUser(@NonNull FullCard fullCard) { + stackViewModel.unassignUserFromCard(fullCard); + } + + @Override + public void onMove(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard) { + 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(), fullBoard.getBoard().getLocalId(), fullCard.getCard().getTitle(), fullCard.getLocalId(), CardUtil.cardHasCommentsOrAttachments(fullCard)) + .show(getChildFragmentManager(), MoveCardDialogFragment.class.getSimpleName()); + } + + @Override + public void onShareLink(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard) { + stackViewModel.getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> { + final int shareLinkRes = account.getServerDeckVersionAsObject().getShareLinkResource(); + final var 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() + activity.getString(shareLinkRes, fullBoard.getBoard().getId(), fullCard.getCard().getId())); + activity.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle())); + }, ContextCompat.getMainExecutor(requireContext())); + } + + @Override + public void onShareContent(@NonNull FullCard fullCard) { + final var 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())); + } + + @AnyThread + private void showExceptionDialog(@NonNull Throwable throwable, long accountId) { + stackViewModel.getAccountFuture(accountId).thenAcceptAsync(account -> ExceptionDialogFragment + .newInstance(throwable, account) + .show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()), + ContextCompat.getMainExecutor(requireContext())); + } + + @Override + public void applyTheme(int color) { + binding.emptyContentView.applyTheme(color); + } + + public static Fragment newInstance(@NonNull Account account, long boardId, long stackId) { + final var fragment = new StackFragment(); + + final var args = new Bundle(); + args.putSerializable(KEY_ACCOUNT, account); + args.putLong(KEY_BOARD_ID, boardId); + args.putLong(KEY_STACK_ID, stackId); + fragment.setArguments(args); + + return fragment; + } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackViewModel.java new file mode 100644 index 000000000..8860fe3be --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackViewModel.java @@ -0,0 +1,87 @@ +package it.niedermann.nextcloud.deck.ui.stack; + +import static java.util.concurrent.CompletableFuture.supplyAsync; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.internal.FilterInformation; +import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel; + +public class StackViewModel extends SyncViewModel { + + public StackViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + super(application, account); + } + + public void moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId, @NonNull IResponseCallback<Void> callback) { + syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback); + } + + public LiveData<Account> getAccount(long accountId) { + return new ReactiveLiveData<>(baseRepository.readAccount(accountId)) + .distinctUntilChanged(); + } + + public CompletableFuture<Account> getAccountFuture(long accountId) { + return supplyAsync(() -> baseRepository.readAccountDirectly(accountId)); + } + + public LiveData<FullBoard> getFullBoard(long accountId, long boardId) { + return new ReactiveLiveData<>(baseRepository.getFullBoardById(accountId, boardId)) + .distinctUntilChanged(); + } + + public LiveData<Integer> getBoardColor$(long accountId, long boardId) { + return baseRepository.getBoardColor$(accountId, boardId); + } + + public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { + return new ReactiveLiveData<>(baseRepository.getFullCardsForStack(accountId, localStackId, filter)) + .distinctUntilChanged(); + } + + public LiveData<Boolean> currentBoardHasEditPermission(long accountId, long boardId) { + return new ReactiveLiveData<>(baseRepository.readAccount(accountId)) + .flatMap(account -> account.getServerDeckVersionAsObject().isSupported() + ? new ReactiveLiveData<>(baseRepository.getFullBoardById(accountId, boardId)).map(fullBoard -> fullBoard != null && fullBoard.getBoard().isPermissionEdit()) + : new ReactiveLiveData<>(false)) + .distinctUntilChanged(); + } + + public void archiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) { + syncManager.archiveCard(card, callback); + } + + + public void deleteCard(@NonNull Card card, @NonNull IResponseCallback<Void> callback) { + syncManager.deleteCard(card, callback); + } + + public void assignUserToCard(@NonNull FullCard fullCard) { + getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> syncManager.assignUserToCard(getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())); + } + + public void unassignUserFromCard(@NonNull FullCard fullCard) { + getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> syncManager.unassignUserFromCard(getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())); + } + + private User getUserByUidDirectly(long accountId, String uid) { + return baseRepository.getUserByUidDirectly(accountId, uid); + } +} 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 a8fd87ae6..1456b159d 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 @@ -31,7 +31,7 @@ import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.concurrent.ExecutionException; -import it.niedermann.nextcloud.deck.DeckApplication; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.databinding.ActivityTakePhotoBinding; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; @@ -63,7 +63,10 @@ public class TakePhotoActivity extends AppCompatActivity { setContentView(binding.getRoot()); // TODO do not only rely on current board color in case a card has been opened from a widget - DeckApplication.readCurrentBoardColor().observe(this, this::applyBoardColorBrand); + new ReactiveLiveData<>(viewModel.getCurrentAccountId$()) + .combineWith(viewModel::getCurrentBoardId$) + .flatMap(ids -> viewModel.getBoardColor$(ids.first, ids.second)) + .observe(this, this::applyBoardColorBrand); cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture.addListener(() -> { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java index cadc02687..ff12de766 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java @@ -2,17 +2,19 @@ package it.niedermann.nextcloud.deck.ui.takephoto; import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA; import static androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA; -import static androidx.lifecycle.Transformations.map; + +import android.app.Application; import androidx.annotation.NonNull; import androidx.camera.core.CameraSelector; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; -public class TakePhotoViewModel extends ViewModel { +public class TakePhotoViewModel extends BaseViewModel { @NonNull private CameraSelector cameraSelector = DEFAULT_BACK_CAMERA; @@ -21,6 +23,22 @@ public class TakePhotoViewModel extends ViewModel { @NonNull private final MutableLiveData<Boolean> torchEnabled = new MutableLiveData<>(false); + public TakePhotoViewModel(@NonNull Application application) { + super(application); + } + + public LiveData<Long> getCurrentAccountId$() { + return baseRepository.getCurrentAccountId$(); + } + + public LiveData<Long> getCurrentBoardId$(long accountId) { + return baseRepository.getCurrentBoardId$(accountId); + } + + public LiveData<Integer> getBoardColor$(long accountId, long boardId) { + return baseRepository.getBoardColor$(accountId, boardId); + } + @NonNull public CameraSelector getCameraSelector() { return this.cameraSelector; @@ -50,8 +68,9 @@ public class TakePhotoViewModel extends ViewModel { } public LiveData<Integer> getTorchToggleButtonImageResource() { - return map(isTorchEnabled(), enabled -> enabled - ? R.drawable.ic_baseline_flash_off_24 - : R.drawable.ic_baseline_flash_on_24); + return new ReactiveLiveData<>(isTorchEnabled()) + .map(enabled -> enabled + ? R.drawable.ic_baseline_flash_off_24 + : R.drawable.ic_baseline_flash_on_24); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java index 3e13d4256..83a508a75 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java @@ -1,15 +1,40 @@ package it.niedermann.nextcloud.deck.ui.theme; +import static com.nextcloud.android.common.ui.util.ColorStateListUtilsKt.buildColorStateList; +import static java.time.temporal.ChronoUnit.DAYS; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.StateListDrawable; +import android.os.Build; +import android.widget.ImageView; +import android.widget.TextView; + import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.widget.TextViewCompat; import com.google.android.material.tabs.TabLayout; import com.nextcloud.android.common.ui.theme.MaterialSchemes; import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase; +import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils; +import java.time.LocalDate; + +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import kotlin.Pair; /** * UI Elements which are not yet supported by the <a href="https://github.com/nextcloud/android-common"><code>android-commons</code></a> library. @@ -17,14 +42,17 @@ import it.niedermann.nextcloud.deck.R; */ public class DeckViewThemeUtils extends ViewThemeUtilsBase { + private final AndroidViewThemeUtils platform; private final MaterialViewThemeUtils material; public DeckViewThemeUtils( @NonNull MaterialSchemes schemes, - @NonNull MaterialViewThemeUtils material + @NonNull MaterialViewThemeUtils material, + @NonNull AndroidViewThemeUtils platform ) { super(schemes); this.material = material; + this.platform = platform; } /** @@ -42,4 +70,93 @@ public class DeckViewThemeUtils extends ViewThemeUtilsBase { this.material.themeTabLayout(tabLayout); tabLayout.setBackgroundColor(backgroundColor); } + + public Drawable themeNavigationViewIcon(@NonNull Context context, @DrawableRes int icon) { + return withScheme(context, scheme -> { + final var colorStateListe = buildColorStateList( + new Pair<>(android.R.attr.state_checked, scheme.getOnSecondaryContainer()), + new Pair<>(-android.R.attr.state_checked, scheme.getOnSurfaceVariant()) + ); + + final var drawable = ContextCompat.getDrawable(context, icon); + assert drawable != null; + final var wrapped = DrawableCompat.wrap(drawable).mutate(); + DrawableCompat.setTintList(wrapped, colorStateListe); + wrapped.invalidateSelf(); + + return wrapped; + }); + } + + /** + * There is currently no way to retrieve the actual color used for generating the current scheme. + * Therefore we let pass it as argument. + */ + @Nullable + public Drawable getColoredBoardDrawable(@NonNull Context context, @ColorInt int boardColor) { + final var drawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.circle_grey600_36dp, null); + return drawable == null ? null : platform.colorDrawable(drawable, boardColor); + } + + /** + * Use <strong>only</strong> for <code>@drawable/selected_check</code> + */ + @RequiresApi(api = Build.VERSION_CODES.Q) + public void colorSelectedCheck(@NonNull Context context, @NonNull Drawable selectedCheck) { + try { + final var check = ((StateListDrawable) selectedCheck); + final var checkSelectedIndex = check.findStateDrawableIndex(new int[]{android.R.attr.state_selected}); + final var checkSelectedDrawable = check.getStateDrawable(checkSelectedIndex); + + final var backgroundDrawable = ((LayerDrawable) checkSelectedDrawable).findDrawableByLayerId(R.id.background); + final var foregroundDrawable = ((LayerDrawable) checkSelectedDrawable).findDrawableByLayerId(R.id.foreground); + platform.tintDrawable(context, backgroundDrawable, ColorRole.PRIMARY); + platform.tintDrawable(context, foregroundDrawable, ColorRole.ON_PRIMARY); + } catch (Exception e) { + DeckLog.error(e); + } + } + + @Deprecated(forRemoval = true) + public static void themeDueDate(@NonNull TextView cardDueDate, @NonNull LocalDate dueDate) { + final var context = cardDueDate.getContext(); + final long diff = DAYS.between(LocalDate.now(), dueDate); + + @ColorInt @Nullable Integer textColor = null; + @DrawableRes int backgroundDrawable = 0; + + if (diff == 1) { + // due date: tomorrow + backgroundDrawable = R.drawable.due_tomorrow_background; + textColor = ContextCompat.getColor(context, R.color.due_text_tomorrow); + } else if (diff == 0) { + // due date: today + backgroundDrawable = R.drawable.due_today_background; + textColor = ContextCompat.getColor(context, R.color.due_text_today); + } else if (diff < 0) { + // due date: overdue + backgroundDrawable = R.drawable.due_overdue_background; + textColor = ContextCompat.getColor(context, R.color.due_text_overdue); + } + + cardDueDate.setBackgroundResource(backgroundDrawable); + if (textColor != null) { + cardDueDate.setTextColor(textColor); + TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(textColor)); + } + } + + @Deprecated(forRemoval = true) + public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @ColorInt int color) { + final var drawable = ContextCompat.getDrawable(context, imageId); + assert drawable != null; + final var wrapped = DrawableCompat.wrap(drawable).mutate(); + DrawableCompat.setTint(wrapped, color); + return drawable; + } + + @Deprecated(forRemoval = true) + public static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @ColorRes int colorRes) { + imageView.setImageTintList(ColorStateList.valueOf(ContextCompat.getColor(context, colorRes))); + } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java index 3bd6c6bbf..45d4e25c1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java @@ -1,13 +1,11 @@ package it.niedermann.nextcloud.deck.ui.theme; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; +import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode; import android.content.Context; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.preference.PreferenceManager; import com.nextcloud.android.common.ui.color.ColorUtil; import com.nextcloud.android.common.ui.theme.MaterialSchemes; @@ -20,8 +18,6 @@ import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.R; import scheme.Scheme; public class ThemeUtils extends ViewThemeUtilsBase { @@ -44,7 +40,7 @@ public class ThemeUtils extends ViewThemeUtilsBase { this.material = new MaterialViewThemeUtils(schemes, colorUtil); this.androidx = new AndroidXViewThemeUtils(schemes, this.platform); this.dialog = new DialogViewThemeUtils(schemes); - this.deck = new DeckViewThemeUtils(schemes, this.material); + this.deck = new DeckViewThemeUtils(schemes, this.material, this.platform); } public static ThemeUtils of(@ColorInt int color, @NonNull Context context) { @@ -54,28 +50,8 @@ public class ThemeUtils extends ViewThemeUtilsBase { )); } + @Deprecated public static Scheme createScheme(@ColorInt int color, @NonNull Context context) { - return isDarkTheme(context) ? Scheme.dark(color) : Scheme.light(color); - } - - @ColorInt - public static int readBrandMainColor(@NonNull Context context) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_theme_main)); - return sharedPreferences.getInt(context.getString(R.string.shared_preference_theme_main), ContextCompat.getColor(context, R.color.defaultBrand)); - } - - public static void saveBrandColors(@NonNull Context context, @ColorInt int color) { - final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write:", context.getString(R.string.shared_preference_theme_main), "|", color); - editor.putInt(context.getString(R.string.shared_preference_theme_main), color); - editor.apply(); - } - - public static void clearBrandColors(@NonNull Context context) { - final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Remove:", context.getString(R.string.shared_preference_theme_main)); - editor.remove(context.getString(R.string.shared_preference_theme_main)); - editor.apply(); + return isDarkMode(context) ? Scheme.dark(color) : Scheme.light(color); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java index 6b6a5ddfa..926a394e4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java @@ -1,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.theme; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor; +import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode; import android.os.Bundle; import android.view.LayoutInflater; @@ -19,11 +18,17 @@ import scheme.Scheme; public class ThemedDatePickerDialog extends DatePickerDialog implements Themed { + private static final String BUNDLE_KEY_COLOR = "color"; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final var context = requireContext(); - setThemeDark(isDarkTheme(context)); - applyTheme(readBrandMainColor(context)); + final var args = getArguments(); + if (args == null || !args.containsKey(BUNDLE_KEY_COLOR)) { + throw new IllegalArgumentException("Please provide at least local comment id"); + } + + applyTheme(args.getInt(BUNDLE_KEY_COLOR)); + setThemeDark(isDarkMode(requireContext())); return super.onCreateView(inflater, container, savedInstanceState); } @@ -47,8 +52,13 @@ public class ThemedDatePickerDialog extends DatePickerDialog implements Themed { * @param dayOfMonth The initial day of the dialog. * @return a new DatePickerDialog instance. */ - public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { + public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth, @ColorInt int color) { final var dialog = new ThemedDatePickerDialog(); + + final var args = new Bundle(); + args.putInt(BUNDLE_KEY_COLOR, color); + dialog.setArguments(args); + dialog.initialize(callBack, year, monthOfYear - 1, dayOfMonth); return dialog; } @@ -62,8 +72,13 @@ public class ThemedDatePickerDialog extends DatePickerDialog implements Themed { * TimeZone of the Calendar object) * @return a new DatePickerDialog instance */ - public static DatePickerDialog newInstance(OnDateSetListener callback, Calendar initialSelection) { + public static DatePickerDialog newInstance(OnDateSetListener callback, Calendar initialSelection, @ColorInt int color) { final var dialog = new ThemedDatePickerDialog(); + + final var args = new Bundle(); + args.putInt(BUNDLE_KEY_COLOR, color); + dialog.setArguments(args); + dialog.initialize(callback, initialSelection); return dialog; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java index 8638780f7..6d45e4190 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java @@ -1,19 +1,21 @@ package it.niedermann.nextcloud.deck.ui.theme; -import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor; - -import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; + public abstract class ThemedDialogFragment extends DialogFragment implements Themed { @Override public void onStart() { super.onStart(); - @Nullable final var context = getContext(); - if (context != null) { - applyTheme(readBrandMainColor(context)); - } + final var baseRepository = new BaseRepository(requireContext()); + + new ReactiveLiveData<>(baseRepository.getCurrentAccountId$()) + .combineWith(baseRepository::getCurrentBoardId$) + .flatMap(ids -> baseRepository.getBoardColor$(ids.first, ids.second)) + .observe(this, this::applyTheme); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java index cb9dd2be1..39d79a84b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java @@ -1,16 +1,15 @@ package it.niedermann.nextcloud.deck.ui.theme; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountColor; - import android.content.Context; import android.util.AttributeSet; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceViewHolder; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; + public class ThemedPreferenceCategory extends PreferenceCategory { @Override @@ -18,10 +17,16 @@ public class ThemedPreferenceCategory extends PreferenceCategory { super.onBindViewHolder(holder); final var view = holder.itemView.findViewById(android.R.id.title); - @Nullable final Context context = getContext(); + final var context = getContext(); + final var repo = new BaseRepository(context); + if (view instanceof TextView) { - final var scheme = ThemeUtils.createScheme(readCurrentAccountColor(context), context); - ((TextView) view).setTextColor(scheme.getOnPrimaryContainer()); + repo.getCurrentAccountId() + .thenComposeAsync(repo::getCurrentAccountColor) + .thenAcceptAsync(accountColor -> { + final var utils = ThemeUtils.of(accountColor, context); + utils.platform.colorTextView((TextView) view); + }); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java index ac6829b12..7b45ac644 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java @@ -1,7 +1,5 @@ package it.niedermann.nextcloud.deck.ui.theme; -import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor; - import android.view.View; import androidx.annotation.ColorInt; @@ -14,8 +12,7 @@ import com.google.android.material.snackbar.Snackbar; public class ThemedSnackbar { @NonNull - public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @BaseTransientBottomBar.Duration int duration) { - @ColorInt final int color = readBrandMainColor(view.getContext()); + public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @BaseTransientBottomBar.Duration int duration, @ColorInt int color) { final var snackbar = Snackbar.make(view, text, duration); final var utils = ThemeUtils.of(color, view.getContext()); @@ -25,7 +22,7 @@ public class ThemedSnackbar { } @NonNull - public static Snackbar make(@NonNull View view, @StringRes int resId, @BaseTransientBottomBar.Duration int duration) { - return make(view, view.getResources().getText(resId), duration); + public static Snackbar make(@NonNull View view, @StringRes int resId, @BaseTransientBottomBar.Duration int duration, @ColorInt int color) { + return make(view, view.getResources().getText(resId), duration, color); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java index 3a0ce8561..d835a5723 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java @@ -1,9 +1,7 @@ package it.niedermann.nextcloud.deck.ui.theme; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor; +import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode; -import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -11,7 +9,6 @@ import android.view.ViewGroup; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.wdullaer.materialdatetimepicker.time.TimePickerDialog; @@ -21,13 +18,17 @@ import scheme.Scheme; public class ThemedTimePickerDialog extends TimePickerDialog implements Themed { + private static final String BUNDLE_KEY_COLOR = "color"; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - @Nullable Context context = getContext(); - if (context != null) { - setThemeDark(isDarkTheme(context)); - applyTheme(readBrandMainColor(context)); + final var args = getArguments(); + if (args == null || !args.containsKey(BUNDLE_KEY_COLOR)) { + throw new IllegalArgumentException("Please provide at least local comment id"); } + + applyTheme(args.getInt(BUNDLE_KEY_COLOR)); + setThemeDark(isDarkMode(requireContext())); return super.onCreateView(inflater, container, savedInstanceState); } @@ -54,8 +55,13 @@ public class ThemedTimePickerDialog extends TimePickerDialog implements Themed { */ @SuppressWarnings({"SameParameterValue"}) public static TimePickerDialog newInstance(OnTimeSetListener callback, - int hourOfDay, int minute, int second, boolean is24HourMode) { + int hourOfDay, int minute, int second, boolean is24HourMode, @ColorInt int color) { final var dialog = new ThemedTimePickerDialog(); + + final var args = new Bundle(); + args.putInt(BUNDLE_KEY_COLOR, color); + dialog.setArguments(args); + dialog.initialize(callback, hourOfDay, minute, second, is24HourMode); return dialog; } @@ -70,8 +76,8 @@ public class ThemedTimePickerDialog extends TimePickerDialog implements Themed { * @return a new TimePickerDialog instance. */ public static TimePickerDialog newInstance(OnTimeSetListener callback, - int hourOfDay, int minute, boolean is24HourMode) { - return newInstance(callback, hourOfDay, minute, 0, is24HourMode); + int hourOfDay, int minute, boolean is24HourMode, @ColorInt int color) { + return newInstance(callback, hourOfDay, minute, 0, is24HourMode, color); } /** @@ -82,8 +88,8 @@ public class ThemedTimePickerDialog extends TimePickerDialog implements Themed { * @return a new TimePickerDialog instance. */ @SuppressWarnings({"SameParameterValue"}) - public static TimePickerDialog newInstance(OnTimeSetListener callback, boolean is24HourMode) { + public static TimePickerDialog newInstance(OnTimeSetListener callback, boolean is24HourMode, @ColorInt int color) { final var now = LocalTime.now(); - return newInstance(callback, now.getHour(), now.getMinute(), is24HourMode); + return newInstance(callback, now.getHour(), now.getMinute(), is24HourMode, color); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java index ad691c943..5ba3402b5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java @@ -1,14 +1,11 @@ package it.niedermann.nextcloud.deck.ui.tiles; -import android.annotation.TargetApi; import android.content.Intent; -import android.os.Build; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; import it.niedermann.nextcloud.deck.ui.preparecreate.PrepareCreateActivity; -@TargetApi(Build.VERSION_CODES.N) public class EditCardTileService extends TileService { @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java index a30d928c3..2d004e733 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java @@ -10,6 +10,8 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProvider; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.ActivityUpcomingCardsBinding; @@ -41,8 +43,20 @@ public class UpcomingCardsActivity extends AppCompatActivity implements MoveCard binding.loadingSpinner.show(); final var adapter = new UpcomingCardsAdapter(this, getSupportFragmentManager(), - viewModel::assignUser, - viewModel::unassignUser, + (a, c) -> { + try { + viewModel.assignUser(a, c); + } catch (NextcloudFilesAppAccountNotFoundException e) { + ExceptionDialogFragment.newInstance(e, a).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }, + (a, c) -> { + try { + viewModel.unassignUser(a, c); + } catch (NextcloudFilesAppAccountNotFoundException e) { + ExceptionDialogFragment.newInstance(e, a).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }, (fullCard) -> viewModel.archiveCard(fullCard, new IResponseCallback<>() { @Override public void onResponse(FullCard response) { @@ -63,7 +77,7 @@ public class UpcomingCardsActivity extends AppCompatActivity implements MoveCard @Override public void onError(Throwable throwable) { - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + if (SyncManager.isNoOnVoidError(throwable)) { IResponseCallback.super.onError(throwable); runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } @@ -107,7 +121,7 @@ public class UpcomingCardsActivity extends AppCompatActivity implements MoveCard @Override public void onError(Throwable throwable) { IResponseCallback.super.onError(throwable); - if (!SyncManager.ignoreExceptionOnVoidError(throwable)) { + if (SyncManager.isNoOnVoidError(throwable)) { ExceptionDialogFragment.newInstance(throwable, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java index 1df85af63..c9e8403a9 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java @@ -32,7 +32,6 @@ import it.niedermann.nextcloud.deck.ui.card.DefaultCardOnlyTitleViewHolder; import it.niedermann.nextcloud.deck.ui.card.DefaultCardViewHolder; import it.niedermann.nextcloud.deck.ui.card.EditActivity; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; -import scheme.Scheme; public class UpcomingCardsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { @@ -46,7 +45,7 @@ public class UpcomingCardsAdapter extends RecyclerView.Adapter<RecyclerView.View @NonNull protected String counterMaxValue; @NonNull - protected Scheme scheme; + protected ThemeUtils utils; @NonNull private final BiConsumer<Account, Card> assignCard; @NonNull @@ -65,7 +64,7 @@ public class UpcomingCardsAdapter extends RecyclerView.Adapter<RecyclerView.View this.activity = activity; this.counterMaxValue = this.activity.getString(R.string.counter_max_value); this.fragmentManager = fragmentManager; - this.scheme = ThemeUtils.createScheme(ContextCompat.getColor(this.activity, R.color.defaultBrand), this.activity); + this.utils = ThemeUtils.of(ContextCompat.getColor(this.activity, R.color.defaultBrand), this.activity); this.compactMode = getDefaultSharedPreferences(this.activity).getBoolean(this.activity.getString(R.string.pref_key_compact), false); this.assignCard = assignCard; this.unassignCard = unassignCard; @@ -155,7 +154,7 @@ public class UpcomingCardsAdapter extends RecyclerView.Adapter<RecyclerView.View unassignCard, archiveCard, deleteCard - ), counterMaxValue, scheme); + ), counterMaxValue, utils); cardViewHolder.bindCardClickListener((v) -> activity.startActivity(EditActivity.createEditCardIntent(activity, cardItem.getAccount(), cardItem.getCurrentBoardLocalId(), cardItem.getFullCard().getLocalId()))); } else { throw new IllegalStateException("Item at position " + position + " is a " + item.getClass().getSimpleName() + " but viewHolder is no " + AbstractCardViewHolder.class.getSimpleName()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java index b5c0dd3be..3b3a99365 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java @@ -41,16 +41,17 @@ public class UpcomingCardsOptionsItemSelectedListener implements CardOptionsItem @NonNull private final Consumer<Card> deleteCard; - public UpcomingCardsOptionsItemSelectedListener(@NonNull Account account, - @NonNull Activity activity, - @NonNull FragmentManager fragmentManager, - @Nullable Long boardRemoteId, - long boardLocalId, - @NonNull BiConsumer<Account, Card> assignCard, - @NonNull BiConsumer<Account, Card> unassignCard, - @NonNull Consumer<FullCard> archiveCard, - @NonNull Consumer<Card> deleteCard - ) { + public UpcomingCardsOptionsItemSelectedListener + (@NonNull Account account, + @NonNull Activity activity, + @NonNull FragmentManager fragmentManager, + @Nullable Long boardRemoteId, + long boardLocalId, + @NonNull BiConsumer<Account, Card> assignCard, + @NonNull BiConsumer<Account, Card> unassignCard, + @NonNull Consumer<FullCard> archiveCard, + @NonNull Consumer<Card> deleteCard + ) { this.account = account; this.activity = activity; this.fragmentManager = fragmentManager; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java index 550dc1682..93234bc60 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java @@ -3,52 +3,73 @@ package it.niedermann.nextcloud.deck.ui.upcomingcards; import android.app.Application; import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import it.niedermann.nextcloud.deck.api.IResponseCallback; 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.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; @SuppressWarnings("WeakerAccess") -public class UpcomingCardsViewModel extends AndroidViewModel { - - private final SyncManager syncManager; - private final ExecutorService executor; +public class UpcomingCardsViewModel extends BaseViewModel { public UpcomingCardsViewModel(@NonNull Application application) { super(application); - this.syncManager = new SyncManager(application); - this.executor = Executors.newCachedThreadPool(); } public LiveData<List<UpcomingCardsAdapterItem>> getUpcomingCards() { - return this.syncManager.getCardsForUpcomingCards(); + return this.baseRepository.getCardsForUpcomingCards(); } - public void assignUser(@NonNull Account account, @NonNull Card card) { - executor.submit(() -> syncManager.assignUserToCard(syncManager.getUserByUidDirectly(card.getAccountId(), account.getUserName()), card)); + public void assignUser(@NonNull Account account, @NonNull Card card) throws NextcloudFilesAppAccountNotFoundException { + final var syncManager = new SyncManager(getApplication(), account); + executor.submit(() -> syncManager.assignUserToCard(baseRepository.getUserByUidDirectly(card.getAccountId(), account.getUserName()), card)); } - public void unassignUser(@NonNull Account account, @NonNull Card card) { - executor.submit(() -> syncManager.unassignUserFromCard(syncManager.getUserByUidDirectly(card.getAccountId(), account.getUserName()), card)); + public void unassignUser(@NonNull Account account, @NonNull Card card) throws NextcloudFilesAppAccountNotFoundException { + final var syncManager = new SyncManager(getApplication(), account); + executor.submit(() -> syncManager.unassignUserFromCard(baseRepository.getUserByUidDirectly(card.getAccountId(), account.getUserName()), card)); } public void archiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) { - syncManager.archiveCard(card, callback); + executor.submit(() -> { + final var account = baseRepository.readAccountDirectly(card.getAccountId()); + try { + final var syncManager = new SyncManager(getApplication(), account); + syncManager.archiveCard(card, callback); + } catch (NextcloudFilesAppAccountNotFoundException e) { + callback.onError(e); + } + }); } public void deleteCard(@NonNull Card card, @NonNull IResponseCallback<Void> callback) { - syncManager.deleteCard(card, callback); + executor.submit(() -> { + final var account = baseRepository.readAccountDirectly(card.getAccountId()); + try { + final var syncManager = new SyncManager(getApplication(), account); + syncManager.deleteCard(card, callback); + } catch (NextcloudFilesAppAccountNotFoundException e) { + callback.onError(e); + } + }); } public void moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId, @NonNull IResponseCallback<Void> callback) { - syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback); + executor.submit(() -> { + final var account = baseRepository.readAccountDirectly(originAccountId); + try { + final var syncManager = new SyncManager(getApplication(), account); + syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback); + } catch (NextcloudFilesAppAccountNotFoundException e) { + callback.onError(e); + } + }); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java index 8345e63b9..326979744 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java @@ -21,7 +21,7 @@ import java.util.Arrays; import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.WidgetColorChooserBinding; -import it.niedermann.nextcloud.deck.util.ViewUtil; +import it.niedermann.nextcloud.deck.ui.theme.DeckViewThemeUtils; public class ColorChooser extends LinearLayout { @@ -60,17 +60,17 @@ public class ColorChooser extends LinearLayout { image.setLayoutParams(params); image.setOnClickListener((imageView) -> { if (previouslySelectedImageView != null) { // null when first selection - previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor)); + previouslySelectedImageView.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor)); } - image.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_check_36dp, color)); + image.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_alpha_check_36dp, color)); selectedColor = color; this.previouslySelectedColor = color; this.previouslySelectedImageView = image; - binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color))); + binding.customColorChooser.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color))); binding.customColorPicker.setVisibility(View.GONE); binding.brightnessSlide.setVisibility(View.GONE); }); - image.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, color)); + image.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, color)); binding.colorPicker.addView(image, binding.colorPicker.getChildCount() - 1); } @@ -79,21 +79,20 @@ public class ColorChooser extends LinearLayout { binding.customColorPicker.setVisibility(View.VISIBLE); binding.brightnessSlide.setVisibility(View.VISIBLE); if (previouslySelectedImageView != null) { - previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, selectedColor)); + previouslySelectedImageView.setImageDrawable(DeckViewThemeUtils.getTintedImageView(context, R.drawable.circle_grey600_36dp, selectedColor)); previouslySelectedImageView = null; } }); binding.customColorPicker.setColorListener((ColorEnvelopeListener) (envelope, fromUser) -> { if (previouslySelectedImageView != null) { - previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor)); + previouslySelectedImageView.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor)); previouslySelectedImageView = null; } - @ColorInt - final int customColor = envelope.getColor(); + @ColorInt final int customColor = envelope.getColor(); selectedColor = customColor; previouslySelectedColor = customColor; - binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.circle_alpha_colorize_36dp, selectedColor)); + binding.customColorChooser.setImageDrawable(DeckViewThemeUtils.getTintedImageView(context, R.drawable.circle_alpha_colorize_36dp, selectedColor)); }); } @@ -102,14 +101,14 @@ public class ColorChooser extends LinearLayout { selectedColor = newColor; for (int i = 0; i < colors.length; i++) { if (colors[i] == newColor) { - binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color))); + binding.customColorChooser.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color))); binding.colorPicker.getChildAt(i).performClick(); newColorIsCustomColor = false; break; } } if (newColorIsCustomColor) { - binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, this.selectedColor)); + binding.customColorChooser.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, this.selectedColor)); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java index bd0bb68d9..2b3557694 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java @@ -9,10 +9,14 @@ import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.annotation.StringRes; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; + import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.WidgetEmptyContentViewBinding; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.theme.Themed; -public class EmptyContentView extends RelativeLayout { +public class EmptyContentView extends RelativeLayout implements Themed { private static final int NO_DESCRIPTION = -1; @@ -23,11 +27,8 @@ public class EmptyContentView extends RelativeLayout { binding = WidgetEmptyContentViewBinding.inflate(LayoutInflater.from(context), this, true); - final var styles = context.obtainStyledAttributes(attrs, - R.styleable.EmptyContentView, 0, 0); - + final var styles = context.obtainStyledAttributes(attrs, R.styleable.EmptyContentView, 0, 0); @StringRes int descriptionRes = styles.getResourceId(R.styleable.EmptyContentView_description, NO_DESCRIPTION); - binding.title.setText(getResources().getString(styles.getResourceId(R.styleable.EmptyContentView_title, R.string.no_content))); if (descriptionRes == NO_DESCRIPTION) { binding.description.setVisibility(View.GONE); @@ -45,4 +46,13 @@ public class EmptyContentView extends RelativeLayout { public void showDescription() { binding.description.setVisibility(View.VISIBLE); } + + @Override + public void applyTheme(int color) { + final var utils = ThemeUtils.of(color, getContext()); + +// utils.platform.colorImageView(binding.image, ColorRole.SECONDARY_CONTAINER); + utils.platform.colorTextView(binding.title, ColorRole.ON_SURFACE); + utils.platform.colorTextView(binding.description, ColorRole.ON_SURFACE); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java index 9d15df896..c1d5af3a2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java @@ -2,7 +2,6 @@ package it.niedermann.nextcloud.deck.ui.view; import android.content.Context; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.util.AttributeSet; import android.widget.ImageView; import android.widget.RelativeLayout; @@ -69,7 +68,7 @@ public class OverlappingAvatars extends RelativeLayout { addView(avatar); avatar.requestLayout(); Glide.with(context) - .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(assignedUsers.get(avatarCount).getUid()) + "/" + avatarSize) + .load(account.getAvatarUrl(avatarSize, assignedUsers.get(avatarCount).getUid())) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) .apply(RequestOptions.circleCropTransform()) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/BaseViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/BaseViewModel.java new file mode 100644 index 000000000..d76007be4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/BaseViewModel.java @@ -0,0 +1,40 @@ +package it.niedermann.nextcloud.deck.ui.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.ViewModel; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import it.niedermann.nextcloud.deck.persistence.BaseRepository; + +/** + * To be used for {@link ViewModel}s which need an {@link BaseRepository} instance + */ +public abstract class BaseViewModel extends AndroidViewModel { + + protected final Application application; + protected final BaseRepository baseRepository; + protected final ExecutorService executor; + + public BaseViewModel(@NonNull Application application) { + this(application, new BaseRepository(application)); + } + + public BaseViewModel(@NonNull Application application, + @NonNull BaseRepository baseRepository) { + this(application, baseRepository, Executors.newCachedThreadPool()); + } + + public BaseViewModel(@NonNull Application application, + @NonNull BaseRepository baseRepository, + @NonNull ExecutorService executor) { + super(application); + this.application = application; + this.baseRepository = baseRepository; + this.executor = executor; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/SyncViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/SyncViewModel.java new file mode 100644 index 000000000..543c9ba92 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/SyncViewModel.java @@ -0,0 +1,80 @@ +package it.niedermann.nextcloud.deck.ui.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.archivedboards.ArchivedBoardsViewModel; +import it.niedermann.nextcloud.deck.ui.board.accesscontrol.AccessControlViewModel; +import it.niedermann.nextcloud.deck.ui.board.managelabels.LabelsViewModel; +import it.niedermann.nextcloud.deck.ui.card.NewCardViewModel; +import it.niedermann.nextcloud.deck.ui.card.comments.CommentsViewModel; +import it.niedermann.nextcloud.deck.ui.stack.StackViewModel; + +/** + * To be used for {@link ViewModel}s which need an {@link SyncManager} instance + */ +public abstract class SyncViewModel extends BaseViewModel { + + protected final Account account; + protected final SyncManager syncManager; + + public SyncViewModel(@NonNull Application application, + @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + this(application, account, new SyncManager(application, account)); + } + + public SyncViewModel(@NonNull Application application, + @NonNull Account account, + @NonNull SyncManager syncManager) { + super(application, syncManager); + this.account = account; + this.syncManager = syncManager; + } + + public static class Factory implements ViewModelProvider.Factory { + + private final Application application; + private final Account account; + + public Factory(@NonNull Application application, @NonNull Account account) { + this.application = application; + this.account = account; + } + + @SuppressWarnings("unchecked") + @NonNull + @Override + public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { + try { + if (modelClass == AccessControlViewModel.class) { + return (T) new AccessControlViewModel(application, account); + } + if (modelClass == ArchivedBoardsViewModel.class) { + return (T) new ArchivedBoardsViewModel(application, account); + } + if (modelClass == CommentsViewModel.class) { + return (T) new CommentsViewModel(application, account); + } + if (modelClass == LabelsViewModel.class) { + return (T) new LabelsViewModel(application, account); + } + if (modelClass == NewCardViewModel.class) { + return (T) new NewCardViewModel(application, account); + } + if (modelClass == StackViewModel.class) { + return (T) new StackViewModel(application, account); + } + throw new IllegalArgumentException(getClass().getSimpleName() + " can not instantiate " + modelClass.getSimpleName()); + } catch (NextcloudFilesAppAccountNotFoundException e) { + throw new RuntimeException(e); + } + } + } +} 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 index 56ea508c0..0d0acd7c4 100644 --- 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 @@ -1,5 +1,7 @@ package it.niedermann.nextcloud.deck.ui.widget.filter; +import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE; + import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.ComponentName; @@ -15,9 +17,7 @@ import java.util.concurrent.Executors; 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; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; public class FilterWidget extends AppWidgetProvider { public static final String ACCOUNT_KEY = "filter_widget_account"; @@ -25,7 +25,7 @@ public class FilterWidget extends AppWidgetProvider { final ExecutorService executor = Executors.newCachedThreadPool(); static void updateAppWidget(@NonNull ExecutorService executor, @NonNull Context context, AppWidgetManager awm, int[] appWidgetIds, Account account) { - final SyncManager syncManager = new SyncManager(context); + final var baseRepository = new BaseRepository(context); for (int appWidgetId : appWidgetIds) { executor.submit(() -> { try { @@ -73,10 +73,10 @@ public class FilterWidget extends AppWidgetProvider { @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); - final SyncManager syncManager = new SyncManager(context); + final var baseRepository = new BaseRepository(context); for (int appWidgetId : appWidgetIds) { - syncManager.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + FilterWidget.class.getSimpleName() + " with id " + appWidgetId)); + baseRepository.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/FilterWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java index fbc3900f7..77ff718f0 100644 --- 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 @@ -15,12 +15,12 @@ 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; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; public class FilterWidgetFactory implements RemoteViewsService.RemoteViewsFactory { private final Context context; private final int appWidgetId; - private final SyncManager syncManager; + private final BaseRepository baseRepository; @NonNull private final List<FilterWidgetCard> data = new ArrayList<>(); @@ -28,7 +28,7 @@ public class FilterWidgetFactory implements RemoteViewsService.RemoteViewsFactor 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); + this.baseRepository = new BaseRepository(context); } @Override 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 index e5afe1a0f..3f90acebb 100644 --- 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 @@ -3,24 +3,20 @@ 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.IResponseCallback; import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; -public class FilterWidgetViewModel extends AndroidViewModel { +public class FilterWidgetViewModel extends BaseViewModel { @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() { @@ -28,6 +24,6 @@ public class FilterWidgetViewModel extends AndroidViewModel { } public void updateFilterWidget(@NonNull IResponseCallback<Integer> callback) { - syncManager.createFilterWidget(config$.getValue(), callback); + baseRepository.createFilterWidget(config$.getValue(), callback); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java index ef1451c23..1ccdfc0be 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java @@ -1,31 +1,29 @@ package it.niedermann.nextcloud.deck.ui.widget.singlecard; -import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.saveBrandColors; - import android.appwidget.AppWidgetManager; import android.content.Intent; import android.os.Bundle; -import android.view.Menu; -import android.view.View; -import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.model.Board; +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.ui.MainActivity; import it.niedermann.nextcloud.deck.ui.card.SelectCardListener; -import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.ui.main.MainActivity; public class SelectCardForWidgetActivity extends MainActivity implements SelectCardListener { private int appWidgetId; - @ColorInt private int originalBrandColor; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + final var intent = getIntent(); if (intent == null) { finish(); @@ -40,39 +38,22 @@ public class SelectCardForWidgetActivity extends MainActivity implements SelectC if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { finish(); } - originalBrandColor = ThemeUtils.readBrandMainColor(this); } @Override - public void onCardSelected(FullCard fullCard) { - mainViewModel.addOrUpdateSingleCardWidget(appWidgetId, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId()); - final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, + public void onCardSelected(@NonNull FullCard fullCard, long boardId) { + mainViewModel.addOrUpdateSingleCardWidget(appWidgetId, fullCard.getAccountId(), boardId, fullCard.getLocalId()); + final var intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, getApplicationContext(), SingleCardWidget.class) .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - setResult(RESULT_OK, updateIntent); - getApplicationContext().sendBroadcast(updateIntent); - saveBrandColors(this, originalBrandColor); + setResult(RESULT_OK, intent); + getApplicationContext().sendBroadcast(intent); finish(); } @Override - protected void setCurrentBoard(@NonNull Board board) { - super.setCurrentBoard(board); - binding.listMenuButton.setVisibility(View.GONE); - binding.fab.setVisibility(View.GONE); + protected void applyBoard(@NonNull Account account, @NonNull Map<Integer, Long> navigationMap, @Nullable FullBoard currentBoard) { + super.applyBoard(account, navigationMap, currentBoard); binding.toolbar.setTitle(R.string.simple_select); } - - @Override - protected void showEditButtonsIfPermissionsGranted() { - binding.fab.hide(); - binding.listMenuButton.setVisibility(View.GONE); - binding.emptyContentViewStacks.hideDescription(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return true; - } - } 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 bdfd111d0..b7d6eac44 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 @@ -23,7 +23,7 @@ import java.util.concurrent.Executors; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; import it.niedermann.nextcloud.deck.ui.card.EditActivity; import it.niedermann.nextcloud.deck.util.DateUtil; @@ -32,12 +32,12 @@ public class SingleCardWidget extends AppWidgetProvider { private final ExecutorService executor = Executors.newCachedThreadPool(); void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { - final SyncManager syncManager = new SyncManager(context); + final var baseRepository = new BaseRepository(context); for (int appWidgetId : appWidgetIds) { executor.submit(() -> { try { - final FullSingleCardWidgetModel fullModel = syncManager.getSingleCardWidgetModelDirectly(appWidgetId); + final FullSingleCardWidgetModel fullModel = baseRepository.getSingleCardWidgetModelDirectly(appWidgetId); final Intent intent = EditActivity.createEditCardIntent(context, fullModel.getAccount(), fullModel.getModel().getBoardId(), fullModel.getFullCard().getLocalId()); final PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT)); @@ -142,10 +142,10 @@ public class SingleCardWidget extends AppWidgetProvider { @Override public void onDeleted(Context context, int[] appWidgetIds) { - final SyncManager syncManager = new SyncManager(context); + final var baseRepository = new BaseRepository(context); for (int appWidgetId : appWidgetIds) { - syncManager.deleteSingleCardWidgetModel(appWidgetId); + baseRepository.deleteSingleCardWidgetModel(appWidgetId); } super.onDeleted(context, appWidgetIds); 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 index db24afa43..1e5760110 100644 --- 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 @@ -15,19 +15,19 @@ 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.persistence.BaseRepository; 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 final BaseRepository baseRepository; 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); + this.baseRepository = new BaseRepository(context); } @Override @@ -38,7 +38,7 @@ public class SingleCardWidgetFactory implements RemoteViewsService.RemoteViewsFa @Override public void onDataSetChanged() { try { - this.model = syncManager.getSingleCardWidgetModelDirectly(appWidgetId); + this.model = baseRepository.getSingleCardWidgetModelDirectly(appWidgetId); } catch (NoSuchElementException e) { this.model = null; } 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 3d7d1100d..dc3e50416 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 @@ -21,9 +21,9 @@ import java.util.concurrent.Executors; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; 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.persistence.BaseRepository; import it.niedermann.nextcloud.deck.ui.card.EditActivity; +import it.niedermann.nextcloud.deck.ui.main.MainActivity; public class StackWidget extends AppWidgetProvider { private static final int PENDING_INTENT_OPEN_APP_RQ = 0; @@ -57,19 +57,19 @@ public class StackWidget extends AppWidgetProvider { @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); - final SyncManager syncManager = new SyncManager(context); + final var baseRepository = new BaseRepository(context); for (int appWidgetId : appWidgetIds) { DeckLog.info("Delete", StackWidget.class.getSimpleName(), "with id", appWidgetId); - syncManager.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + StackWidget.class.getSimpleName() + " with id " + appWidgetId)); + baseRepository.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + StackWidget.class.getSimpleName() + " with id " + appWidgetId)); } } private static void updateAppWidget(@NonNull ExecutorService executor, @NonNull Context context, AppWidgetManager awm, int[] appWidgetIds) { - final SyncManager syncManager = new SyncManager(context); + final var baseRepository = new BaseRepository(context); for (int appWidgetId : appWidgetIds) { executor.submit(() -> { - if (syncManager.filterWidgetExists(appWidgetId)) { + if (baseRepository.filterWidgetExists(appWidgetId)) { final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack); final Intent serviceIntent = new Intent(context, StackWidgetService.class); @@ -88,9 +88,9 @@ public class StackWidget extends AppWidgetProvider { 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()); + baseRepository.getFilterWidget(appWidgetId, response -> { + final Stack stack = baseRepository.getStackDirectly(response.getAccounts().get(0).getBoards().get(0).getStacks().get(0).getStackId()); + @ColorInt final Integer boardColor = baseRepository.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); 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 3df706c21..2b6248c4e 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 @@ -3,23 +3,19 @@ package it.niedermann.nextcloud.deck.ui.widget.stack; import android.app.Application; import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; import it.niedermann.nextcloud.deck.api.ResponseCallback; import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel; @SuppressWarnings("WeakerAccess") -public class StackWidgetConfigurationViewModel extends AndroidViewModel { - - private final SyncManager syncManager; +public class StackWidgetConfigurationViewModel extends BaseViewModel { public StackWidgetConfigurationViewModel(@NonNull Application application) { super(application); - this.syncManager = new SyncManager(application); } public void addStackWidget(@NonNull FilterWidget config, @NonNull ResponseCallback<Integer> callback) { - syncManager.createFilterWidget(config, callback); + baseRepository.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 e44fd9876..d5b4c8743 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 @@ -18,13 +18,13 @@ import java.util.NoSuchElementException; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; import it.niedermann.nextcloud.deck.ui.card.EditActivity; public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory { private final Context context; private final int appWidgetId; - private final SyncManager syncManager; + private final BaseRepository baseRepository; @NonNull private final List<FilterWidgetCard> data = new ArrayList<>(); @@ -33,7 +33,7 @@ public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory this.context = context; appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - this.syncManager = new SyncManager(context); + this.baseRepository = new BaseRepository(context); } @Override @@ -44,7 +44,7 @@ public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory @Override public void onDataSetChanged() { try { - final List<FilterWidgetCard> response = syncManager.getCardsForFilterWidget(appWidgetId); + final List<FilterWidgetCard> response = baseRepository.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())); @@ -78,7 +78,7 @@ public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_stack_entry); widget_entry.setTextViewText(R.id.widget_entry_content_tv, filterWidgetCard.getCard().getCard().getTitle()); - final Intent intent = EditActivity.createEditCardIntent(context, syncManager.readAccountDirectly(filterWidgetCard.getCard().getAccountId()), filterWidgetCard.getStack().getBoardId(), filterWidgetCard.getCard().getLocalId()); + final Intent intent = EditActivity.createEditCardIntent(context, baseRepository.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); 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 index bbb1b0af3..fffc0a3c3 100644 --- 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 @@ -30,7 +30,7 @@ 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.persistence.BaseRepository; import it.niedermann.nextcloud.deck.ui.card.EditActivity; public class UpcomingWidget extends AppWidgetProvider { @@ -43,23 +43,23 @@ public class UpcomingWidget extends AppWidgetProvider { @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); - final SyncManager syncManager = new SyncManager(context); + final BaseRepository baseRepository = new BaseRepository(context); for (int appWidgetId : appWidgetIds) { executor.submit(() -> { - if (syncManager.filterWidgetExists(appWidgetId)) { + if (baseRepository.filterWidgetExists(appWidgetId)) { DeckLog.warn(UpcomingWidget.class.getSimpleName(), "with id", appWidgetId, "already exists, perform update instead."); updateAppWidget(executor, context, appWidgetManager, appWidgetIds); } else { - final List<Account> accountsList = syncManager.readAccountsDirectly(); + final List<Account> accountsList = baseRepository.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())); + fwa.setUsers(new FilterWidgetUser(baseRepository.getUserByUidDirectly(account.getId(), account.getUserName()).getLocalId())); return fwa; }).collect(Collectors.toList())); - syncManager.createFilterWidget(config, new IResponseCallback<>() { + baseRepository.createFilterWidget(config, new IResponseCallback<>() { @Override public void onResponse(Integer response) { DeckLog.verbose("Successfully created", UpcomingWidget.class.getSimpleName(), "with id", appWidgetId); @@ -96,8 +96,8 @@ public class UpcomingWidget extends AppWidgetProvider { } else if (PENDING_INTENT_ACTION_EDIT.equals(intent.getAction())) { if (intent.hasExtra(PENDING_INTENT_PARAM_ACCOUNT_ID) && intent.hasExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID)) { executor.submit(() -> { - final SyncManager syncManager = new SyncManager(context); - context.startActivity(EditActivity.createEditCardIntent(context, syncManager.readAccountDirectly(intent.getLongExtra(PENDING_INTENT_PARAM_ACCOUNT_ID, -1)), syncManager.getBoardLocalIdByLocalCardIdDirectly(intent.getLongExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID, -1)), intent.getLongExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID, -1))); + final var baseRepository = new BaseRepository(context); + context.startActivity(EditActivity.createEditCardIntent(context, baseRepository.readAccountDirectly(intent.getLongExtra(PENDING_INTENT_PARAM_ACCOUNT_ID, -1)), baseRepository.getBoardLocalIdByLocalCardIdDirectly(intent.getLongExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID, -1)), intent.getLongExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID, -1))); }); } else { DeckLog.error(PENDING_INTENT_PARAM_ACCOUNT_ID, "and", PENDING_INTENT_PARAM_LOCAL_CARD_ID, "must be provided for action", PENDING_INTENT_ACTION_EDIT); @@ -110,11 +110,11 @@ public class UpcomingWidget extends AppWidgetProvider { @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); - final SyncManager syncManager = new SyncManager(context); + final var baseRepository = new BaseRepository(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)); + baseRepository.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + UpcomingWidget.class.getSimpleName() + " with id " + appWidgetId)); } } 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 index c23e4ca3d..9f98bf6cb 100644 --- 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 @@ -16,7 +16,7 @@ import it.niedermann.android.util.DimensionUtil; 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.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem; import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterSectionItem; import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsUtil; @@ -24,7 +24,7 @@ import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsUtil; public class UpcomingWidgetFactory implements RemoteViewsService.RemoteViewsFactory { private final Context context; private final int appWidgetId; - private final SyncManager syncManager; + private final BaseRepository baseRepository; private final int headerHorizontalPadding; private final int headerVerticalPaddingNth; @@ -34,7 +34,7 @@ public class UpcomingWidgetFactory implements RemoteViewsService.RemoteViewsFact 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.baseRepository = new BaseRepository(context); this.headerHorizontalPadding = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1hx); this.headerVerticalPaddingNth = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_2x); } @@ -47,7 +47,7 @@ public class UpcomingWidgetFactory implements RemoteViewsService.RemoteViewsFact @Override public void onDataSetChanged() { try { - final List<UpcomingCardsAdapterItem> response = syncManager.getCardsForUpcomingCardsForWidget(); + final List<UpcomingCardsAdapterItem> response = baseRepository.getCardsForUpcomingCardsForWidget(); DeckLog.verbose(UpcomingWidgetFactory.class.getSimpleName(), "with id", appWidgetId, "fetched", response.size(), "cards from the database."); data.clear(); data.addAll(UpcomingCardsUtil.addDueDateSeparators(context, response)); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java index 578f7f616..6ce764191 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java @@ -1,40 +1,49 @@ package it.niedermann.nextcloud.deck.util; +import static java.util.stream.Collectors.toList; + +import android.content.Context; import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; -import androidx.activity.ComponentActivity; import androidx.annotation.NonNull; import androidx.viewbinding.ViewBinding; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; + import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; public abstract class AutoCompleteAdapter<ItemType extends IRemoteEntity> extends BaseAdapter implements Filterable { - public static final long NO_CARD = Long.MIN_VALUE; - public static final long ITEM_CREATE = Long.MIN_VALUE; - @NonNull - protected final ComponentActivity activity; @NonNull private List<ItemType> itemList = new ArrayList<>(); @NonNull - protected List<ItemType> itemsToExclude = new ArrayList<>(); + private final List<ItemType> itemsToExclude = new ArrayList<>(); @NonNull protected SyncManager syncManager; - protected final long accountId; + protected final Account account; protected final long boardId; - protected final long cardId; + protected final ReactiveLiveData<String> constraint$ = new ReactiveLiveData<String>(); + private final AutoCompleteFilter filter = new AutoCompleteFilter() { + @Override + protected Filter.FilterResults performFiltering(CharSequence constraint) { + constraint$.postValue(constraint == null ? "" : constraint.toString()); + return filterResults; + } + }; - protected AutoCompleteAdapter(@NonNull ComponentActivity activity, long accountId, long boardId, long cardId) { - this.activity = activity; - this.accountId = accountId; + protected AutoCompleteAdapter(@NonNull Context context, @NonNull Account account, long boardId) throws NextcloudFilesAppAccountNotFoundException { + this.account = account; this.boardId = boardId; - this.cardId = cardId; - this.syncManager = new SyncManager(activity); + this.syncManager = new SyncManager(context, account); } @Override @@ -49,7 +58,20 @@ public abstract class AutoCompleteAdapter<ItemType extends IRemoteEntity> extend @Override public long getItemId(int position) { - return itemList.get(position).getLocalId(); + // Create proposals do have null as local ID + final var localId = itemList.get(position).getLocalId(); + return localId == null ? Long.MIN_VALUE : localId; + } + + protected List<ItemType> filterExcluded(@NonNull List<ItemType> users) { + return users.stream().filter(this::userIsNotInExclusionList).collect(toList()); + } + + private boolean userIsNotInExclusionList(@NonNull ItemType user) { + return itemsToExclude + .stream() + .map(IRemoteEntity::getLocalId) + .noneMatch(idToExclude -> Objects.equals(user.getLocalId(), idToExclude)); } protected static class ViewHolder<ViewBindingType extends ViewBinding> { @@ -64,7 +86,7 @@ public abstract class AutoCompleteAdapter<ItemType extends IRemoteEntity> extend protected final Filter.FilterResults filterResults = new Filter.FilterResults(); @Override - protected void publishResults(CharSequence constraint, FilterResults results) { + public void publishResults(CharSequence constraint, FilterResults results) { if (results != null && results.count > 0) { if (!itemList.equals(results.values)) { //noinspection unchecked @@ -75,13 +97,33 @@ public abstract class AutoCompleteAdapter<ItemType extends IRemoteEntity> extend notifyDataSetInvalidated(); } } + + private void publishResults(List<ItemType> list) { + DeckLog.verbose("New result list", list.stream().map(IRemoteEntity::toString).collect(toList())); + filterResults.values = list; + filterResults.count = list.size(); + publishResults("", filterResults); + } + + public Filter.FilterResults getFilter() { + return filterResults; + } + } + + protected void publishResults(List<ItemType> list) { + filter.publishResults(list); + } + + @Override + public Filter getFilter() { + return filter; } public void exclude(ItemType item) { this.itemsToExclude.add(item); } - public void include(ItemType item) { + public void doNotLongerExclude(ItemType item) { this.itemsToExclude.remove(item); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java index a2a90d4e0..93575f401 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java @@ -1,10 +1,16 @@ package it.niedermann.nextcloud.deck.util; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +/** + * If we really want <strong>this</strong>, we should default to {@link Executors#newWorkStealingPool()}. + * Though I recommend to distinguish between blocking threads and non-blocking threads (like network operations), where it does not make sense to limit it to available CPU cores. + */ +@Deprecated(forRemoval = true) public class ExecutorServiceProvider { private static final int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/OnTextChangedWatcher.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/OnTextChangedWatcher.java new file mode 100644 index 000000000..81a21fbc8 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/OnTextChangedWatcher.java @@ -0,0 +1,35 @@ +package it.niedermann.nextcloud.deck.util; + +import android.text.Editable; +import android.text.TextWatcher; + +import androidx.annotation.NonNull; + +import java.util.function.Consumer; + +/** + * Simple {@link TextWatcher} which only listens on {@link #onTextChanged(CharSequence, int, int, int)} and is therefore usable as {@link FunctionalInterface} + */ +public class OnTextChangedWatcher implements TextWatcher { + + private final Consumer<String> consumer; + + public OnTextChangedWatcher(@NonNull Consumer<String> consumer) { + this.consumer = consumer; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + consumer.accept(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) { + + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java index 6c4b091fc..8eb0c27b7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java @@ -23,8 +23,13 @@ public class VCardUtil { final var cr = context.getContentResolver(); try (final var cursor = cr.query(contactUri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - final String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)); - return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); + final var columnIndex = cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY); + if (columnIndex >= 0) { + final String lookupKey = cursor.getString(columnIndex); + return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey); + } else { + throw new NoSuchElementException("Could not find column index for " + ContactsContract.Contacts.LOOKUP_KEY); + } } else { throw new NoSuchElementException("Cursor has zero entries"); } 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 deleted file mode 100644 index 12f3418c3..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java +++ /dev/null @@ -1,84 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import static java.time.temporal.ChronoUnit.DAYS; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.ColorInt; -import androidx.annotation.ColorRes; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Px; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.widget.TextViewCompat; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; - -import java.time.LocalDate; - -import it.niedermann.android.util.DimensionUtil; -import it.niedermann.nextcloud.deck.R; - -public final class ViewUtil { - - private ViewUtil() { - throw new UnsupportedOperationException("This class must not get instantiated"); - } - - public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @DrawableRes int errorResource) { - addAvatar(avatar, baseUrl, userId, DimensionUtil.INSTANCE.dpToPx(avatar.getContext(), R.dimen.avatar_size), errorResource); - } - - public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @Px int avatarSizeInPx, @DrawableRes int errorResource) { - final String uri = baseUrl + "/index.php/avatar/" + Uri.encode(userId) + "/" + avatarSizeInPx; - Glide.with(avatar.getContext()) - .load(uri) - .placeholder(errorResource) - .error(errorResource) - .apply(RequestOptions.circleCropTransform()) - .into(avatar); - } - - public static void themeDueDate(@NonNull Context context, @NonNull TextView cardDueDate, @NonNull LocalDate dueDate) { - long diff = DAYS.between(LocalDate.now(), dueDate); - - int backgroundDrawable = 0; - int textColor = isDarkTheme(context) ? R.color.dark_fg_primary : R.color.grey600; - - if (diff == 1) { - // due date: tomorrow - backgroundDrawable = R.drawable.due_tomorrow_background; - } else if (diff == 0) { - // due date: today - backgroundDrawable = R.drawable.due_today_background; - } else if (diff < 0) { - // due date: overdue - backgroundDrawable = R.drawable.due_overdue_background; - textColor = R.color.overdue_text_color; - } - - cardDueDate.setBackgroundResource(backgroundDrawable); - cardDueDate.setTextColor(ContextCompat.getColor(context, textColor)); - TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(ContextCompat.getColor(context, textColor))); - } - - public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @ColorInt int color) { - final var drawable = ContextCompat.getDrawable(context, imageId); - assert drawable != null; - final var wrapped = DrawableCompat.wrap(drawable).mutate(); - DrawableCompat.setTint(wrapped, color); - return drawable; - } - - public static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @ColorRes int colorRes) { - imageView.setImageTintList(ColorStateList.valueOf(ContextCompat.getColor(context, colorRes))); - } -} diff --git a/app/src/main/res/drawable/selected.xml b/app/src/main/res/drawable/selected_check.xml index f28dd14cd..60184ea55 100644 --- a/app/src/main/res/drawable/selected.xml +++ b/app/src/main/res/drawable/selected_check.xml @@ -2,13 +2,13 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:enterFadeDuration="@android:integer/config_shortAnimTime" android:state_selected="true"> <layer-list> - <item> + <item android:id="@+id/background"> <shape android:shape="oval"> <solid android:color="@color/defaultBrand" /> <stroke android:width="1dp" android:color="@android:color/white" /> </shape> </item> - <item android:drawable="@drawable/circle_alpha_check_36dp" /> + <item android:id="@+id/foreground" android:drawable="@drawable/circle_alpha_check_36dp" /> </layer-list> </item> </selector>
\ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index decf08fcb..1bc25e332 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -10,10 +10,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:actionBarSize" + android:layout_height="wrap_content" app:navigationIcon="@drawable/ic_arrow_back_white_24dp" tools:title="@string/about" /> diff --git a/app/src/main/res/layout/activity_archived.xml b/app/src/main/res/layout/activity_archived.xml index d5e9646a8..b540b5f1f 100644 --- a/app/src/main/res/layout/activity_archived.xml +++ b/app/src/main/res/layout/activity_archived.xml @@ -10,10 +10,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:actionBarSize" + android:layout_height="wrap_content" app:navigationIcon="@drawable/ic_arrow_back_white_24dp" tools:title="@string/archived_cards" /> </com.google.android.material.appbar.AppBarLayout> diff --git a/app/src/main/res/layout/activity_attachments.xml b/app/src/main/res/layout/activity_attachments.xml index af6d472f4..784dd1090 100644 --- a/app/src/main/res/layout/activity_attachments.xml +++ b/app/src/main/res/layout/activity_attachments.xml @@ -12,7 +12,7 @@ android:layout_height="wrap_content" android:background="@android:color/background_dark"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/app/src/main/res/layout/activity_edit.xml b/app/src/main/res/layout/activity_edit.xml index 0bb4cba59..1da20f6c8 100644 --- a/app/src/main/res/layout/activity_edit.xml +++ b/app/src/main/res/layout/activity_edit.xml @@ -10,7 +10,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -27,7 +27,7 @@ android:maxLines="5" tools:text="@tools:sample/lorem" /> - </androidx.appcompat.widget.Toolbar> + </com.google.android.material.appbar.MaterialToolbar> <com.google.android.material.tabs.TabLayout android:id="@+id/tab_layout" diff --git a/app/src/main/res/layout/activity_exception.xml b/app/src/main/res/layout/activity_exception.xml index c851e8f82..4923c701b 100644 --- a/app/src/main/res/layout/activity_exception.xml +++ b/app/src/main/res/layout/activity_exception.xml @@ -10,10 +10,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:actionBarSize" + android:layout_height="wrap_content" tools:title="@string/simple_exception" /> </com.google.android.material.appbar.AppBarLayout> diff --git a/app/src/main/res/layout/activity_filter_widget.xml b/app/src/main/res/layout/activity_filter_widget.xml index cf125c65d..fbd600073 100644 --- a/app/src/main/res/layout/activity_filter_widget.xml +++ b/app/src/main/res/layout/activity_filter_widget.xml @@ -11,10 +11,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:actionBarSize" + android:layout_height="wrap_content" app:title="@string/add_filter_widget" /> </com.google.android.material.appbar.AppBarLayout> diff --git a/app/src/main/res/layout/activity_import_account.xml b/app/src/main/res/layout/activity_import_account.xml index 7575a3be2..b247804ed 100644 --- a/app/src/main/res/layout/activity_import_account.xml +++ b/app/src/main/res/layout/activity_import_account.xml @@ -30,7 +30,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" - android:layout_centerVertical="true" android:layout_marginBottom="48dp" android:gravity="center_horizontal" android:textSize="24sp" @@ -71,6 +70,7 @@ android:layout_marginTop="@dimen/spacer_4x" android:indeterminate="true" android:indeterminateTint="@color/defaultBrand" + android:progressTint="@color/defaultBrand" android:visibility="gone" /> <TextView diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a02b252ac..81cdd33ca 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -11,7 +11,7 @@ android:id="@+id/coordinatorLayout" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.MainActivity"> + tools:context=".ui.main.MainActivity"> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:id="@+id/swipe_refresh_layout" @@ -108,7 +108,7 @@ android:background="?attr/colorPrimary" tools:title="Deck"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="0dp" android:layout_height="wrap_content" @@ -118,7 +118,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/searchToolbar" android:layout_width="0dp" android:layout_height="wrap_content" @@ -138,7 +138,7 @@ android:inputType="text" android:maxLines="1" tools:hint="@string/app_name_short" /> - </androidx.appcompat.widget.Toolbar> + </com.google.android.material.appbar.MaterialToolbar> <ImageButton android:id="@+id/enableSearch" @@ -221,6 +221,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:background="@null" app:tabGravity="center" app:tabMode="fixed" /> @@ -246,6 +247,7 @@ android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" android:text="@string/add_card" + android:visibility="gone" app:icon="@drawable/ic_add_white_24dp" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_manage_accounts.xml b/app/src/main/res/layout/activity_manage_accounts.xml index bbf1b75c4..882baeb1b 100644 --- a/app/src/main/res/layout/activity_manage_accounts.xml +++ b/app/src/main/res/layout/activity_manage_accounts.xml @@ -10,10 +10,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" + android:layout_height="wrap_content" app:contentInsetStartWithNavigation="0dp" app:navigationIcon="@drawable/ic_arrow_back_white_24dp" app:title="@string/manage_accounts" diff --git a/app/src/main/res/layout/activity_pick_stack.xml b/app/src/main/res/layout/activity_pick_stack.xml index 8c456fc13..f86bdb45f 100644 --- a/app/src/main/res/layout/activity_pick_stack.xml +++ b/app/src/main/res/layout/activity_pick_stack.xml @@ -15,10 +15,10 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:actionBarSize" + android:layout_height="wrap_content" app:title="@string/add_card" /> </com.google.android.material.appbar.AppBarLayout> @@ -50,25 +50,24 @@ </com.google.android.material.textfield.TextInputLayout> - <ScrollView + <androidx.core.widget.NestedScrollView android:id="@+id/fragment_container_wrapper" android:layout_width="match_parent" android:layout_height="0dp" android:layout_above="@+id/buttonBar" android:layout_below="@id/appBarLayout" - android:padding="@dimen/spacer_2x" + android:paddingHorizontal="@dimen/spacer_2x" app:layout_constraintBottom_toTopOf="@id/buttonBar" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/inputWrapper" - app:layout_constraintVertical_weight="1" - tools:background="@color/bg_highlighted"> + app:layout_constraintVertical_weight="1"> - <FrameLayout + <androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="wrap_content" /> - </ScrollView> + </androidx.core.widget.NestedScrollView> <LinearLayout android:id="@+id/buttonBar" @@ -95,11 +94,11 @@ <com.google.android.material.button.MaterialButton android:id="@+id/submit" style="@style/Widget.Material3.Button" - android:enabled="false" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacer_1x" android:layout_weight=".5" + android:enabled="false" android:text="@string/simple_add" app:backgroundTint="@color/defaultBrand" /> </LinearLayout> diff --git a/app/src/main/res/layout/activity_push_notification.xml b/app/src/main/res/layout/activity_push_notification.xml index d50553892..4aca3a744 100644 --- a/app/src/main/res/layout/activity_push_notification.xml +++ b/app/src/main/res/layout/activity_push_notification.xml @@ -11,10 +11,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:actionBarSize" + android:layout_height="wrap_content" app:navigationIcon="@drawable/ic_arrow_back_white_24dp" app:title="@string/app_name" /> </com.google.android.material.appbar.AppBarLayout> diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 07f7a62fe..c04a45705 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,21 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/settings_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> - <com.google.android.material.appbar.AppBarLayout + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + app:navigationIcon="@drawable/ic_arrow_back_white_24dp" + app:title="@string/simple_settings" /> - <androidx.appcompat.widget.Toolbar - android:id="@+id/toolbar" - android:layout_width="match_parent" - android:layout_height="?android:actionBarSize" - app:navigationIcon="@drawable/ic_arrow_back_white_24dp" - app:title="@string/simple_settings" /> - </com.google.android.material.appbar.AppBarLayout> + <androidx.fragment.app.FragmentContainerView + android:id="@+id/settingsFragment" + android:name="it.niedermann.nextcloud.deck.ui.settings.SettingsFragment" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/activity_upcoming_cards.xml b/app/src/main/res/layout/activity_upcoming_cards.xml index 4e3f36736..23516ff78 100644 --- a/app/src/main/res/layout/activity_upcoming_cards.xml +++ b/app/src/main/res/layout/activity_upcoming_cards.xml @@ -11,10 +11,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:actionBarSize" + android:layout_height="wrap_content" app:navigationIcon="@drawable/ic_arrow_back_white_24dp" tools:title="@string/widget_upcoming_title" /> </com.google.android.material.appbar.AppBarLayout> diff --git a/app/src/main/res/layout/dialog_account_switcher.xml b/app/src/main/res/layout/dialog_account_switcher.xml index 9ca6cba5d..048fc0bcd 100644 --- a/app/src/main/res/layout/dialog_account_switcher.xml +++ b/app/src/main/res/layout/dialog_account_switcher.xml @@ -60,7 +60,7 @@ android:scaleType="center" android:scaleX=".7" android:scaleY=".7" - app:srcCompat="@drawable/selected" /> + app:srcCompat="@drawable/selected_check" /> </LinearLayout> <View diff --git a/app/src/main/res/layout/dialog_move_card.xml b/app/src/main/res/layout/dialog_move_card.xml index 0466ed915..a8d70fcdd 100644 --- a/app/src/main/res/layout/dialog_move_card.xml +++ b/app/src/main/res/layout/dialog_move_card.xml @@ -1,18 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="vertical" - android:paddingStart="@dimen/spacer_2x" + android:paddingHorizontal="@dimen/spacer_2x" android:paddingTop="@dimen/spacer_2x" - android:paddingEnd="@dimen/spacer_2x" android:paddingBottom="@dimen/spacer_1x"> <TextView android:id="@+id/title" - style="@style/TextAppearance.AppCompat.Title" + style="@style/TextAppearance.Material3.TitleLarge" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/spacer_1x" @@ -20,24 +19,22 @@ android:maxLines="5" tools:text="@string/action_card_move_title" /> - <ScrollView - android:id="@+id/scrollView" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@id/title" + <androidx.core.widget.NestedScrollView + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" android:layout_marginTop="@dimen/spacer_2x"> - <FrameLayout + <androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" /> - </ScrollView> + </androidx.core.widget.NestedScrollView> <TextView android:id="@+id/move_warning" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_below="@id/scrollView" android:layout_marginTop="@dimen/spacer_2x" android:drawablePadding="@dimen/spacer_3x" android:paddingStart="@dimen/spacer_3x" @@ -52,7 +49,6 @@ <com.google.android.flexbox.FlexboxLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/move_warning" android:layout_marginTop="@dimen/spacer_1x" android:orientation="horizontal" app:justifyContent="space_between"> @@ -76,4 +72,4 @@ android:text="@string/simple_move" android:textColor="@color/defaultBrand" /> </com.google.android.flexbox.FlexboxLayout> -</RelativeLayout>
\ No newline at end of file +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_about_credits_tab.xml b/app/src/main/res/layout/fragment_about_credits_tab.xml index fc026e5e1..02bb701ee 100644 --- a/app/src/main/res/layout/fragment_about_credits_tab.xml +++ b/app/src/main/res/layout/fragment_about_credits_tab.xml @@ -118,17 +118,5 @@ android:layout_height="wrap_content" android:padding="10dp" android:text="@string/about_translators_transifex" /> - - <TextView - style="?android:attr/listSeparatorTextViewStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/about_testers_title" /> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:padding="10dp" - android:text="@string/about_testers" /> </LinearLayout> </ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_pick_stack.xml b/app/src/main/res/layout/fragment_pick_stack.xml index 9769969ff..d543db414 100644 --- a/app/src/main/res/layout/fragment_pick_stack.xml +++ b/app/src/main/res/layout/fragment_pick_stack.xml @@ -1,7 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> @@ -10,19 +11,22 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:prompt="@string/choose_account" - tools:listitem="@layout/item_prepare_create_account" /> + android:visibility="gone" + tools:listitem="@layout/item_prepare_create_account" + tools:visibility="visible" /> <androidx.appcompat.widget.AppCompatSpinner android:id="@+id/board_select" android:layout_width="match_parent" android:layout_height="wrap_content" android:prompt="@string/choose_board" - tools:listitem="@layout/item_board" /> + tools:listitem="@layout/item_prepare_create_board" /> - <androidx.appcompat.widget.AppCompatSpinner + <androidx.recyclerview.widget.RecyclerView android:id="@+id/stack_select" android:layout_width="match_parent" android:layout_height="wrap_content" - android:prompt="@string/choose_list" - tools:listitem="@layout/item_board" /> + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:itemCount="4" + tools:listitem="@layout/item_prepare_create_stack" /> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_stack.xml b/app/src/main/res/layout/fragment_stack.xml index 481143860..c7d08ae8a 100644 --- a/app/src/main/res/layout/fragment_stack.xml +++ b/app/src/main/res/layout/fragment_stack.xml @@ -27,7 +27,7 @@ android:layout_height="match_parent" android:clipToPadding="false" android:paddingTop="@dimen/spacer_1x" - android:paddingBottom="80dp" + android:paddingBottom="@dimen/stack_bottom_padding" android:scrollbarStyle="outsideOverlay" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" diff --git a/app/src/main/res/layout/item_account_choose.xml b/app/src/main/res/layout/item_account_choose.xml index c2d7ed0fb..b5a6dd726 100644 --- a/app/src/main/res/layout/item_account_choose.xml +++ b/app/src/main/res/layout/item_account_choose.xml @@ -29,7 +29,7 @@ android:layout_height="12dp" android:layout_gravity="end|bottom" android:visibility="gone" - app:srcCompat="@drawable/selected" + app:srcCompat="@drawable/selected_check" tools:src="@drawable/ic_check_grey600_24dp" tools:visibility="visible" /> </FrameLayout> diff --git a/app/src/main/res/layout/item_board.xml b/app/src/main/res/layout/item_board.xml deleted file mode 100644 index 57e5bcbc3..000000000 --- a/app/src/main/res/layout/item_board.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<TextView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/boardName" - android:padding="@dimen/spacer_2x" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:textColor="@android:color/white" - android:background="@android:color/transparent" - tools:text="Awesome board name" />
\ No newline at end of file diff --git a/app/src/main/res/layout/item_card_compact.xml b/app/src/main/res/layout/item_card_compact.xml index 2886a675e..b7060e201 100644 --- a/app/src/main/res/layout/item_card_compact.xml +++ b/app/src/main/res/layout/item_card_compact.xml @@ -64,12 +64,10 @@ android:id="@+id/card_due_date" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@drawable/due_tomorrow_background" android:drawablePadding="@dimen/spacer_1hx" android:gravity="center" android:padding="@dimen/spacer_1hx" android:paddingEnd="@dimen/spacer_1x" - android:textColor="@color/fg_secondary" app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp" tools:text="tomorrow" /> @@ -82,7 +80,7 @@ android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@string/label_menu" android:padding="@dimen/spacer_1hx" - android:tint="?attr/colorAccent" + app:tint="?attr/colorAccent" app:srcCompat="@drawable/ic_menu" /> </LinearLayout> diff --git a/app/src/main/res/layout/item_card_default.xml b/app/src/main/res/layout/item_card_default.xml index b472bd12c..a33df5620 100644 --- a/app/src/main/res/layout/item_card_default.xml +++ b/app/src/main/res/layout/item_card_default.xml @@ -66,12 +66,10 @@ android:id="@+id/card_due_date" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@drawable/due_tomorrow_background" android:drawablePadding="@dimen/spacer_1hx" android:gravity="center" android:padding="@dimen/spacer_1hx" android:paddingEnd="@dimen/spacer_1x" - android:textColor="@color/fg_secondary" app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp" tools:text="tomorrow" /> diff --git a/app/src/main/res/layout/item_card_default_only_title.xml b/app/src/main/res/layout/item_card_default_only_title.xml index f16d096bc..868ff6594 100644 --- a/app/src/main/res/layout/item_card_default_only_title.xml +++ b/app/src/main/res/layout/item_card_default_only_title.xml @@ -60,12 +60,10 @@ android:id="@+id/card_due_date" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@drawable/due_tomorrow_background" android:drawablePadding="@dimen/spacer_1hx" android:gravity="center" android:padding="@dimen/spacer_1hx" android:paddingEnd="@dimen/spacer_1x" - android:textColor="@color/fg_secondary" app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp" tools:text="tomorrow" /> diff --git a/app/src/main/res/layout/item_filter_duetype.xml b/app/src/main/res/layout/item_filter_duetype.xml index a5943ff2f..1e3f5ac8e 100644 --- a/app/src/main/res/layout/item_filter_duetype.xml +++ b/app/src/main/res/layout/item_filter_duetype.xml @@ -17,12 +17,13 @@ tools:text="@tools:sample/lorem" /> <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/selected_check" android:layout_width="22dp" android:layout_height="22dp" android:layout_marginStart="@dimen/spacer_1x" app:layout_alignSelf="center" app:layout_flexShrink="0" - app:srcCompat="@drawable/selected" + app:srcCompat="@drawable/selected_check" tools:src="@drawable/ic_check_grey600_24dp" /> </com.google.android.flexbox.FlexboxLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/item_filter_label.xml b/app/src/main/res/layout/item_filter_label.xml index 8592e3275..49bc47fcb 100644 --- a/app/src/main/res/layout/item_filter_label.xml +++ b/app/src/main/res/layout/item_filter_label.xml @@ -15,12 +15,13 @@ tools:text="@tools:sample/lorem" /> <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/selected_check" android:layout_width="22dp" android:layout_height="22dp" android:layout_marginStart="@dimen/spacer_1x" app:layout_alignSelf="center" app:layout_flexShrink="0" - app:srcCompat="@drawable/selected" + app:srcCompat="@drawable/selected_check" tools:src="@drawable/ic_check_grey600_24dp" /> </com.google.android.flexbox.FlexboxLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/item_filter_user.xml b/app/src/main/res/layout/item_filter_user.xml index 2f801d0a5..35324d859 100644 --- a/app/src/main/res/layout/item_filter_user.xml +++ b/app/src/main/res/layout/item_filter_user.xml @@ -23,10 +23,11 @@ tools:srcCompat="@tools:sample/avatars" /> <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/selected_check" android:layout_width="20dp" android:layout_height="20dp" android:layout_gravity="end|bottom" - app:srcCompat="@drawable/selected" /> + app:srcCompat="@drawable/selected_check" /> </FrameLayout> <TextView diff --git a/app/src/main/res/layout/item_prepare_create_stack.xml b/app/src/main/res/layout/item_prepare_create_stack.xml index 516941997..303c9328d 100644 --- a/app/src/main/res/layout/item_prepare_create_stack.xml +++ b/app/src/main/res/layout/item_prepare_create_stack.xml @@ -1,15 +1,32 @@ <?xml version="1.0" encoding="utf-8"?> -<TextView xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/stackTitle" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="middle" + android:background="?attr/selectableItemBackground" + android:orientation="horizontal" android:paddingStart="72dp" android:paddingTop="20dp" android:paddingEnd="@dimen/spacer_2x" - android:paddingBottom="20dp" - android:singleLine="true" - android:textAppearance="?attr/textAppearanceListItem" - tools:text="@tools:sample/full_names" /> + android:paddingBottom="20dp"> + <TextView + android:id="@+id/stackTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="middle" + android:singleLine="true" + android:textAppearance="?attr/textAppearanceListItem" + tools:text="@tools:sample/full_names" /> + + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/selected_check" + android:layout_width="22dp" + android:layout_height="22dp" + android:layout_marginStart="@dimen/spacer_1x" + app:srcCompat="@drawable/selected_check" + tools:src="@drawable/ic_check_grey600_24dp" /> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/item_tip.xml b/app/src/main/res/layout/item_tip.xml index fadb62e6b..d74648df6 100644 --- a/app/src/main/res/layout/item_tip.xml +++ b/app/src/main/res/layout/item_tip.xml @@ -5,8 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:paddingTop="@dimen/spacer_1x" - android:paddingBottom="@dimen/spacer_1x"> + android:paddingVertical="@dimen/spacer_1x"> <TextView android:id="@+id/tip" diff --git a/app/src/main/res/layout/widget_empty_content_view.xml b/app/src/main/res/layout/widget_empty_content_view.xml index 9fbac7470..ab8b3b9b9 100644 --- a/app/src/main/res/layout/widget_empty_content_view.xml +++ b/app/src/main/res/layout/widget_empty_content_view.xml @@ -1,30 +1,37 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:hint="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:padding="@dimen/spacer_2x"> + android:padding="@dimen/spacer_2x" + android:paddingHorizontal="@dimen/spacer_2x"> <androidx.appcompat.widget.AppCompatImageView android:id="@+id/image" - android:layout_width="match_parent" + android:layout_width="72dp" android:layout_height="72dp" android:layout_above="@+id/title" - android:layout_gravity="center" + android:layout_marginBottom="@dimen/spacer_2x" android:contentDescription="@null" android:tint="@color/fg_secondary" + app:layout_constraintBottom_toTopOf="@id/title" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" hint:src="@drawable/ic_app_logo" /> <TextView android:id="@+id/title" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerVertical="true" android:gravity="center" - android:paddingVertical="@dimen/spacer_2x" android:textAlignment="center" android:textSize="@dimen/empty_content_font_size" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" hint:text="@string/no_cards" /> <TextView @@ -32,8 +39,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/title" - android:layout_centerHorizontal="true" - android:paddingHorizontal="@dimen/spacer_2x" + android:layout_marginTop="@dimen/spacer_2x" android:textAlignment="center" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" hint:text="@string/add_a_new_card_using_the_button" /> -</RelativeLayout>
\ No newline at end of file +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 155896ff5..d3e6cb425 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,15 +1,43 @@ <?xml version="1.0" encoding="utf-8"?> <resources> + + <!-- ======================================= --> + <!-- Base Theme --> + <!-- ======================================= --> + <color name="primary">@android:color/black</color> <color name="accent">@android:color/white</color> + + <!-- ======================================= --> + <!-- Custom styles --> + <!-- TODO REMOVE --> + <!-- ======================================= --> + <color name="fg_secondary">#666</color> <color name="bg_highlighted">#212121</color> <color name="bg_info_box">#222222</color> <color name="bg_card">#1e1e1e</color> - <color name="bg_card_wrapper">@color/primary</color> <color name="defaultTextHighlightBackground">#55eeeeff</color> + <!-- ======================================= --> + <!-- Widgets --> + <!-- ======================================= --> + <color name="widget_outer_background">#dd121212</color> <color name="widget_background">#dd000000</color> <color name="widget_foreground">#d8d8d8</color> + + <!-- ======================================= --> + <!-- Static colors --> + <!-- Are theme independent and should match --> + <!-- the colors of the Deck server app. --> + <!-- ======================================= --> + + <!-- Due Date badges --> + <color name="due_tomorrow">#232323</color> + <color name="due_today">#ac7c06</color> + <color name="due_overdue">#aa2926</color> + <color name="due_text_tomorrow">#ffffff</color> + <color name="due_text_today">#ffffff</color> + <color name="due_text_overdue">#ffffff</color> </resources> diff --git a/app/src/main/res/values-v24/styles.xml b/app/src/main/res/values-v24/styles.xml deleted file mode 100644 index d6835c741..000000000 --- a/app/src/main/res/values-v24/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <!-- https://github.com/stefan-niedermann/nextcloud-deck/issues/444 --> - <style name="NavigationView"> - <item name="android:ellipsize">middle</item> - <item name="android:listDivider">@null</item> - <item name="android:colorControlHighlight">@android:color/transparent</item> - </style> -</resources>
\ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 99bede58e..1983b6367 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,29 +1,61 @@ <?xml version="1.0" encoding="utf-8"?> <resources> + + <!-- ======================================= --> + <!-- Base Theme --> + <!-- ======================================= --> + <color name="primary">@android:color/white</color> - <color name="accent">#000000</color> + <color name="accent">@android:color/black</color> <color name="defaultBrand">#0082C9</color> <color name="danger">#d40000</color> + + <!-- ======================================= --> + <!-- Custom styles --> + <!-- TODO REMOVE --> + <!-- ======================================= --> + + <color name="grey600">#757575</color> <color name="fg_secondary">#999</color> <color name="bg_highlighted">#eee</color> - <color name="grey600">#757575</color> <color name="bg_info_box">#dddddd</color> <color name="bg_card">@android:color/white</color> - <color name="bg_card_wrapper">#fafafa</color> - <color name="dark_fg_primary">#e5e5e5</color> <color name="defaultTextHighlightBackground">#2233334a</color> - <color name="activity_create">#00D400</color> - <color name="activity_delete">#D40000</color> + <!-- ======================================= --> + <!-- Widgets --> + <!-- ======================================= --> + + <color name="widget_outer_background">#eef9f9f9</color> + <color name="widget_background">#ddffffff</color> + <color name="widget_foreground">#222222</color> + <!-- ======================================= --> + <!-- Static colors --> + <!-- Are theme independent and should match --> + <!-- the colors of the Deck server app. --> + <!-- ======================================= --> - <!-- due date colors --> - <color name="due_tomorrow">#7fffc53a</color> - <color name="due_today">#7feca700</color> + <!-- Due Date badges --> + <color name="due_tomorrow">#f2f2f2</color> + <color name="due_today">#f1c14b</color> <color name="due_overdue">#ef6e6b</color> - <color name="overdue_text_color">#FFFFFF</color> + <color name="due_text_tomorrow">#666666</color> + <color name="due_text_today">#333333</color> + <color name="due_text_overdue">#ffffff</color> + + <!-- Activity --> + <color name="activity_create">#00D400</color> + <color name="activity_delete">#D40000</color> - <!-- board color picker colors --> + <!-- ======================================= --> + <!-- Static colors --> + <!-- These colors are stored in the backend --> + <!-- and must not be altered. They are --> + <!-- theme independent. --> + <!-- ======================================= --> + + <!-- Default colors for boards, labels, … --> <color name="board_default_color">#b6469d</color> <color name="board_default_custom_color">#616161</color> <string-array name="board_default_colors"> @@ -39,8 +71,4 @@ <item>#5b64b3</item> <item>#8855a8</item> </string-array> - - <color name="widget_outer_background">#eef9f9f9</color> - <color name="widget_background">#ddffffff</color> - <color name="widget_foreground">#222222</color> </resources> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 1acec4aaa..f7756172e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -7,6 +7,8 @@ <dimen name="spacer_3x">24dp</dimen> <dimen name="spacer_4x">32dp</dimen> + <dimen name="stack_bottom_padding">80dp</dimen> + <dimen name="compact_label_height">6dp</dimen> <dimen name="attachments_bottom_navigation_height">80dp</dimen> diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml index cc2771000..ba14b8366 100644 --- a/app/src/main/res/values/setup.xml +++ b/app/src/main/res/values/setup.xml @@ -2,7 +2,6 @@ <resources> <string name="shared_preference_last_sync" translatable="false">it.niedermann.nextcloud.deck.last_sync</string> <string name="shared_preference_last_background_sync" translatable="false">it.niedermann.nextcloud.deck.last_background_sync</string> - <string name="shared_preference_theme_main" translatable="false">it.niedermann.nextcloud.deck.theme_main</string> <string name="shared_preference_description_preview" translatable="false">it.niedermann.nextcloud.deck.description_preview</string> <string name="pref_key_wifi_only" translatable="false">wifiOnly</string> @@ -46,10 +45,10 @@ <item>@string/pref_value_theme_dark</item> </string-array> - <!-- To be concatenated with the account id --> <string name="shared_preference_last_account" translatable="false">it.niedermann.nextcloud.deck.last_account</string> - <string name="shared_preference_last_account_color" translatable="false">it.niedermann.nextcloud.deck.last_account_color</string> + <!-- To be concatenated with the account id --> <string name="shared_preference_last_board_for_account_" translatable="false">it.niedermann.nextcloud.deck.last_board_for_account_</string> + <!-- To be concatenated with the account id, underscore and board id --> <string name="shared_preference_last_stack_for_account_and_board_" translatable="false">it.niedermann.nextcloud.deck.last_stack_for_board_</string> <!-- Transitions --> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30cb73eec..e51f75722 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,8 +52,6 @@ <string name="about_translators_title">Translators</string> <string name="about_translators_transifex">Nextcloud community on %1$s</string> <string name="about_translators_transifex_label">Transifex</string> - <string name="about_testers_title">Testers</string> - <string name="about_testers" translatable="false">Julius, David</string> <string name="about_source_title">Source code</string> <string name="about_source">This project is hosted on GitHub: %1$s</string> <string name="about_issues_title">Issues</string> @@ -84,7 +82,7 @@ <string name="add_account">Add account</string> <string name="choose_account">Choose account</string> - <string name="add_card">Add new card</string> + <string name="add_card">Add card</string> <string name="activity">Activity</string> <string name="add_list">Add list</string> <string name="rename_list">Rename list</string> @@ -123,7 +121,7 @@ <string name="attachments">Attachments</string> <string name="no_cards">No cards yet</string> <string name="no_account">No account configured</string> - <string name="account_already_added">Account already added</string> + <string name="account_already_added">The account %1$s has already been added</string> <string name="account_is_getting_imported">Account is getting imported</string> <string name="not_synced_yet">Not synced yet</string> <string name="no_lists_yet">No lists yet</string> @@ -179,6 +177,7 @@ <string name="title_is_mandatory">Title is required</string> <string name="provide_at_least_a_title_or_description">Provide at least a title or description</string> <string name="welcome_text">Welcome to %1$s</string> + <string name="welcome_text_further_accounts">Add another account</string> <string name="maintenance_mode_explanation">The server %1$s is currently in maintenance mode. Please contact your administrator or try later again.</string> <string name="share_add_to_card">Add to card</string> <string name="share_success">Successfully added %1$s to %2$s</string> @@ -333,7 +332,7 @@ <string name="downloads">Downloads</string> <string name="files">Files</string> <string name="gallery">Gallery</string> - <string name="simple_attach">attach</string> + <string name="simple_attach">Attach</string> <string name="add_stack_widget">Add list widget</string> <string name="add_filter_widget">Add filter widget</string> <string name="simple_order">Order</string> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e29bb0793..d65645073 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -75,7 +75,7 @@ <style name="tabStyle" parent="Widget.Material3.TabLayout"> <item name="backgroundColor">@android:color/transparent</item> <item name="itemBackground">@android:color/transparent</item> - <item name="tabIndicatorColor">@color/defaultBrand</item> + <item name="tabIndicatorColor">?attr/colorAccent</item> <item name="tabTextColor">?attr/colorAccent</item> <item name="tabIconTint">?attr/colorAccent</item> </style> @@ -105,7 +105,7 @@ <style name="NavigationView"> <!-- https://github.com/stefan-niedermann/nextcloud-deck/issues/444 --> - <item name="android:ellipsize">end</item> + <item name="android:ellipsize">middle</item> <item name="android:listDivider">@null</item> <item name="android:colorControlHighlight">@android:color/transparent</item> </style> diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java index 6fcaef062..c50d5b3ae 100644 --- a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java +++ b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java @@ -3,7 +3,6 @@ package it.niedermann.nextcloud.deck.persistence.sync; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -38,13 +37,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import java.lang.reflect.InvocationTargetException; import java.time.Instant; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import it.niedermann.nextcloud.deck.TestUtil; import it.niedermann.nextcloud.deck.api.IResponseCallback; @@ -62,11 +59,11 @@ import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.Version; 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.WrappedLiveData; import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AbstractSyncDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.CardDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.StackDataProvider; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil; @RunWith(RobolectricTestRunner.class) public class SyncManagerTest { @@ -78,22 +75,14 @@ public class SyncManagerTest { private final ServerAdapter serverAdapter = mock(ServerAdapter.class); private final DataBaseAdapter dataBaseAdapter = mock(DataBaseAdapter.class); private final SyncHelper.Factory syncHelperFactory = mock(SyncHelper.Factory.class); + private final ConnectivityUtil connectivityUtil = mock(ConnectivityUtil.class); private SyncManager syncManager; @Before - public void setup() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - final var constructor = SyncManager.class.getDeclaredConstructor(Context.class, - DataBaseAdapter.class, - ServerAdapter.class, - ExecutorService.class, - SyncHelper.Factory.class); - constructor.setAccessible(true); - syncManager = constructor.newInstance(context, - dataBaseAdapter, - serverAdapter, - MoreExecutors.newDirectExecutorService(), - syncHelperFactory); + public void setup() { + when(dataBaseAdapter.getCurrentAccountId$()).thenReturn(new MutableLiveData<>()); + syncManager = new SyncManager(context, serverAdapter, connectivityUtil, syncHelperFactory, dataBaseAdapter, MoreExecutors.newDirectExecutorService()); } @Test @@ -250,15 +239,6 @@ public class SyncManagerTest { } @Test - public void testHasInternetConnection() { - when(serverAdapter.hasInternetConnection()).thenReturn(true); - assertTrue(syncManager.hasInternetConnection()); - - when(serverAdapter.hasInternetConnection()).thenReturn(false); - assertFalse(syncManager.hasInternetConnection()); - } - - @Test public void testReadAccountDirectly() { final var account = new Account(1337L, "Test", "Peter", "example.com"); when(dataBaseAdapter.readAccountDirectly(1337L)).thenReturn(account); @@ -268,7 +248,7 @@ public class SyncManagerTest { @Test public void testReadAccounts() throws InterruptedException { final var accounts = Collections.singletonList(new Account(1337L, "Test", "Peter", "example.com")); - final var wrappedAccounts = new WrappedLiveData<List<Account>>(); + final var wrappedAccounts = new MutableLiveData<List<Account>>(); wrappedAccounts.setValue(accounts); when(dataBaseAdapter.readAccounts()).thenReturn(wrappedAccounts); @@ -469,21 +449,6 @@ public class SyncManagerTest { // Bad paths - assertThrows(IllegalArgumentException.class, () -> syncManagerSpy.synchronize(new ResponseCallback<>(new Account(null)) { - @Override - public void onResponse(Boolean response) { - - } - })); - - //noinspection ConstantConditions - assertThrows(IllegalArgumentException.class, () -> syncManagerSpy.synchronize(new ResponseCallback<>(null) { - @Override - public void onResponse(Boolean response) { - - } - })); - final var syncHelper_negative = new SyncHelperMock(false); when(syncHelperFactory.create(any(), any(), any())).thenReturn(syncHelper_negative); diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java index ae013141d..f3c84a78c 100644 --- a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java +++ b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java @@ -37,14 +37,14 @@ public class DataBaseAdapterTest { @Before public void createAdapter() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - final var constructor = DataBaseAdapter.class.getDeclaredConstructor(Context.class, DeckDatabase.class, ExecutorService.class); + final var constructor = DataBaseAdapter.class.getDeclaredConstructor(Context.class, DeckDatabase.class, ExecutorService.class, ExecutorService.class); if (isPrivate(constructor.getModifiers())) { constructor.setAccessible(true); db = Room .inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), DeckDatabase.class) .allowMainThreadQueries() .build(); - adapter = constructor.newInstance(ApplicationProvider.getApplicationContext(), db, MoreExecutors.newDirectExecutorService()); + adapter = constructor.newInstance(ApplicationProvider.getApplicationContext(), db, MoreExecutors.newDirectExecutorService(), MoreExecutors.newDirectExecutorService()); } } diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java index 4282a5f1d..7cbf0d0d7 100644 --- a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java +++ b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java @@ -8,16 +8,19 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import java.net.MalformedURLException; + import it.niedermann.nextcloud.deck.TestUtil; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DeckDatabaseTestUtil; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; @RunWith(RobolectricTestRunner.class) public class AccountDaoTest extends AbstractDaoTest { @Test - public void testCreate() { + public void testCreate() throws MalformedURLException { final var accountToCreate = new Account(); accountToCreate.setName("test@example.com"); accountToCreate.setUserName("test"); @@ -31,7 +34,9 @@ public class AccountDaoTest extends AbstractDaoTest { assertEquals(Integer.valueOf(Capabilities.DEFAULT_COLOR), account.getColor()); assertEquals(Integer.valueOf(Capabilities.DEFAULT_TEXT_COLOR), account.getTextColor()); assertEquals("0.6.4", account.getServerDeckVersion()); - assertEquals("https://example.com/index.php/avatar/test/1337", account.getAvatarUrl(1337)); + final var expectedAvatarUrl = new SingleSignOnUrl("test@example.com", "https://example.com/index.php/avatar/test/1337"); + assertEquals(expectedAvatarUrl.getSsoAccountName(), account.getAvatarUrl(1337).getSsoAccountName()); + assertEquals(expectedAvatarUrl.toURL(), account.getAvatarUrl(1337).toURL()); assertEquals(1, db.getAccountDao().countAccountsDirectly()); assertNull(account.getEtag()); assertFalse(account.isMaintenanceEnabled()); diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java index 67bc0938a..9116fdaf1 100644 --- a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java +++ b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java @@ -61,7 +61,7 @@ public class BoardDaoTest extends AbstractDaoTest { boardVisibleArchived.setArchived(true); db.getBoardDao().update(boardInVisible1, boardInVisible2, boardInVisible3, boardVisibleArchived); - final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getBoardsForAccount(account.getId())); + final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getNotDeletedBoards(account.getId(), 1)); assertEquals(4, boards.size()); assertTrue(boards.stream().anyMatch((board -> boardVisible1.getLocalId().equals(board.getLocalId())))); assertTrue(boards.stream().anyMatch((board -> boardVisible2.getLocalId().equals(board.getLocalId())))); @@ -92,7 +92,7 @@ public class BoardDaoTest extends AbstractDaoTest { board4.setArchived(true); db.getBoardDao().update(board5, board6, board7, board4); - final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getArchivedBoardsForAccount(account.getId())); + final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getNotDeletedBoards(account.getId(), 1)); assertEquals(1, boards.size()); assertFalse(boards.stream().anyMatch((board -> board1.getLocalId().equals(board.getLocalId())))); assertFalse(boards.stream().anyMatch((board -> board2.getLocalId().equals(board.getLocalId())))); @@ -123,7 +123,7 @@ public class BoardDaoTest extends AbstractDaoTest { board4.setArchived(true); db.getBoardDao().update(board5, board6, board7, board4); - final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getNonArchivedBoardsForAccount(account.getId())); + final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getNotDeletedBoards(account.getId(), 0)); assertEquals(3, boards.size()); assertTrue(boards.stream().anyMatch((board -> board1.getLocalId().equals(board.getLocalId())))); assertTrue(boards.stream().anyMatch((board -> board2.getLocalId().equals(board.getLocalId())))); diff --git a/app/src/test/resources/robolectric.properties b/app/src/test/resources/robolectric.properties index 5d28440d3..e23ee50f1 100644 --- a/app/src/test/resources/robolectric.properties +++ b/app/src/test/resources/robolectric.properties @@ -1 +1 @@ -sdk=23, 30
\ No newline at end of file +sdk=24, 30
\ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1021009.txt b/fastlane/metadata/android/en-US/changelogs/1021009.txt index 6006910a8..485d181ba 100644 --- a/fastlane/metadata/android/en-US/changelogs/1021009.txt +++ b/fastlane/metadata/android/en-US/changelogs/1021009.txt @@ -1,3 +1,4 @@ - 🎨 Unify material theming with Nextcloud files app +- ↔️ Improve 'Move Card' Dialog (#972) - 🐞 Fix not themed filter indicator - 🐞 Fix loading cover images in multi account setup in Upcoming cards view
\ No newline at end of file diff --git a/reactive-livedata/build.gradle b/reactive-livedata/build.gradle new file mode 100644 index 000000000..4a15acfc1 --- /dev/null +++ b/reactive-livedata/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 33 + buildToolsVersion "31.0.0" + defaultConfig { + minSdkVersion 22 + targetSdkVersion 33 + } + namespace 'it.niedermann.android.reactivelivedata' + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } +} + +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' + + implementation "androidx.lifecycle:lifecycle-livedata:2.5.1" + implementation 'androidx.core:core:1.9.0' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.9.2' + testImplementation 'androidx.test:core:1.5.0' + testImplementation 'androidx.arch.core:core-testing:2.1.0' +} diff --git a/reactive-livedata/src/main/AndroidManifest.xml b/reactive-livedata/src/main/AndroidManifest.xml new file mode 100644 index 000000000..cc947c567 --- /dev/null +++ b/reactive-livedata/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest /> diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveData.java new file mode 100644 index 000000000..845e99e66 --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveData.java @@ -0,0 +1,216 @@ +package it.niedermann.android.reactivelivedata; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; +import androidx.core.util.Consumer; +import androidx.core.util.Pair; +import androidx.core.util.Predicate; +import androidx.core.util.Supplier; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.Transformations; + +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ExecutorService; + +import it.niedermann.android.reactivelivedata.combinator.DoubleCombinatorLiveData; +import it.niedermann.android.reactivelivedata.combinator.TripleCombinatorLiveData; +import it.niedermann.android.reactivelivedata.debounce.DebounceLiveData; +import it.niedermann.android.reactivelivedata.distinct.DistinctUntilChangedLiveData; +import it.niedermann.android.reactivelivedata.filter.FilterLiveData; +import it.niedermann.android.reactivelivedata.flatmap.FlatMapLiveData; +import it.niedermann.android.reactivelivedata.map.MapLiveData; +import it.niedermann.android.reactivelivedata.merge.MergeLiveData; +import it.niedermann.android.reactivelivedata.take.TakeLiveData; +import it.niedermann.android.reactivelivedata.tap.TapLiveData; +import kotlin.Triple; + +/** + * @see ReactiveLiveDataBuilder + */ +public class ReactiveLiveData<T> extends MediatorLiveData<T> implements ReactiveLiveDataBuilder<T> { + + public ReactiveLiveData(@Nullable LiveData<T> source) { + if (source == null) { + setValue(null); + } else { + addSource(source, this::setValue); + } + } + + public ReactiveLiveData(@NonNull T value) { + setValue(value); + } + + public ReactiveLiveData() { + super(); + } + + /** + * Observe without getting notified about the emitted values. + */ + public void observe(@NonNull LifecycleOwner owner) { + super.observe(owner, val -> { + // Nothing to do… + }); + } + + /** + * Observe without getting getting the emitted value. + */ + public void observe(@NonNull LifecycleOwner owner, @NonNull Runnable runnable) { + super.observe(owner, val -> runnable.run()); + } + + /** + * Cancel observation directly after one value has been emitted. + */ + public void observeOnce(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) { + final var internalObserver = new Observer<T>() { + @Override + public void onChanged(T result) { + removeObserver(this); + observer.onChanged(result); + } + }; + + observe(owner, internalObserver); + } + + /** + * @see Transformations#map(LiveData, Function) + */ + @NonNull + @Override + public <Y> ReactiveLiveData<Y> map(@NonNull Function<T, Y> mapFunction) { + return new MapLiveData<>(this, mapFunction); + } + + /** + * @see #map(Function) but the mapFunction will be executed on the given executor + */ + public <Y> ReactiveLiveData<Y> map(@NonNull Function<T, Y> mapFunction, @NonNull ExecutorService executor) { + return new MapLiveData<>(this, mapFunction, executor); + } + + /** + * @see Transformations#switchMap(LiveData, Function) + */ + @NonNull + @Override + public <Y> ReactiveLiveData<Y> flatMap(@NonNull Function<T, LiveData<Y>> flatMapFunction) { + return new FlatMapLiveData<>(this, flatMapFunction); + } + + @NonNull + @Override + public <Y> ReactiveLiveData<Y> flatMap(@NonNull Supplier<LiveData<Y>> switchMapSupplier) { + return new FlatMapLiveData<>(this, switchMapSupplier); + } + + /** + * @see Transformations#distinctUntilChanged(LiveData) + */ + @NonNull + @Override + public ReactiveLiveData<T> distinctUntilChanged() { + return new DistinctUntilChangedLiveData<>(this); + } + + @NonNull + public ReactiveLiveData<T> filter(@NonNull Predicate<T> predicate) { + return new FilterLiveData<>(this, predicate); + } + + @NonNull + @Override + public ReactiveLiveData<T> filter(@NonNull Supplier<Boolean> supplier) { + return new FilterLiveData<>(this, supplier); + } + + @NonNull + @Override + public ReactiveLiveData<T> tap(@NonNull Consumer<T> consumer) { + return new TapLiveData<>(this, consumer); + } + + @NonNull + @Override + public ReactiveLiveData<T> tap(@NonNull Runnable runnable) { + return new TapLiveData<>(this, runnable); + } + + /** + * @see #tap(Consumer) but the tap consumer will be executed on the given executor + */ + public ReactiveLiveData<T> tap(@NonNull Consumer<T> consumer, @NonNull ExecutorService executor) { + return new TapLiveData<>(this, consumer, executor); + } + + public ReactiveLiveData<T> tap(@NonNull Runnable runnable, @NonNull ExecutorService executor) { + return new TapLiveData<>(this, runnable, executor); + } + + @NonNull + @Override + public ReactiveLiveData<T> merge(@NonNull Supplier<LiveData<T>> secondSource) { + return new MergeLiveData<>(this, secondSource); + } + + @NonNull + @Override + public ReactiveLiveData<T> take(int limit) { + return new TakeLiveData<>(this, limit); + } + + @NonNull + @Override + public <Y> ReactiveLiveData<Pair<T, Y>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction) { + return new DoubleCombinatorLiveData<>(this, secondSourceFunction); + } + + @NonNull + @Override + public <Y> ReactiveLiveData<Pair<T, Y>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier) { + return new DoubleCombinatorLiveData<>(this, secondSourceSupplier); + } + + @NonNull + @Override + public <Y, Z> ReactiveLiveData<Triple<T, Y, Z>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) { + return new TripleCombinatorLiveData<>(this, secondSourceFunction, thirdSourceFunction); + } + + @NonNull + @Override + public <Y, Z> ReactiveLiveData<Triple<T, Y, Z>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier) { + return new TripleCombinatorLiveData<>(this, secondSourceFunction, thirdSourceSupplier); + } + + @NonNull + @Override + public <Y, Z> ReactiveLiveData<Triple<T, Y, Z>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) { + return new TripleCombinatorLiveData<>(this, secondSourceSupplier, thirdSourceFunction); + } + + @NonNull + @Override + public <Y, Z> ReactiveLiveData<Triple<T, Y, Z>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier) { + return new TripleCombinatorLiveData<>(this, secondSourceSupplier, thirdSourceSupplier); + } + + @NonNull + @Override + public ReactiveLiveData<T> debounce(long timeout, @NonNull ChronoUnit timeUnit) { + return new DebounceLiveData<>(this, timeout, timeUnit); + } + + @NonNull + @Override + public ReactiveLiveData<T> debounce(long timeout) { + return new DebounceLiveData<>(this, timeout); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataBuilder.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataBuilder.java new file mode 100644 index 000000000..e5fcb319e --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataBuilder.java @@ -0,0 +1,130 @@ +package it.niedermann.android.reactivelivedata; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; +import androidx.core.util.Consumer; +import androidx.core.util.Pair; +import androidx.core.util.Predicate; +import androidx.core.util.Supplier; +import androidx.lifecycle.LiveData; + +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import kotlin.Triple; + +/** + * Partial implementation of <a href="https://reactivex.io/documentation/operators.html">ReactiveX</a> features + */ +public interface ReactiveLiveDataBuilder<T> { + + /** + * @see <a href="https://reactivex.io/documentation/operators/map.html">ReactiveX#map</a> + */ + @NonNull + <Y> ReactiveLiveDataBuilder<Y> map(@NonNull Function<T, Y> mapFunction); + + /** + * @see <a href="https://reactivex.io/documentation/operators/flatmap.html">ReactiveX#flatmap</a> + */ + @NonNull + <Y> ReactiveLiveDataBuilder<Y> flatMap(@NonNull Function<T, LiveData<Y>> flatMapFunction); + + /** + * @see #flatMap(Function) + */ + @NonNull + <Y> ReactiveLiveDataBuilder<Y> flatMap(@NonNull Supplier<LiveData<Y>> flatMapSupplier); + + /** + * @see <a href="https://reactivex.io/documentation/operators/distinct.html">ReactiveX#distinct</a> + */ + @NonNull + ReactiveLiveDataBuilder<T> distinctUntilChanged(); + + /** + * @see <a href="https://reactivex.io/documentation/operators/filter.html">ReactiveX#filter</a> + */ + @NonNull + ReactiveLiveDataBuilder<T> filter(@NonNull Predicate<T> predicate); + + /** + * @see #filter(Predicate) + */ + @NonNull + ReactiveLiveDataBuilder<T> filter(@NonNull Supplier<Boolean> supplier); + + /** + * @see <a href="https://reactivex.io/documentation/operators/do.html">ReactiveX#do</a> + */ + @NonNull + ReactiveLiveDataBuilder<T> tap(@NonNull Consumer<T> consumer); + + /** + * @see #tap(Consumer) + */ + @NonNull + ReactiveLiveDataBuilder<T> tap(@NonNull Runnable runnable); + + /** + * @see <a href="https://reactivex.io/documentation/operators/merge.html">ReactiveX#merge</a> + */ + @NonNull + ReactiveLiveData<T> merge(@NonNull Supplier<LiveData<T>> liveData); + + /** + * @see <a href="https://reactivex.io/documentation/operators/take.html">ReactiveX#take</a> + */ + @NonNull + ReactiveLiveDataBuilder<T> take(int limit); + + /** + * @see <a href="https://reactivex.io/documentation/operators/combinelatest.html">ReactiveX#combinelatest</a> + */ + @NonNull + <Y> ReactiveLiveDataBuilder<Pair<T, Y>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction); + + /** + * @see #combineWith(Function) + */ + @NonNull + <Y> ReactiveLiveDataBuilder<Pair<T, Y>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier); + + /** + * @see <a href="https://reactivex.io/documentation/operators/combinelatest.html">ReactiveX#combinelatest</a> + */ + @NonNull + <Y, Z> ReactiveLiveDataBuilder<Triple<T, Y, Z>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Function<T, LiveData<Z>> thirdSourceFunction); + + /** + * @see #combineWith(Function) + */ + @NonNull + <Y, Z> ReactiveLiveDataBuilder<Triple<T, Y, Z>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier); + + /** + * @see #combineWith(Function) + */ + @NonNull + <Y, Z> ReactiveLiveDataBuilder<Triple<T, Y, Z>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Function<T, LiveData<Z>> thirdSourceFunction); + + /** + * @see #combineWith(Function) + */ + @NonNull + <Y, Z> ReactiveLiveDataBuilder<Triple<T, Y, Z>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier); + + /** + * @see <a href="https://reactivex.io/documentation/operators/debounce.html">ReactiveX#debounce</a>> + */ + @NonNull + ReactiveLiveDataBuilder<T> debounce(long timeout, @NonNull ChronoUnit timeUnit); + + /** + * @param timeout defaults to {@link TimeUnit#MILLISECONDS} + * + * @see #debounce(long, ChronoUnit) + */ + @NonNull + ReactiveLiveDataBuilder<T> debounce(long timeout); +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorLiveData.java new file mode 100644 index 000000000..63c704546 --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorLiveData.java @@ -0,0 +1,20 @@ +package it.niedermann.android.reactivelivedata.combinator; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; +import androidx.core.util.Pair; +import androidx.core.util.Supplier; +import androidx.lifecycle.LiveData; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; + +public class DoubleCombinatorLiveData<T, Y> extends ReactiveLiveData<Pair<T, Y>> { + + public DoubleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<Y>> secondSourceSupplier) { + this(source, val -> secondSourceSupplier.get()); + } + + public DoubleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Function<T, LiveData<Y>> secondSourceFunction) { + addSource(source, new DoubleCombinatorObserver<>(this, secondSourceFunction)); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorObserver.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorObserver.java new file mode 100644 index 000000000..18c2affbb --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorObserver.java @@ -0,0 +1,45 @@ +package it.niedermann.android.reactivelivedata.combinator; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; +import androidx.core.util.Pair; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.Observer; + +class DoubleCombinatorObserver<T, Y> implements Observer<T> { + private final MediatorLiveData<Pair<T, Y>> mediator; + private final Function<T, LiveData<Y>> secondSourceFunction; + private T value1; + private Y value2; + + private LiveData<Y> secondSource; + + private boolean value1emitted = false; + private boolean value2emitted = false; + + public DoubleCombinatorObserver(@NonNull MediatorLiveData<Pair<T, Y>> mediator, @NonNull Function<T, LiveData<Y>> secondSourceFunction) { + this.mediator = mediator; + this.secondSourceFunction = secondSourceFunction; + } + + @Override + public void onChanged(T emittedValue1) { + value1 = emittedValue1; + value1emitted = true; + if (value2emitted) { + mediator.setValue(new Pair<>(value1, value2)); + } + + if (secondSource == null) { + secondSource = secondSourceFunction.apply(emittedValue1); + mediator.addSource(secondSource, val2 -> { + value2 = val2; + value2emitted = true; + if (value1emitted) { + mediator.setValue(new Pair<>(value1, value2)); + } + }); + } + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorLiveData.java new file mode 100644 index 000000000..de0353c42 --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorLiveData.java @@ -0,0 +1,28 @@ +package it.niedermann.android.reactivelivedata.combinator; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; +import androidx.core.util.Supplier; +import androidx.lifecycle.LiveData; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import kotlin.Triple; + +public class TripleCombinatorLiveData<T, Y, Z> extends ReactiveLiveData<Triple<T, Y, Z>> { + + public TripleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier) { + this(source, val -> secondSourceSupplier.get(), val -> thirdSourceSupplier.get()); + } + + public TripleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier) { + this(source, secondSourceFunction, val -> thirdSourceSupplier.get()); + } + + public TripleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) { + this(source, val -> secondSourceSupplier.get(), thirdSourceFunction); + } + + public TripleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) { + addSource(source, new TripleCombinatorObserver<>(this, secondSourceFunction, thirdSourceFunction)); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorObserver.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorObserver.java new file mode 100644 index 000000000..f146fa9cc --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorObserver.java @@ -0,0 +1,62 @@ +package it.niedermann.android.reactivelivedata.combinator; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.Observer; + +import kotlin.Triple; + +class TripleCombinatorObserver<T, Y, Z> implements Observer<T> { + private final MediatorLiveData<Triple<T, Y, Z>> mediator; + private final Function<T, LiveData<Y>> secondSourceFunction; + private final Function<T, LiveData<Z>> thirdSourceFunction; + private T value1; + private Y value2; + private Z value3; + + private LiveData<Y> secondSource; + private LiveData<Z> thirdSource; + + private boolean value1emitted = false; + private boolean value2emitted = false; + private boolean value3emitted = false; + + public TripleCombinatorObserver(@NonNull MediatorLiveData<Triple<T, Y, Z>> mediator, @NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) { + this.mediator = mediator; + this.secondSourceFunction = secondSourceFunction; + this.thirdSourceFunction = thirdSourceFunction; + } + + @Override + public void onChanged(T emittedValue1) { + value1 = emittedValue1; + value1emitted = true; + if (value2emitted && value3emitted) { + mediator.setValue(new Triple<>(value1, value2, value3)); + } + + if (secondSource == null) { + secondSource = secondSourceFunction.apply(emittedValue1); + mediator.addSource(secondSource, val2 -> { + value2 = val2; + value2emitted = true; + if (value1emitted && value3emitted) { + mediator.setValue(new Triple<>(value1, value2, value3)); + } + }); + } + + if (thirdSource == null) { + thirdSource = thirdSourceFunction.apply(emittedValue1); + mediator.addSource(thirdSource, val3 -> { + value3 = val3; + value3emitted = true; + if (value1emitted && value2emitted) { + mediator.setValue(new Triple<>(value1, value2, value3)); + } + }); + } + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceLiveData.java new file mode 100644 index 000000000..22a67d59a --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceLiveData.java @@ -0,0 +1,19 @@ +package it.niedermann.android.reactivelivedata.debounce; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import java.time.temporal.ChronoUnit; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; + +public class DebounceLiveData<T> extends ReactiveLiveData<T> { + + public DebounceLiveData(@NonNull LiveData<T> source, long timeout) { + this(source, timeout, ChronoUnit.MILLIS); + } + + public DebounceLiveData(@NonNull LiveData<T> source, long timeout, @NonNull ChronoUnit timeUnit) { + addSource(source, new DebounceObserver<>(this, timeout, timeUnit)); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceObserver.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceObserver.java new file mode 100644 index 000000000..4d18cfa0e --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceObserver.java @@ -0,0 +1,79 @@ +package it.niedermann.android.reactivelivedata.debounce; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.Observer; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +class DebounceObserver<T> implements Observer<T> { + private final MediatorLiveData<T> mediator; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final long timeout; + private final ChronoUnit timeUnit; + private T lastEmittedValue = null; + private Instant lastEmit = Instant.now(); + private boolean firstEmit = true; + private Future<?> scheduledRecheck; + + public DebounceObserver(@NonNull MediatorLiveData<T> mediator, long timeout, @NonNull ChronoUnit timeUnit) { + this.mediator = mediator; + this.timeout = timeout; + this.timeUnit = timeUnit; + } + + @Override + public void onChanged(T value) { + final var now = Instant.now(); + + if (firstEmit) { + firstEmit = false; + emitValue(value, now); + } else { + if (lastEmit.isBefore(now.minus(timeout, timeUnit))) { + emitValue(value, now); + } else { + scheduleRecheck(value, getRemainingTimeToNextTimeout(now, lastEmit)); + } + } + } + + private void emitValue(T value, @NonNull Instant lastEmit) { + cancelScheduledRecheck(); + mediator.postValue(value); + this.lastEmit = lastEmit; + } + + private Duration getRemainingTimeToNextTimeout(@NonNull Instant now, @NonNull Instant lastEmit) { + final var millisSinceLastEmit = now.toEpochMilli() - lastEmit.toEpochMilli(); + final var millisToNextEmit = Duration.of(timeout, timeUnit).toMillis() - millisSinceLastEmit; + return Duration.ofMillis(millisToNextEmit); + } + + private void cancelScheduledRecheck() { + if (scheduledRecheck != null) { + scheduledRecheck.cancel(true); + } + } + + private synchronized void scheduleRecheck(T newValue, @NonNull Duration sleep) { + cancelScheduledRecheck(); + scheduledRecheck = executor.submit(() -> { + try { + Thread.sleep(sleep.toMillis()); + if (!Objects.equals(lastEmittedValue, newValue)) { + mediator.postValue(newValue); + lastEmittedValue = newValue; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } +}
\ No newline at end of file diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/distinct/DistinctUntilChangedLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/distinct/DistinctUntilChangedLiveData.java new file mode 100644 index 000000000..6ec85ed85 --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/distinct/DistinctUntilChangedLiveData.java @@ -0,0 +1,14 @@ +package it.niedermann.android.reactivelivedata.distinct; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Transformations; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; + +public class DistinctUntilChangedLiveData<T> extends ReactiveLiveData<T> { + + public DistinctUntilChangedLiveData(@NonNull LiveData<T> source) { + super(Transformations.distinctUntilChanged(source)); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/filter/FilterLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/filter/FilterLiveData.java new file mode 100644 index 000000000..fac27030f --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/filter/FilterLiveData.java @@ -0,0 +1,23 @@ +package it.niedermann.android.reactivelivedata.filter; + +import androidx.annotation.NonNull; +import androidx.core.util.Predicate; +import androidx.core.util.Supplier; +import androidx.lifecycle.LiveData; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; + +public class FilterLiveData<T> extends ReactiveLiveData<T> { + + public FilterLiveData(@NonNull LiveData<T> source, @NonNull Supplier<Boolean> supplier) { + this(source, val -> supplier.get()); + } + + public FilterLiveData(@NonNull LiveData<T> source, @NonNull Predicate<T> predicate) { + addSource(source, val -> { + if (predicate.test(val)) { + setValue(val); + } + }); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/flatmap/FlatMapLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/flatmap/FlatMapLiveData.java new file mode 100644 index 000000000..5212ea7fa --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/flatmap/FlatMapLiveData.java @@ -0,0 +1,20 @@ +package it.niedermann.android.reactivelivedata.flatmap; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; +import androidx.core.util.Supplier; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Transformations; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; + +public class FlatMapLiveData<T, Y> extends ReactiveLiveData<Y> { + + public FlatMapLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<Y>> switchMapSupplier) { + this(source, val -> switchMapSupplier.get()); + } + + public FlatMapLiveData(@NonNull LiveData<T> source, @NonNull Function<T, LiveData<Y>> flatMapFunction) { + super(Transformations.switchMap(source, flatMapFunction)); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/map/MapLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/map/MapLiveData.java new file mode 100644 index 000000000..eaeb2b435 --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/map/MapLiveData.java @@ -0,0 +1,21 @@ +package it.niedermann.android.reactivelivedata.map; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Transformations; + +import java.util.concurrent.ExecutorService; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; + +public class MapLiveData<T, Y> extends ReactiveLiveData<Y> { + + public MapLiveData(@NonNull LiveData<T> source, @NonNull Function<T, Y> mapFunction) { + super(Transformations.map(source, mapFunction)); + } + + public MapLiveData(@NonNull LiveData<T> source, @NonNull Function<T, Y> mapFunction, @NonNull ExecutorService executor) { + addSource(source, val -> executor.submit(() -> postValue(mapFunction.apply(val)))); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/merge/MergeLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/merge/MergeLiveData.java new file mode 100644 index 000000000..cc3c1ddd6 --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/merge/MergeLiveData.java @@ -0,0 +1,15 @@ +package it.niedermann.android.reactivelivedata.merge; + +import androidx.annotation.NonNull; +import androidx.core.util.Supplier; +import androidx.lifecycle.LiveData; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; + +public class MergeLiveData<T> extends ReactiveLiveData<T> { + + public MergeLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<T>> secondSource) { + addSource(source, this::setValue); + addSource(secondSource.get(), this::setValue); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeLiveData.java new file mode 100644 index 000000000..0ca536db2 --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeLiveData.java @@ -0,0 +1,13 @@ +package it.niedermann.android.reactivelivedata.take; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; + +public class TakeLiveData<T> extends ReactiveLiveData<T> { + + public TakeLiveData(@NonNull LiveData<T> source, int limit) { + addSource(source, new TakeObserver<>(this, limit)); + } +} diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeObserver.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeObserver.java new file mode 100644 index 000000000..0f1f4e576 --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeObserver.java @@ -0,0 +1,37 @@ +package it.niedermann.android.reactivelivedata.take; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.Observer; + +class TakeObserver<T> implements Observer<T> { + private final MediatorLiveData<T> mediator; + private final int limit; + private int counter = 0; + + public TakeObserver(@NonNull MediatorLiveData<T> mediator, int limit) { + if (limit == Integer.MAX_VALUE) { + throw new RuntimeException("limit must be lower than Integer.MAX_VALUE"); + } + + if (limit < 1) { + throw new RuntimeException("limit must be 1 or higher"); + } + + this.mediator = mediator; + this.limit = limit; + } + + @Override + public void onChanged(T value) { + if (counter < limit) { + mediator.setValue(value); + } + counter++; + + // Prevent integer overflow + if (counter == limit + 1) { + counter = limit; + } + } +}
\ No newline at end of file diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/tap/TapLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/tap/TapLiveData.java new file mode 100644 index 000000000..c9d08a98d --- /dev/null +++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/tap/TapLiveData.java @@ -0,0 +1,34 @@ +package it.niedermann.android.reactivelivedata.tap; + +import androidx.annotation.NonNull; +import androidx.core.util.Consumer; +import androidx.lifecycle.LiveData; + +import java.util.concurrent.ExecutorService; + +import it.niedermann.android.reactivelivedata.map.MapLiveData; + +public class TapLiveData<T> extends MapLiveData<T, T> { + + public TapLiveData(@NonNull LiveData<T> source, @NonNull Runnable runnable) { + this(source, val -> runnable.run()); + } + + public TapLiveData(@NonNull LiveData<T> source, @NonNull Consumer<T> consumer) { + super(source, val -> { + consumer.accept(val); + return val; + }); + } + + public TapLiveData(@NonNull LiveData<T> source, @NonNull Runnable runnable, @NonNull ExecutorService executor) { + this(source, val -> runnable.run(), executor); + } + + public TapLiveData(@NonNull LiveData<T> source, @NonNull Consumer<T> consumer, @NonNull ExecutorService executor) { + super(source, val -> { + consumer.accept(val); + return val; + }, executor); + } +} diff --git a/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataTest.java b/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataTest.java new file mode 100644 index 000000000..a644a8ccf --- /dev/null +++ b/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataTest.java @@ -0,0 +1,188 @@ +package it.niedermann.android.reactivelivedata; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static it.niedermann.android.reactivelivedata.TestUtil.getOrAwaitValue; + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule; +import androidx.core.util.Pair; +import androidx.lifecycle.MutableLiveData; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.concurrent.TimeUnit; + +@RunWith(RobolectricTestRunner.class) +public class ReactiveLiveDataTest { + + @Rule + public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule(); + + @Test + public void filter() throws InterruptedException { + final var s1$ = new MutableLiveData<Integer>(); + + final var reactive1$ = new ReactiveLiveData<>(s1$) + .filter(val -> val < 2); + + s1$.postValue(1); + assertEquals(Integer.valueOf(1), getOrAwaitValue(reactive1$)); + + s1$.postValue(2); + assertEquals(Integer.valueOf(1), getOrAwaitValue(reactive1$)); + } + + @Test + public void map() throws InterruptedException { + final var s1$ = new MutableLiveData<Integer>(); + + final var reactive1$ = new ReactiveLiveData<>(s1$) + .map(val -> val * 2); + + s1$.postValue(1); + assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$)); + + s1$.postValue(2); + assertEquals(Integer.valueOf(4), getOrAwaitValue(reactive1$)); + + s1$.postValue(3); + assertEquals(Integer.valueOf(6), getOrAwaitValue(reactive1$)); + } + + @Test + public void flatMap() throws InterruptedException { + final var s0$ = new MutableLiveData<Void>(null); + final var s1$ = new MutableLiveData<>("Foo"); + final var s2$ = new MutableLiveData<>("Bar"); + + final var reactive1$ = new ReactiveLiveData<>(s0$) + .flatMap(() -> s1$); + + final var reactive2$ = new ReactiveLiveData<>(s0$) + .flatMap(() -> s2$); + + assertEquals("Foo", getOrAwaitValue(reactive1$)); + assertEquals("Bar", getOrAwaitValue(reactive2$)); + } + + @Test + public void flatMap_chained() throws InterruptedException { + final var s0$ = new MutableLiveData<Void>(null); + final var s1$ = new MutableLiveData<>("Foo"); + final var s2$ = new MutableLiveData<>("Bar"); + final var s3$ = new MutableLiveData<>("Baz"); + + final var reactive1$ = new ReactiveLiveData<>(s0$) + .flatMap(() -> s1$) + .flatMap(val -> "Foo".equals(val) ? s2$ : s3$); + + assertEquals("Bar", getOrAwaitValue(reactive1$)); + + s1$.postValue("Qux"); + + assertEquals("Baz", getOrAwaitValue(reactive1$)); + } + + @Test + public void combineWith() throws InterruptedException { + final var s1$ = new MutableLiveData<>(5); + final var s2$ = new MutableLiveData<>("Foo"); + + final var reactive1$ = new ReactiveLiveData<>(s1$) + .combineWith(val -> s2$); + + assertEquals(new Pair<>(5, "Foo"), getOrAwaitValue(reactive1$)); + } + + @Test + public void merge() throws InterruptedException { + final var s1$ = new MutableLiveData<>(5); + final var s2$ = new MutableLiveData<>(9); + + final var reactive1$ = new ReactiveLiveData<>(s1$) + .merge(() -> s2$); + + assertEquals(Integer.valueOf(5), getOrAwaitValue(reactive1$)); + assertEquals(Integer.valueOf(9), getOrAwaitValue(reactive1$)); + } + + @Test + public void take() throws InterruptedException { + assertThrows(RuntimeException.class, () -> new ReactiveLiveData<>().take(Integer.MAX_VALUE)); + assertThrows(RuntimeException.class, () -> new ReactiveLiveData<>().take(0)); + + final var s1$ = new MutableLiveData<>(0); + + final var reactive1$ = new ReactiveLiveData<>(s1$) + .take(3); + + assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$)); + + s1$.setValue(1); + assertEquals(Integer.valueOf(1), getOrAwaitValue(reactive1$)); + + s1$.setValue(2); + assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$)); + + s1$.setValue(3); + assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$)); + + s1$.setValue(4); + assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$)); + } + + @Test + public void debounce() throws InterruptedException { + final var s1$ = new MutableLiveData<>(0); + + final var reactive1$ = new ReactiveLiveData<>(s1$) + .debounce(120); + + assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 10, TimeUnit.MILLISECONDS)); + + for (int i = 1; i <= 6; i++) { + Thread.sleep(50); + s1$.setValue(i); + switch (i) { + case 1: + case 2: + assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 10, TimeUnit.MILLISECONDS)); + break; + case 3: + case 4: + case 5: + assertEquals(Integer.valueOf(3), getOrAwaitValue(reactive1$, 10, TimeUnit.MILLISECONDS)); + break; + case 6: + assertEquals(Integer.valueOf(6), getOrAwaitValue(reactive1$, 10, TimeUnit.MILLISECONDS)); + break; + default: + throw new IllegalStateException(); + } + } + } + + @Test + public void debounce_shouldPickUpChangesAfterTheTimeoutDirectly() throws InterruptedException { + final var s1$ = new MutableLiveData<>(0); + + final var reactive1$ = new ReactiveLiveData<>(s1$) + .debounce(120); + + assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 0, TimeUnit.MILLISECONDS)); + + Thread.sleep(50); + s1$.setValue(1); + assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 0, TimeUnit.MILLISECONDS)); + + Thread.sleep(50); + s1$.setValue(2); + assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 0, TimeUnit.MILLISECONDS)); + + Thread.sleep(50); + assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$, 0, TimeUnit.MILLISECONDS)); + } +} diff --git a/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/TestUtil.java b/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/TestUtil.java new file mode 100644 index 000000000..8f47e15f7 --- /dev/null +++ b/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/TestUtil.java @@ -0,0 +1,45 @@ +package it.niedermann.android.reactivelivedata; + +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class TestUtil { + + private TestUtil() { + // Util class + } + + /** + * @see #getOrAwaitValue(LiveData, long, TimeUnit) + */ + public static <T> T getOrAwaitValue(final LiveData<T> liveData) throws InterruptedException { + return getOrAwaitValue(liveData, 2, TimeUnit.SECONDS); + } + + /** + * @see <a href="https://gist.github.com/JoseAlcerreca/1e9ee05dcdd6a6a6fa1cbfc125559bba">Source</a> + */ + public static <T> T getOrAwaitValue(final LiveData<T> liveData, long timeout, TimeUnit unit) throws InterruptedException { + final var data = new Object[1]; + final var latch = new CountDownLatch(1); + final var observer = new Observer<T>() { + @Override + public void onChanged(@Nullable T o) { + data[0] = o; + latch.countDown(); + liveData.removeObserver(this); + } + }; + liveData.observeForever(observer); + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(timeout, unit)) { + throw new RuntimeException("LiveData value was never set."); + } + //noinspection unchecked + return (T) data[0]; + } +} diff --git a/settings.gradle b/settings.gradle index edf1d2bdb..6b84d234f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ include ':app' include ':cross-tab-drag-and-drop' include ':tab-layout-helper' +include ':reactive-livedata'
\ No newline at end of file |